tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit b5c06a3466c1396daf4b73ae4ef4528bb121a6c6
parent d76a531491d563d6721ed479dbdbafa08d89c115
Author: Ryan Hunt <rhunt@eqrion.net>
Date:   Mon, 15 Dec 2025 22:14:38 +0000

Bug 1988289 - Don't terminate worker when there are pending JS async tasks. r=iain,asuth,dom-worker-reviewers,smaug,edenchuang

A worker can be GC'ed and shutdown even if there are async tasks pending
if a StrongWorkerRef is not held by the async task. This can result in
a worker being terminated too early if it's just waiting on a WebAssembly
compile task.

Add two new JS-API callbacks for monitoring the progress of async tasks,
and use this in the workers code to create and release StrongWorkerRefs.

These callbacks are used with the Atomics.waitAsync background tasks,
which are tasks that can be canceled if there is an explicit shutdown
request. This previously was handled automatically, but now if there
is StrongWorkerRef for these tasks, it seems like we need a way to
cancel these tasks. A JS::CancelAsyncTasks embedder function is added
to do this and is invoked when the worker is explicitly asked to
shutdown.

Differential Revision: https://phabricator.services.mozilla.com/D265016

Diffstat:
Mdom/base/nsJSEnvironment.cpp | 5+++--
Mdom/workers/RuntimeService.cpp | 30++++++++++++++++++++++++------
Mdom/workers/WorkerPrivate.cpp | 27+++++++++++++++++++++++++++
Mdom/workers/WorkerPrivate.h | 9++++++++-
Mdom/worklet/WorkletThread.cpp | 6+++---
Mjs/public/Promise.h | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mjs/src/jsapi.cpp | 17++++++++++++-----
Mjs/src/vm/OffThreadPromiseRuntimeState.cpp | 129++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mjs/src/vm/OffThreadPromiseRuntimeState.h | 16+++++++++++++---
9 files changed, 286 insertions(+), 69 deletions(-)

