OLD | NEW |
---|---|
1 /* | 1 /* |
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. | 2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. |
3 * | 3 * |
4 * Redistribution and use in source and binary forms, with or without | 4 * Redistribution and use in source and binary forms, with or without |
5 * modification, are permitted provided that the following conditions | 5 * modification, are permitted provided that the following conditions |
6 * are met: | 6 * are met: |
7 * 1. Redistributions of source code must retain the above copyright | 7 * 1. Redistributions of source code must retain the above copyright |
8 * notice, this list of conditions and the following disclaimer. | 8 * notice, this list of conditions and the following disclaimer. |
9 * 2. Redistributions in binary form must reproduce the above copyright | 9 * 2. Redistributions in binary form must reproduce the above copyright |
10 * notice, this list of conditions and the following disclaimer in the | 10 * notice, this list of conditions and the following disclaimer in the |
(...skipping 24 matching lines...) Expand all Loading... | |
35 #include "core/inspector/WorkerThreadDebugger.h" | 35 #include "core/inspector/WorkerThreadDebugger.h" |
36 #include "core/workers/WorkerBackingThread.h" | 36 #include "core/workers/WorkerBackingThread.h" |
37 #include "core/workers/WorkerClients.h" | 37 #include "core/workers/WorkerClients.h" |
38 #include "core/workers/WorkerReportingProxy.h" | 38 #include "core/workers/WorkerReportingProxy.h" |
39 #include "core/workers/WorkerThreadStartupData.h" | 39 #include "core/workers/WorkerThreadStartupData.h" |
40 #include "platform/ThreadSafeFunctional.h" | 40 #include "platform/ThreadSafeFunctional.h" |
41 #include "platform/WaitableEvent.h" | 41 #include "platform/WaitableEvent.h" |
42 #include "platform/WebThreadSupportingGC.h" | 42 #include "platform/WebThreadSupportingGC.h" |
43 #include "platform/heap/SafePoint.h" | 43 #include "platform/heap/SafePoint.h" |
44 #include "platform/heap/ThreadState.h" | 44 #include "platform/heap/ThreadState.h" |
45 #include "platform/scheduler/CancellableTaskFactory.h" | |
45 #include "platform/weborigin/KURL.h" | 46 #include "platform/weborigin/KURL.h" |
46 #include "public/platform/WebThread.h" | 47 #include "public/platform/WebThread.h" |
47 #include "wtf/Functional.h" | 48 #include "wtf/Functional.h" |
48 #include "wtf/Noncopyable.h" | 49 #include "wtf/Noncopyable.h" |
49 #include "wtf/text/WTFString.h" | 50 #include "wtf/text/WTFString.h" |
50 #include <limits.h> | 51 #include <limits.h> |
51 | 52 |
52 namespace blink { | 53 namespace blink { |
53 | 54 |
54 class WorkerThread::WorkerMicrotaskRunner : public WebThread::TaskObserver { | 55 // TODO(nhiroki): Adjust the delay based on UMA. |
56 const long long kForceTerminationDelayInMs = 3000; // 3 secs | |
57 | |
58 // ForceTerminationTask is used for posting a delayed task to terminate an | |
59 // isolate from the main thread. This task is expected to run when the shutdown | |
60 // sequence does not start in a certain time period because of an inifite loop | |
61 // in the JS execution context etc. When the shutdown sequence is started before | |
62 // this task runs, the task is simply cancelled. | |
63 class WorkerThread::ForceTerminationTask final { | |
64 public: | |
65 static PassOwnPtr<ForceTerminationTask> create(WorkerThread* workerThread) | |
66 { | |
67 return adoptPtr(new ForceTerminationTask(workerThread)); | |
68 } | |
69 | |
70 void schedule() | |
71 { | |
72 DCHECK(isMainThread()); | |
73 Platform::current()->mainThread()->getWebTaskRunner()->postDelayedTask(B LINK_FROM_HERE, m_cancellableTaskFactory->cancelAndCreate(), m_workerThread->m_f orceTerminationDelayInMs); | |
74 } | |
75 | |
76 private: | |
77 explicit ForceTerminationTask(WorkerThread* workerThread) | |
78 : m_workerThread(workerThread) | |
79 { | |
80 DCHECK(isMainThread()); | |
81 m_cancellableTaskFactory = CancellableTaskFactory::create(this, &ForceTe rminationTask::run); | |
82 } | |
83 | |
84 void run() | |
85 { | |
86 DCHECK(isMainThread()); | |
87 MutexLocker lock(m_workerThread->m_threadStateMutex); | |
88 if (m_workerThread->m_readyToShutdown) { | |
89 // Shutdown sequence is now running. Just return. | |
90 return; | |
91 } | |
92 | |
93 m_workerThread->m_workerGlobalScope->scriptController()->willScheduleExe cutionTermination(); | |
yhirano
2016/05/31 04:48:07
[optional] It might be good to make a function for
nhiroki
2016/05/31 06:08:56
Done.
| |
94 m_workerThread->isolate()->TerminateExecution(); | |
95 DCHECK_EQ(WorkerThread::ExitCode::NotTerminated, m_workerThread->m_exitC ode); | |
96 m_workerThread->m_exitCode = WorkerThread::ExitCode::AsyncForciblyTermin ated; | |
97 } | |
98 | |
99 WorkerThread* m_workerThread; | |
100 OwnPtr<CancellableTaskFactory> m_cancellableTaskFactory; | |
101 }; | |
102 | |
103 class WorkerThread::WorkerMicrotaskRunner final : public WebThread::TaskObserver { | |
55 public: | 104 public: |
56 explicit WorkerMicrotaskRunner(WorkerThread* workerThread) | 105 explicit WorkerMicrotaskRunner(WorkerThread* workerThread) |
57 : m_workerThread(workerThread) | 106 : m_workerThread(workerThread) |
58 { | 107 { |
59 } | 108 } |
60 | 109 |
61 void willProcessTask() override | 110 void willProcessTask() override |
62 { | 111 { |
63 // No tasks should get executed after we have closed. | 112 // No tasks should get executed after we have closed. |
64 DCHECK(!m_workerThread->workerGlobalScope() || !m_workerThread->workerGl obalScope()->isClosing()); | 113 DCHECK(!m_workerThread->workerGlobalScope() || !m_workerThread->workerGl obalScope()->isClosing()); |
(...skipping 28 matching lines...) Expand all Loading... | |
93 } | 142 } |
94 | 143 |
95 static HashSet<WorkerThread*>& workerThreads() | 144 static HashSet<WorkerThread*>& workerThreads() |
96 { | 145 { |
97 DEFINE_STATIC_LOCAL(HashSet<WorkerThread*>, threads, ()); | 146 DEFINE_STATIC_LOCAL(HashSet<WorkerThread*>, threads, ()); |
98 return threads; | 147 return threads; |
99 } | 148 } |
100 | 149 |
101 WorkerThread::~WorkerThread() | 150 WorkerThread::~WorkerThread() |
102 { | 151 { |
152 DCHECK(isMainThread()); | |
103 MutexLocker lock(threadSetMutex()); | 153 MutexLocker lock(threadSetMutex()); |
104 DCHECK(workerThreads().contains(this)); | 154 DCHECK(workerThreads().contains(this)); |
105 workerThreads().remove(this); | 155 workerThreads().remove(this); |
156 | |
157 // TODO(nhiroki): Record how this thread is terminated (i.e. m_exitCode) | |
158 // in UMA. | |
159 DCHECK_NE(ExitCode::NotTerminated, m_exitCode); | |
106 } | 160 } |
107 | 161 |
108 void WorkerThread::start(PassOwnPtr<WorkerThreadStartupData> startupData) | 162 void WorkerThread::start(PassOwnPtr<WorkerThreadStartupData> startupData) |
109 { | 163 { |
110 DCHECK(isMainThread()); | 164 DCHECK(isMainThread()); |
111 | 165 |
112 if (m_started) | 166 if (m_started) |
113 return; | 167 return; |
114 | 168 |
115 m_started = true; | 169 m_started = true; |
116 workerBackingThread().backingThread().postTask(BLINK_FROM_HERE, threadSafeBi nd(&WorkerThread::initializeOnWorkerThread, AllowCrossThreadAccess(this), passed (std::move(startupData)))); | 170 workerBackingThread().backingThread().postTask(BLINK_FROM_HERE, threadSafeBi nd(&WorkerThread::initializeOnWorkerThread, AllowCrossThreadAccess(this), passed (std::move(startupData)))); |
117 } | 171 } |
118 | 172 |
119 void WorkerThread::terminate() | 173 void WorkerThread::terminate() |
120 { | 174 { |
121 DCHECK(isMainThread()); | 175 DCHECK(isMainThread()); |
122 | 176 terminateInternal(TerminationMode::Graceful); |
123 // Prevent the deadlock between GC and an attempt to terminate a thread. | |
124 SafePointScope safePointScope(BlinkGC::HeapPointersOnStack); | |
125 | |
126 // Protect against this method, initializeOnWorkerThread() or termination | |
127 // via the global scope racing each other. | |
128 MutexLocker lock(m_threadStateMutex); | |
129 | |
130 // If terminate has already been called, just return. | |
131 if (m_terminated) | |
132 return; | |
133 m_terminated = true; | |
134 | |
135 // Signal the thread to notify that the thread's stopping. | |
136 if (m_terminationEvent) | |
137 m_terminationEvent->signal(); | |
138 | |
139 // If the worker thread was never initialized, don't start another | |
140 // shutdown, but still wait for the thread to signal when shutdown has | |
141 // completed on initializeOnWorkerThread(). | |
142 if (!m_workerGlobalScope) | |
143 return; | |
144 | |
145 // Determine if we should terminate the isolate so that the task can | |
146 // be handled by thread event loop. If script execution weren't forbidden, | |
147 // a while(1) loop in JS could keep the thread alive forever. | |
148 // | |
149 // (1) |m_readyToShutdown|: It this is set, the worker thread has already | |
150 // noticed that the thread is about to be terminated and the worker global | |
151 // scope is already disposed, so we don't have to explicitly terminate the | |
152 // isolate. | |
153 // | |
154 // (2) |workerScriptCount() == 1|: If other WorkerGlobalScopes are running | |
155 // on the worker thread, we should not terminate the isolate. This condition | |
156 // is not entirely correct because other scripts can be being initialized or | |
157 // terminated simuletaneously. Though this function itself is protected by a | |
158 // mutex, it is possible that |workerScriptCount()| here is not consistent | |
159 // with that in |initialize| and |shutdown|. | |
160 // | |
161 // (3) |m_runningDebuggerTask|: Terminating during debugger task may lead to | |
162 // crash due to heavy use of v8 api in debugger. Any debugger task is | |
163 // guaranteed to finish, so we can wait for the completion. | |
164 bool shouldTerminateIsolate = !m_readyToShutdown && (workerBackingThread().w orkerScriptCount() == 1) && !m_runningDebuggerTask; | |
165 | |
166 if (shouldTerminateIsolate) { | |
167 // TODO(yhirano): TerminateExecution should be called more | |
168 // carefully (https://crbug.com/413518). | |
169 m_workerGlobalScope->scriptController()->willScheduleExecutionTerminatio n(); | |
170 isolate()->TerminateExecution(); | |
171 } | |
172 | |
173 m_inspectorTaskRunner->kill(); | |
174 workerBackingThread().backingThread().postTask(BLINK_FROM_HERE, threadSafeBi nd(&WorkerThread::prepareForShutdownOnWorkerThread, AllowCrossThreadAccess(this) )); | |
175 workerBackingThread().backingThread().postTask(BLINK_FROM_HERE, threadSafeBi nd(&WorkerThread::performShutdownOnWorkerThread, AllowCrossThreadAccess(this))); | |
176 } | 177 } |
177 | 178 |
178 void WorkerThread::terminateAndWait() | 179 void WorkerThread::terminateAndWait() |
179 { | 180 { |
180 DCHECK(isMainThread()); | 181 DCHECK(isMainThread()); |
181 terminate(); | 182 |
183 // The main thread will be blocked, so asynchronous graceful shutdown does | |
184 // not work. | |
185 terminateInternal(TerminationMode::Forcible); | |
186 | |
187 // Terminating the worker thread that hasn't started yet does not run the | |
188 // shutdown sequence on the worker thread, so should not wait for the | |
189 // shutdown event. | |
190 if (m_exitCode == ExitCode::TerminatedBeforeStarting) | |
191 return; | |
182 m_shutdownEvent->wait(); | 192 m_shutdownEvent->wait(); |
183 } | 193 } |
184 | 194 |
185 void WorkerThread::terminateAndWaitForAllWorkers() | 195 void WorkerThread::terminateAndWaitForAllWorkers() |
186 { | 196 { |
187 DCHECK(isMainThread()); | 197 DCHECK(isMainThread()); |
188 | 198 |
189 // Keep this lock to prevent WorkerThread instances from being destroyed. | 199 // Keep this lock to prevent WorkerThread instances from being destroyed. |
190 MutexLocker lock(threadSetMutex()); | 200 MutexLocker lock(threadSetMutex()); |
191 HashSet<WorkerThread*> threads = workerThreads(); | 201 HashSet<WorkerThread*> threads = workerThreads(); |
202 | |
203 // The main thread will be blocked, so asynchronous graceful shutdown does | |
204 // not work. | |
192 for (WorkerThread* thread : threads) | 205 for (WorkerThread* thread : threads) |
193 thread->terminate(); | 206 thread->terminateInternal(TerminationMode::Forcible); |
194 | 207 |
195 for (WorkerThread* thread : threads) | 208 for (WorkerThread* thread : threads) { |
209 // Terminating the worker thread that hasn't started yet does not run | |
210 // the shutdown sequence on the worker thread, so should not wait for | |
211 // the shutdown event. | |
212 if (thread->m_exitCode == ExitCode::TerminatedBeforeStarting) | |
213 continue; | |
196 thread->m_shutdownEvent->wait(); | 214 thread->m_shutdownEvent->wait(); |
215 } | |
197 } | 216 } |
198 | 217 |
199 v8::Isolate* WorkerThread::isolate() | 218 v8::Isolate* WorkerThread::isolate() |
200 { | 219 { |
201 return workerBackingThread().isolate(); | 220 return workerBackingThread().isolate(); |
202 } | 221 } |
203 | 222 |
204 bool WorkerThread::isCurrentThread() | 223 bool WorkerThread::isCurrentThread() |
205 { | 224 { |
206 return m_started && workerBackingThread().backingThread().isCurrentThread(); | 225 return m_started && workerBackingThread().backingThread().isCurrentThread(); |
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
268 } | 287 } |
269 | 288 |
270 PlatformThreadId WorkerThread::platformThreadId() | 289 PlatformThreadId WorkerThread::platformThreadId() |
271 { | 290 { |
272 if (!m_started) | 291 if (!m_started) |
273 return 0; | 292 return 0; |
274 return workerBackingThread().backingThread().platformThread().threadId(); | 293 return workerBackingThread().backingThread().platformThread().threadId(); |
275 } | 294 } |
276 | 295 |
277 WorkerThread::WorkerThread(PassRefPtr<WorkerLoaderProxy> workerLoaderProxy, Work erReportingProxy& workerReportingProxy) | 296 WorkerThread::WorkerThread(PassRefPtr<WorkerLoaderProxy> workerLoaderProxy, Work erReportingProxy& workerReportingProxy) |
278 : m_inspectorTaskRunner(adoptPtr(new InspectorTaskRunner())) | 297 : m_forceTerminationDelayInMs(kForceTerminationDelayInMs) |
298 , m_inspectorTaskRunner(adoptPtr(new InspectorTaskRunner())) | |
279 , m_workerLoaderProxy(workerLoaderProxy) | 299 , m_workerLoaderProxy(workerLoaderProxy) |
280 , m_workerReportingProxy(workerReportingProxy) | 300 , m_workerReportingProxy(workerReportingProxy) |
281 , m_terminationEvent(adoptPtr(new WaitableEvent( | 301 , m_terminationEvent(adoptPtr(new WaitableEvent( |
282 WaitableEvent::ResetPolicy::Manual, | 302 WaitableEvent::ResetPolicy::Manual, |
283 WaitableEvent::InitialState::NonSignaled))) | 303 WaitableEvent::InitialState::NonSignaled))) |
284 , m_shutdownEvent(adoptPtr(new WaitableEvent( | 304 , m_shutdownEvent(adoptPtr(new WaitableEvent( |
285 WaitableEvent::ResetPolicy::Manual, | 305 WaitableEvent::ResetPolicy::Manual, |
286 WaitableEvent::InitialState::NonSignaled))) | 306 WaitableEvent::InitialState::NonSignaled))) |
287 { | 307 { |
288 MutexLocker lock(threadSetMutex()); | 308 MutexLocker lock(threadSetMutex()); |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
354 } | 374 } |
355 | 375 |
356 CachedMetadataHandler* handler = workerGlobalScope()->createWorkerScriptCach edMetadataHandler(scriptURL, cachedMetaData.get()); | 376 CachedMetadataHandler* handler = workerGlobalScope()->createWorkerScriptCach edMetadataHandler(scriptURL, cachedMetaData.get()); |
357 bool success = m_workerGlobalScope->scriptController()->evaluate(ScriptSourc eCode(sourceCode, scriptURL), nullptr, handler, v8CacheOptions); | 377 bool success = m_workerGlobalScope->scriptController()->evaluate(ScriptSourc eCode(sourceCode, scriptURL), nullptr, handler, v8CacheOptions); |
358 m_workerGlobalScope->didEvaluateWorkerScript(); | 378 m_workerGlobalScope->didEvaluateWorkerScript(); |
359 m_workerReportingProxy.didEvaluateWorkerScript(success); | 379 m_workerReportingProxy.didEvaluateWorkerScript(success); |
360 | 380 |
361 postInitialize(); | 381 postInitialize(); |
362 } | 382 } |
363 | 383 |
384 void WorkerThread::terminateInternal(TerminationMode mode) | |
385 { | |
386 // Prevent the deadlock between GC and an attempt to terminate a thread. | |
387 SafePointScope safePointScope(BlinkGC::HeapPointersOnStack); | |
388 | |
389 // Protect against this method, initializeOnWorkerThread() or termination | |
390 // via the global scope racing each other. | |
391 MutexLocker lock(m_threadStateMutex); | |
392 | |
393 // If terminate has already been called. | |
394 if (m_terminated) { | |
395 // The synchronous forcible termination request should overtake the | |
396 // scheduled termination task because the request will block the main | |
397 // thread and the scheduled termination task never runs. | |
398 if (mode == TerminationMode::Forcible && m_exitCode == ExitCode::NotTerm inated) { | |
399 DCHECK(m_scheduledForceTerminationTask); | |
400 m_scheduledForceTerminationTask.reset(); | |
401 m_workerGlobalScope->scriptController()->willScheduleExecutionTermin ation(); | |
402 isolate()->TerminateExecution(); | |
403 DCHECK_EQ(ExitCode::NotTerminated, m_exitCode); | |
404 m_exitCode = ExitCode::SyncForciblyTerminated; | |
405 } | |
406 return; | |
407 } | |
408 m_terminated = true; | |
409 | |
410 // Signal the thread to notify that the thread's stopping. | |
411 if (m_terminationEvent) | |
412 m_terminationEvent->signal(); | |
413 | |
414 // If the worker thread was never initialized, don't start another | |
415 // shutdown, but still wait for the thread to signal when shutdown has | |
416 // completed on initializeOnWorkerThread(). | |
417 if (!m_workerGlobalScope) { | |
418 DCHECK_EQ(ExitCode::NotTerminated, m_exitCode); | |
419 m_exitCode = ExitCode::TerminatedBeforeStarting; | |
420 return; | |
421 } | |
422 | |
423 // Determine if we should synchronously terminate or schedule to terminate | |
424 // the isolate so that the task can be handled by thread event loop. If | |
425 // script execution weren't forbidden, a while(1) loop in JS could keep | |
426 // the thread alive forever. | |
427 // | |
428 // (1) |m_readyToShutdown|: It this is set, the worker thread has already | |
429 // noticed that the thread is about to be terminated and the worker global | |
430 // scope is already disposed, so we don't have to explicitly terminate the | |
431 // isolate. | |
432 // | |
433 // (2) |workerScriptCount() == 1|: If other WorkerGlobalScopes are running | |
434 // on the worker thread, we should not terminate the isolate. This condition | |
435 // is not entirely correct because other scripts can be being initialized or | |
436 // terminated simuletaneously. Though this function itself is protected by a | |
437 // mutex, it is possible that |workerScriptCount()| here is not consistent | |
438 // with that in |initialize| and |shutdown|. | |
439 // | |
440 // (3) |m_runningDebuggerTask|: Terminating during debugger task may lead to | |
441 // crash due to heavy use of v8 api in debugger. Any debugger task is | |
442 // guaranteed to finish, so we can wait for the completion. | |
443 bool shouldScheduleToTerminateIsolate = !m_readyToShutdown && (workerBacking Thread().workerScriptCount() == 1) && !m_runningDebuggerTask; | |
444 | |
445 if (shouldScheduleToTerminateIsolate) { | |
446 if (mode == TerminationMode::Forcible) { | |
447 m_workerGlobalScope->scriptController()->willScheduleExecutionTermin ation(); | |
448 isolate()->TerminateExecution(); | |
449 DCHECK_EQ(ExitCode::NotTerminated, m_exitCode); | |
450 m_exitCode = ExitCode::SyncForciblyTerminated; | |
451 } else { | |
452 DCHECK_EQ(TerminationMode::Graceful, mode); | |
453 m_scheduledForceTerminationTask = ForceTerminationTask::create(this) ; | |
454 m_scheduledForceTerminationTask->schedule(); | |
455 } | |
456 } | |
457 | |
458 m_inspectorTaskRunner->kill(); | |
459 workerBackingThread().backingThread().postTask(BLINK_FROM_HERE, threadSafeBi nd(&WorkerThread::prepareForShutdownOnWorkerThread, AllowCrossThreadAccess(this) )); | |
460 workerBackingThread().backingThread().postTask(BLINK_FROM_HERE, threadSafeBi nd(&WorkerThread::performShutdownOnWorkerThread, AllowCrossThreadAccess(this))); | |
461 } | |
462 | |
364 void WorkerThread::prepareForShutdownOnWorkerThread() | 463 void WorkerThread::prepareForShutdownOnWorkerThread() |
365 { | 464 { |
366 DCHECK(isCurrentThread()); | 465 DCHECK(isCurrentThread()); |
367 { | 466 { |
368 MutexLocker lock(m_threadStateMutex); | 467 MutexLocker lock(m_threadStateMutex); |
369 if (m_readyToShutdown) | 468 if (m_readyToShutdown) |
370 return; | 469 return; |
371 m_readyToShutdown = true; | 470 m_readyToShutdown = true; |
471 if (m_exitCode == ExitCode::NotTerminated) | |
472 m_exitCode = ExitCode::GracefullyTerminated; | |
372 } | 473 } |
373 | 474 |
374 workerReportingProxy().willDestroyWorkerGlobalScope(); | 475 workerReportingProxy().willDestroyWorkerGlobalScope(); |
375 InspectorInstrumentation::allAsyncTasksCanceled(workerGlobalScope()); | 476 InspectorInstrumentation::allAsyncTasksCanceled(workerGlobalScope()); |
376 workerGlobalScope()->dispose(); | 477 workerGlobalScope()->dispose(); |
377 workerBackingThread().backingThread().removeTaskObserver(m_microtaskRunner.g et()); | 478 workerBackingThread().backingThread().removeTaskObserver(m_microtaskRunner.g et()); |
378 } | 479 } |
379 | 480 |
380 void WorkerThread::performShutdownOnWorkerThread() | 481 void WorkerThread::performShutdownOnWorkerThread() |
381 { | 482 { |
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
453 } | 554 } |
454 | 555 |
455 void WorkerThread::runDebuggerTaskDontWaitOnWorkerThread() | 556 void WorkerThread::runDebuggerTaskDontWaitOnWorkerThread() |
456 { | 557 { |
457 DCHECK(isCurrentThread()); | 558 DCHECK(isCurrentThread()); |
458 std::unique_ptr<CrossThreadClosure> task = m_inspectorTaskRunner->takeNextTa sk(InspectorTaskRunner::DontWaitForTask); | 559 std::unique_ptr<CrossThreadClosure> task = m_inspectorTaskRunner->takeNextTa sk(InspectorTaskRunner::DontWaitForTask); |
459 if (task) | 560 if (task) |
460 (*task)(); | 561 (*task)(); |
461 } | 562 } |
462 | 563 |
564 WorkerThread::ExitCode WorkerThread::exitCode() | |
565 { | |
566 MutexLocker lock(m_threadStateMutex); | |
567 return m_exitCode; | |
568 } | |
569 | |
463 } // namespace blink | 570 } // namespace blink |
OLD | NEW |