diff --git a/dom/base/nsJSEnvironment.cpp b/dom/base/nsJSEnvironment.cpp @@ -1806,8 +1806,9 @@ void nsJSContext::EnsureStatics() { JS::SetCreateGCSliceBudgetCallback(jsapi.cx(), CreateGCSliceBudget); - JS::InitDispatchsToEventLoop(jsapi.cx(), DispatchToEventLoop, - DelayedDispatchToEventLoop, nullptr); + JS::InitAsyncTaskCallbacks(jsapi.cx(), DispatchToEventLoop, + DelayedDispatchToEventLoop, nullptr, nullptr, + nullptr); JS::InitConsumeStreamCallback(jsapi.cx(), ConsumeStream, FetchUtil::ReportJSStreamError); diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp @@ -345,9 +345,11 @@ void LoadJSGCMemoryOptions(const char* aPrefName, void* /* aClosure */) { JSGCParamKey key; }; -#define PREF(suffix_, key_) \ - {nsLiteralCString(suffix_), \ - PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX suffix_, key_} +#define PREF(suffix_, key_) \ + { \ + nsLiteralCString(suffix_), \ + PREF_JS_OPTIONS_PREFIX PREF_MEM_OPTIONS_PREFIX suffix_, key_ \ + } constexpr WorkerGCPref kWorkerPrefs[] = { PREF("max", JSGC_MAX_BYTES), PREF("gc_high_frequency_time_limit_ms", JSGC_HIGH_FREQUENCY_TIME_LIMIT), @@ -725,6 +727,22 @@ static bool DelayedDispatchToEventLoop( return true; } +static void AsyncTaskStarted(void* aClosure, JS::Dispatchable* aDispatchable) { + // See comment at JS::InitDispatchsToEventLoop() below for how we know the + // WorkerPrivate is alive. + WorkerPrivate* workerPrivate = reinterpret_cast<WorkerPrivate*>(aClosure); + workerPrivate->AssertIsOnWorkerThread(); + workerPrivate->JSAsyncTaskStarted(aDispatchable); +} + +static void AsyncTaskFinished(void* aClosure, JS::Dispatchable* aDispatchable) { + // See comment at JS::InitDispatchsToEventLoop() below for how we know the + // WorkerPrivate is alive. + WorkerPrivate* workerPrivate = reinterpret_cast<WorkerPrivate*>(aClosure); + workerPrivate->AssertIsOnWorkerThread(); + workerPrivate->JSAsyncTaskFinished(aDispatchable); +} + static bool ConsumeStream(JSContext* aCx, JS::Handle<JSObject*> aObj, JS::MimeType aMimeType, JS::StreamConsumer* aConsumer) { @@ -766,9 +784,9 @@ bool InitJSContextForWorker(WorkerPrivate* aWorkerPrivate, // A WorkerPrivate lives strictly longer than its JSRuntime so we can safely // store a raw pointer as the callback's closure argument on the JSRuntime. - JS::InitDispatchsToEventLoop(aWorkerCx, DispatchToEventLoop, - DelayedDispatchToEventLoop, - (void*)aWorkerPrivate); + JS::InitAsyncTaskCallbacks(aWorkerCx, DispatchToEventLoop, + DelayedDispatchToEventLoop, AsyncTaskStarted, + AsyncTaskFinished, (void*)aWorkerPrivate); JS::InitConsumeStreamCallback(aWorkerCx, ConsumeStream, FetchUtil::ReportJSStreamError); diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp @@ -2999,6 +2999,7 @@ WorkerPrivate::WorkerPrivate( WorkerPrivate::~WorkerPrivate() { MOZ_DIAGNOSTIC_ASSERT(mTopLevelWorkerFinishedRunnableCount == 0); MOZ_DIAGNOSTIC_ASSERT(mWorkerFinishedRunnableCount == 0); + MOZ_DIAGNOSTIC_ASSERT(mPendingJSAsyncTasks.empty()); mWorkerDebuggerEventTarget->ForgetWorkerPrivate(this); @@ -5040,6 +5041,19 @@ nsresult WorkerPrivate::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { return mShutdownTasks.RemoveTask(aTask); } +void WorkerPrivate::JSAsyncTaskStarted(JS::Dispatchable* aDispatchable) { + RefPtr<StrongWorkerRef> ref = StrongWorkerRef::Create(this, "JSAsyncTask"); + MOZ_ASSERT_DEBUG_OR_FUZZING(ref); + if (NS_WARN_IF(!ref)) { + return; + } + MOZ_ALWAYS_TRUE(mPendingJSAsyncTasks.putNew(aDispatchable, std::move(ref))); +} + +void WorkerPrivate::JSAsyncTaskFinished(JS::Dispatchable* aDispatchable) { + mPendingJSAsyncTasks.remove(aDispatchable); +} + void WorkerPrivate::RunShutdownTasks() { TargetShutdownTaskSet::TasksArray shutdownTasks; @@ -5796,6 +5810,19 @@ bool WorkerPrivate::NotifyInternal(WorkerStatus aStatus) { if (aStatus >= Closing) { CancelAllTimeouts(); + + JSContext* cx = GetJSContext(); + if (cx) { + // This will invoke the JS async task finished callback for cancellable + // JS tasks, which will invoke JSAsyncTaskFinished and remove from + // mPendingJSAsyncTasks. + // + // There may still be outstanding JS tasks for things that couldn't be + // cancelled. These must either finish normally, or be blocked on + // through a call to JS::ShutdownAsyncTasks. Cycle collector shutdown + // will call this during worker shutdown. + JS::CancelAsyncTasks(cx); + } } if (aStatus == Closing && GlobalScope()) { diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h @@ -61,7 +61,8 @@ class nsIThreadInternal; namespace JS { struct RuntimeStats; -} +class Dispatchable; +} // namespace JS namespace mozilla { class ThrottledEventQueue; @@ -1208,6 +1209,9 @@ class WorkerPrivate final void IncreaseWorkerFinishedRunnableCount() { ++mWorkerFinishedRunnableCount; } void DecreaseWorkerFinishedRunnableCount() { --mWorkerFinishedRunnableCount; } + void JSAsyncTaskStarted(JS::Dispatchable* aDispatchable); + void JSAsyncTaskFinished(JS::Dispatchable* aDispatchable); + void RunShutdownTasks(); bool CancelBeforeWorkerScopeConstructed() const { @@ -1715,6 +1719,9 @@ class WorkerPrivate final Atomic<uint32_t> mTopLevelWorkerFinishedRunnableCount; Atomic<uint32_t> mWorkerFinishedRunnableCount; + // A set of active JS async tasks that should prevent idle shutdown. + HashMap<JS::Dispatchable*, RefPtr<StrongWorkerRef>> mPendingJSAsyncTasks; + TargetShutdownTaskSet mShutdownTasks MOZ_GUARDED_BY(mMutex); bool mShutdownTasksRun MOZ_GUARDED_BY(mMutex) = false; diff --git a/dom/worklet/WorkletThread.cpp b/dom/worklet/WorkletThread.cpp @@ -387,9 +387,9 @@ void WorkletThread::EnsureCycleCollectedJSContext( // A thread lives strictly longer than its JSRuntime so we can safely // store a raw pointer as the callback's closure argument on the JSRuntime. - JS::InitDispatchsToEventLoop(context->Context(), DispatchToEventLoop, - DelayedDispatchToEventLoop, - NS_GetCurrentThread()); + JS::InitAsyncTaskCallbacks(context->Context(), DispatchToEventLoop, + DelayedDispatchToEventLoop, nullptr, nullptr, + NS_GetCurrentThread()); JS_SetNativeStackQuota(context->Context(), WORKLET_CONTEXT_NATIVE_STACK_LIMIT); diff --git a/js/public/Promise.h b/js/public/Promise.h @@ -664,7 +664,7 @@ class JS_PUBLIC_API Dispatchable { }; /** - * Callbacks to dispatch a JS::Dispatchable to a JSContext's thread's event + * Callback to dispatch a JS::Dispatchable to a JSContext's thread's event * loop. * * The DispatchToEventLoopCallback set on a particular JSContext must accept @@ -680,6 +680,9 @@ class JS_PUBLIC_API Dispatchable { * If a timeout manager is not available for given context, it should return * false, optionally with a warning message printed. * + * The callback accepts a closure that is the same as the one given by the call + * to JS::InitAsyncTaskCallbacks. + * * If the callback returns `true`, it must eventually run the given * Dispatchable; otherwise, SpiderMonkey may leak memory or hang. * @@ -687,30 +690,113 @@ class JS_PUBLIC_API Dispatchable { * shutting down and is no longer accepting runnables. Shutting down is a * one-way transition: once the callback has rejected a runnable, it must reject * all subsequently submitted runnables as well. - * - * To establish a DispatchToEventLoopCallback, the embedding may either call - * InitDispatchsToEventLoop to provide its own, or call js::UseInternalJobQueues - * to select a default implementation built into SpiderMonkey. This latter - * depends on the embedding to call js::RunJobs on the JavaScript thread to - * process queued Dispatchables at appropriate times. */ typedef bool (*DispatchToEventLoopCallback)( void* closure, js::UniquePtr<Dispatchable>&& dispatchable); +/** + * Callback to dispatch a JS::Dispatchable to a JSContext's thread's event + * loop with a delay. + * + * The DelayedDispatchToEventLoopCallback set on a particular JSContext must + * accept JS::Dispatchable instances and arrange for their `run` methods to be + * called after the given delay on the JSContext's thread. This is used for + * cross-thread dispatch, so the callback itself must be safe to call from any + * thread. It cannot trigger a GC. + * + * The embeddings must have its own timeout manager to handle the delay. + * If a timeout manager is not available for given context, it should return + * false, optionally with a warning message printed. + * + * The callback accepts a closure that is the same as the one given by the call + * to JS::InitAsyncTaskCallbacks. + * + * If the callback returns `true`, it must eventually run the given + * Dispatchable; otherwise, SpiderMonkey may leak memory or hang. + * + * The callback may return `false` to indicate that the JSContext's thread is + * shutting down and is no longer accepting runnables. Shutting down is a + * one-way transition: once the callback has rejected a runnable, it must reject + * all subsequently submitted runnables as well. + */ + typedef bool (*DelayedDispatchToEventLoopCallback)( void* closure, js::UniquePtr<Dispatchable>&& dispatchable, uint32_t delay); -extern JS_PUBLIC_API void InitDispatchsToEventLoop( - JSContext* cx, DispatchToEventLoopCallback callback, - DelayedDispatchToEventLoopCallback delayedCallback, void* closure); +/** + * Callback to notify the embedder that a background task has begun. This can + * be used to keep the JSContext's thread alive if it's subject to garbage + * collection (e.g. as web workers are). + * + * This callback will be called on the same thread as the JSContext, but it + * must not perform any garbage collection or re-enter the engine. + * + * The passed dispatchable can be used to track the async task. Most async + * tasks will finish in a bounded amount of time, but some + * (i.e. Atomics.waitAsync) are not guaranteed to finish. These tasks may be + * canceled using JS::CancelAsyncTasks. + * + * The callback accepts a closure that is the same as the one given by the call + * to JS::InitAsyncTaskCallbacks. + */ +typedef void (*AsyncTaskStartedCallback)(void* closure, + Dispatchable* dispatchable); + +/** + * Callback to notify the embedder that a background task has finished. This can + * be used to keep the JSContext's thread alive if it's subject to garbage + * collection (e.g. as web workers are). + * + * This callback will be called on the same thread as the JSContext, but it + * must not perform any garbage collection or re-enter the engine. + * + * The passed dispatchable will be the same as the one given in + * AsyncTaskStartedCallback. See that callback for more information. + * + * The callback accepts a closure that is the same as the one given by the call + * to JS::InitAsyncTaskCallbacks. + */ +typedef void (*AsyncTaskFinishedCallback)(void* closure, + Dispatchable* dispatchable); + +/** + * Initialize the async task callbacks. Embedders must use this or else call + * js::UseInternalJobQueues to select a default implementation built into + * SpiderMonkey. This latter depends on the embedding to call js::RunJobs on + * the JavaScript thread to process queued Dispatchables at appropriate times. + * + * The dispatchCallback and delayedDispatchCallback are mandatory. + * asyncTaskStartedCallback and asyncTaskFinishedCallback are optional and may + * be null. + * + */ +extern JS_PUBLIC_API void InitAsyncTaskCallbacks( + JSContext* cx, DispatchToEventLoopCallback dispatchCallback, + DelayedDispatchToEventLoopCallback delayedDispatchCallback, + AsyncTaskStartedCallback asyncTaskStartedCallback, + AsyncTaskFinishedCallback asyncTaskFinishedCallback, void* closure); + +/** + * Cancel all cancellable async tasks. Some tasks may not be cancellable, and + * must be waited on through a call to JS::ShutdownAsyncTasks. + * + * This will fire JSAsyncTaskFinished callbacks for cancelled tasks. + */ +extern JS_PUBLIC_API void CancelAsyncTasks(JSContext* cx); /** - * When a JSRuntime is destroyed it implicitly cancels all async tasks in - * progress, releasing any roots held by the task. However, this is not soon - * enough for cycle collection, which needs to have roots dropped earlier so - * that the cycle collector can transitively remove roots for a future GC. For - * these and other cases, the set of pending async tasks can be canceled + * Cancel all cancellable async tasks and block until non-cancellable tasks are + * finished. + * + * This will fire JSAsyncTaskFinished callbacks for all tasks, and there should + * be no outstanding tasks after this returns. All roots held by tasks will be + * released. + * + * Destroying a JSRuntime will implicitly perform this. However, this is not + * soon enough for cycle collection, which needs to have roots dropped earlier + * so that the cycle collector can transitively remove roots for a future GC. + * For these and other cases, the set of pending async tasks can be canceled * with this call earlier than JSRuntime destruction. */ diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp @@ -3179,11 +3179,18 @@ JS_PUBLIC_API JSObject* JS::GetWaitForAllPromise( return js::GetWaitForAllPromise(cx, promises); } -JS_PUBLIC_API void JS::InitDispatchsToEventLoop( - JSContext* cx, JS::DispatchToEventLoopCallback callback, - JS::DelayedDispatchToEventLoopCallback delayCallback, void* closure) { - cx->runtime()->offThreadPromiseState.ref().init(callback, delayCallback, - closure); +JS_PUBLIC_API void JS::InitAsyncTaskCallbacks( + JSContext* cx, JS::DispatchToEventLoopCallback dispatchCallback, + JS::DelayedDispatchToEventLoopCallback delayedDispatchCallback, + JS::AsyncTaskStartedCallback asyncTaskStartedCallback, + JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback, void* closure) { + cx->runtime()->offThreadPromiseState.ref().init( + dispatchCallback, delayedDispatchCallback, asyncTaskStartedCallback, + asyncTaskFinishedCallback, closure); +} + +JS_PUBLIC_API void JS::CancelAsyncTasks(JSContext* cx) { + cx->runtime()->offThreadPromiseState.ref().cancelTasks(cx); } JS_PUBLIC_API void JS::ShutdownAsyncTasks(JSContext* cx) { diff --git a/js/src/vm/OffThreadPromiseRuntimeState.cpp b/js/src/vm/OffThreadPromiseRuntimeState.cpp @@ -63,8 +63,7 @@ bool OffThreadPromiseTask::init(JSContext* cx, OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref(); MOZ_ASSERT(state.initialized()); - state.numRegistered_++; - registered_ = true; + state.registerTask(cx, this); return true; } @@ -88,14 +87,22 @@ bool OffThreadPromiseTask::InitCancellable( return false; } - OffThreadPromiseTask* rawTask = task.release(); - if (!state.cancellable().putNew(rawTask)) { - state.numRegistered_--; - rawTask->registered_ = false; + if (!state.cancellable().putNew(task.get())) { + // The task will be freed because it's only owned by this function. Eagerly + // unregister it now using the provided helper thread lock so that it + // doesn't need to be reacquired. + task->unregister(state, lock); ReportOutOfMemory(cx); return false; } + + // We're infallible from this point on. + OffThreadPromiseTask* rawTask = task.release(); + + // Only mark the task as cancellable once we've added it to the cancellable + // set. The destructor will remove from the set if this flag is set. rawTask->cancellable_ = true; + return true; } @@ -106,12 +113,7 @@ void OffThreadPromiseTask::unregister(OffThreadPromiseRuntimeState& state) { void OffThreadPromiseTask::unregister(OffThreadPromiseRuntimeState& state, const AutoLockHelperThreadState& lock) { MOZ_ASSERT(registered_); - if (cancellable_) { - cancellable_ = false; - state.cancellable().remove(this); - } - state.numRegistered_--; - registered_ = false; + state.unregisterTask(this); } void OffThreadPromiseTask::run(JSContext* cx, @@ -229,8 +231,7 @@ void OffThreadPromiseTask::DispatchResolveAndDestroy( { // Hazard analysis can't tell that the callback does not GC. JS::AutoSuppressGCAnalysis nogc; - if (state.dispatchToEventLoopCallback_(state.dispatchToEventLoopClosure_, - std::move(task))) { + if (state.dispatchToEventLoop(std::move(task))) { return; } } @@ -248,6 +249,8 @@ void OffThreadPromiseTask::DispatchResolveAndDestroy( OffThreadPromiseRuntimeState::OffThreadPromiseRuntimeState() : dispatchToEventLoopCallback_(nullptr), delayedDispatchToEventLoopCallback_(nullptr), + asyncTaskStartedCallback_(nullptr), + asyncTaskFinishedCallback_(nullptr), dispatchToEventLoopClosure_(nullptr), #ifdef DEBUG forceQuitting_(false), @@ -264,12 +267,16 @@ OffThreadPromiseRuntimeState::~OffThreadPromiseRuntimeState() { } void OffThreadPromiseRuntimeState::init( - JS::DispatchToEventLoopCallback callback, - JS::DelayedDispatchToEventLoopCallback delayedCallback, void* closure) { + JS::DispatchToEventLoopCallback dispatchCallback, + JS::DelayedDispatchToEventLoopCallback delayedDispatchCallback, + JS::AsyncTaskStartedCallback asyncTaskStartedCallback, + JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback, void* closure) { MOZ_ASSERT(!initialized()); - dispatchToEventLoopCallback_ = callback; - delayedDispatchToEventLoopCallback_ = delayedCallback; + dispatchToEventLoopCallback_ = dispatchCallback; + delayedDispatchToEventLoopCallback_ = delayedDispatchCallback; + asyncTaskStartedCallback_ = asyncTaskStartedCallback; + asyncTaskFinishedCallback_ = asyncTaskFinishedCallback; dispatchToEventLoopClosure_ = closure; MOZ_ASSERT(initialized()); @@ -287,6 +294,46 @@ bool OffThreadPromiseRuntimeState::delayedDispatchToEventLoop( std::move(dispatchable), delay); } +void OffThreadPromiseRuntimeState::registerTask(JSContext* cx, + OffThreadPromiseTask* task) { + // Track the total number of pending async tasks + numRegistered_++; + + // Mark the task as registered + task->registered_ = true; + + if (!asyncTaskStartedCallback_) { + return; + } + + // The embedder must not perform a GC, suppress GC analysis here. + JS::AutoSuppressGCAnalysis nogc(cx); + asyncTaskStartedCallback_(dispatchToEventLoopClosure_, task); +} + +void OffThreadPromiseRuntimeState::unregisterTask(OffThreadPromiseTask* task) { + // Track the total number of pending async tasks + MOZ_ASSERT(numRegistered_ != 0); + numRegistered_--; + + // Mark the task as unregistered + task->registered_ = false; + + // If the task was cancellable, remove from our cancellable set. + if (task->cancellable_) { + task->cancellable_ = false; + cancellable().remove(task); + } + + if (!asyncTaskFinishedCallback_) { + return; + } + + // The embedder must not perform a GC, suppress GC analysis here. + JS::AutoSuppressGCAnalysis nogc; + asyncTaskFinishedCallback_(dispatchToEventLoopClosure_, task); +} + /* static */ bool OffThreadPromiseRuntimeState::internalDispatchToEventLoop( void* closure, js::UniquePtr<JS::Dispatchable>&& d) { @@ -380,7 +427,8 @@ bool OffThreadPromiseRuntimeState::usingInternalDispatchQueue() const { } void OffThreadPromiseRuntimeState::initInternalDispatchQueue() { - init(internalDispatchToEventLoop, internalDelayedDispatchToEventLoop, this); + init(internalDispatchToEventLoop, internalDelayedDispatchToEventLoop, nullptr, + nullptr, this); MOZ_ASSERT(usingInternalDispatchQueue()); } @@ -446,14 +494,14 @@ void OffThreadPromiseRuntimeState::stealFailedTask(JS::Dispatchable* task) { } } -void OffThreadPromiseRuntimeState::shutdown(JSContext* cx) { +void OffThreadPromiseRuntimeState::cancelTasks( + js::AutoLockHelperThreadState& lock, JSContext* cx) { + MOZ_ASSERT(initialized()); if (!initialized()) { return; } - AutoLockHelperThreadState lock; - - // Cancel all undispatched tasks. + // Cancel all undispatched tasks that are cancellable. for (auto iter = cancellable().modIter(); !iter.done(); iter.next()) { OffThreadPromiseTask* task = iter.get(); MOZ_ASSERT(task->cancellable_); @@ -461,6 +509,26 @@ void OffThreadPromiseRuntimeState::shutdown(JSContext* cx) { OffThreadPromiseTask::DestroyUndispatchedTask(task, *this, lock); } +} + +void OffThreadPromiseRuntimeState::cancelTasks(JSContext* cx) { + if (!initialized()) { + return; + } + + AutoLockHelperThreadState lock; + cancelTasks(lock, cx); +} + +void OffThreadPromiseRuntimeState::shutdown(JSContext* cx) { + if (!initialized()) { + return; + } + + AutoLockHelperThreadState lock; + + // Cancel all undispatched tasks. + cancelTasks(lock, cx); MOZ_ASSERT(cancellable().empty()); // When the shell is using the internal event loop, we must simulate our @@ -536,18 +604,11 @@ js::PromiseObject* OffThreadPromiseTask::ExtractAndForget( task->runtime()->offThreadPromiseState.ref(); MOZ_ASSERT(state.initialized()); MOZ_ASSERT(task->registered_); - - // TODO: This has overlap with removeFromCancellableListAndDispatch. - // The two methods should be refactored so that they are consistant and - // we don't have unnecessary repetition or distribution of responsibilities. - state.numRegistered_--; - if (task->cancellable_) { - state.cancellable().remove(task); - } - task->registered_ = false; - js::PromiseObject* promise = task->promise_; + // Call the unregister method manually with our provided lock, otherwise the + // destructor will try to acquire it and fail. + task->unregister(state, lock); + // Now we can call the destructor. js_delete(task); - return promise; } diff --git a/js/src/vm/OffThreadPromiseRuntimeState.h b/js/src/vm/OffThreadPromiseRuntimeState.h @@ -257,6 +257,8 @@ class OffThreadPromiseRuntimeState { // not require a lock. JS::DispatchToEventLoopCallback dispatchToEventLoopCallback_; JS::DelayedDispatchToEventLoopCallback delayedDispatchToEventLoopCallback_; + JS::AsyncTaskStartedCallback asyncTaskStartedCallback_; + JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback_; void* dispatchToEventLoopClosure_; #ifdef DEBUG @@ -305,7 +307,6 @@ class OffThreadPromiseRuntimeState { // mean "the DispatchToEventLoopCallback failed after this task was dispatched // for execution". HelperThreadLockData<ConditionVariable> allFailed_; - HelperThreadLockData<size_t> numFailed_; // The queue of JS::Dispatchables used by the DispatchToEventLoopCallback that // calling js::UseInternalJobQueues installs. @@ -341,11 +342,17 @@ class OffThreadPromiseRuntimeState { void operator=(const OffThreadPromiseRuntimeState&) = delete; OffThreadPromiseRuntimeState(const OffThreadPromiseRuntimeState&) = delete; + // Used by OffThreadPromiseTask + void registerTask(JSContext* cx, OffThreadPromiseTask* task); + void unregisterTask(OffThreadPromiseTask* task); + public: OffThreadPromiseRuntimeState(); ~OffThreadPromiseRuntimeState(); - void init(JS::DispatchToEventLoopCallback callback, - JS::DelayedDispatchToEventLoopCallback delayCallback, + void init(JS::DispatchToEventLoopCallback dispatchCallback, + JS::DelayedDispatchToEventLoopCallback delayedDispatchCallback, + JS::AsyncTaskStartedCallback asyncTaskStartedCallback, + JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback, void* closure); void initInternalDispatchQueue(); bool initialized() const; @@ -362,6 +369,9 @@ class OffThreadPromiseRuntimeState { bool delayedDispatchToEventLoop( js::UniquePtr<JS::Dispatchable>&& dispatchable, uint32_t delay); + void cancelTasks(JSContext* cx); + void cancelTasks(AutoLockHelperThreadState& lock, JSContext* cx); + // shutdown() must be called by the JSRuntime while the JSRuntime is valid. void shutdown(JSContext* cx);