tor-browser

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

commit 269a06ecd851689a6b6e73f46bcd8639868fc692
parent 811324ab997c958182ef1b339cd0088dca7d545b
Author: Matthew Gaudet <mgaudet@mozilla.com>
Date:   Wed,  1 Oct 2025 21:38:51 +0000

Bug 1983154 - Gecko Support for JS Micro Task Queue r=arai,smaug,dom-worker-reviewers,asuth

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

Diffstat:
Mdom/workers/RuntimeService.cpp | 65++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mdom/workers/WorkerPrivate.cpp | 26++++++++++++++++++++------
Mdom/worklet/WorkletThread.cpp | 24+++++++++++++++++-------
Mmodules/libpref/init/StaticPrefList.yaml | 3+++
Mxpcom/base/CycleCollectedJSContext.cpp | 586++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mxpcom/base/CycleCollectedJSContext.h | 32+++++++++++++++++++++++++++++++-
6 files changed, 609 insertions(+), 127 deletions(-)

diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp @@ -30,6 +30,7 @@ #include "mozilla/Monitor.h" #include "mozilla/Preferences.h" #include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_javascript.h" #include "mozilla/TimeStamp.h" #include "mozilla/dom/AtomList.h" #include "mozilla/dom/BindingUtils.h" @@ -989,35 +990,61 @@ class WorkerJSContext final : public mozilla::CycleCollectedJSContext { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(runnable); - std::deque<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr; - JSContext* cx = Context(); NS_ASSERTION(cx, "This should never be null!"); JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx)); NS_ASSERTION(global, "This should never be null!"); - // On worker threads, if the current global is the worker global or - // ShadowRealm global, we use the main micro task queue. Otherwise, the - // current global must be either the debugger global or a debugger sandbox, - // and we use the debugger micro task queue instead. - if (IsWorkerGlobal(global) || IsShadowRealmGlobal(global)) { - microTaskQueue = &GetMicroTaskQueue(); + JS::JobQueueMayNotBeEmpty(cx); + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + PROFILER_MARKER_FLOW_ONLY("WorkerJSContext::DispatchToMicroTask", OTHER, + {}, FlowMarker, + Flow::FromPointer(runnable.get())); + + // On worker threads, if the current global is the worker global or + // ShadowRealm global, we use the main micro task queue. Otherwise, the + // current global must be either the debugger global or a debugger + // sandbox, and we use the debugger micro task queue instead. + if (IsWorkerGlobal(global) || IsShadowRealmGlobal(global)) { + if (!EnqueueMicroTask(cx, runnable.forget())) { + // This should never fail, but if it does, we have no choice but to + // crash. + MOZ_CRASH("Failed to enqueue micro task from worker."); + } + } else { + MOZ_ASSERT(IsWorkerDebuggerGlobal(global) || + IsWorkerDebuggerSandbox(global)); + if (!EnqueueDebugMicroTask(cx, runnable.forget())) { + // This should never fail, but if it does, we have no choice but to + // crash. + MOZ_CRASH("Failed to enqueue debugger micro task from worker."); + } + } } else { - MOZ_ASSERT(IsWorkerDebuggerGlobal(global) || - IsWorkerDebuggerSandbox(global)); + std::deque<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr; + // On worker threads, if the current global is the worker global or + // ShadowRealm global, we use the main micro task queue. Otherwise, the + // current global must be either the debugger global or a debugger + // sandbox, and we use the debugger micro task queue instead. + if (IsWorkerGlobal(global) || IsShadowRealmGlobal(global)) { + microTaskQueue = &GetMicroTaskQueue(); + } else { + MOZ_ASSERT(IsWorkerDebuggerGlobal(global) || + IsWorkerDebuggerSandbox(global)); - microTaskQueue = &GetDebuggerMicroTaskQueue(); - } + microTaskQueue = &GetDebuggerMicroTaskQueue(); + } - JS::JobQueueMayNotBeEmpty(cx); - if (!runnable->isInList()) { - // A recycled object may be in the list already. - mMicrotasksToTrace.insertBack(runnable); + if (!runnable->isInList()) { + // A recycled object may be in the list already. + mMicrotasksToTrace.insertBack(runnable); + } + PROFILER_MARKER_FLOW_ONLY("WorkerJSContext::DispatchToMicroTask", OTHER, + {}, FlowMarker, + Flow::FromPointer(runnable.get())); + microTaskQueue->push_back(std::move(runnable)); } - PROFILER_MARKER_FLOW_ONLY("WorkerJSContext::DispatchToMicroTask", OTHER, {}, - FlowMarker, Flow::FromPointer(runnable.get())); - microTaskQueue->push_back(std::move(runnable)); } bool IsSystemCaller() const override { diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp @@ -28,6 +28,7 @@ #include "js/MemoryMetrics.h" #include "js/SourceText.h" #include "js/friend/ErrorMessages.h" // JSMSG_OUT_OF_MEMORY +#include "js/friend/MicroTask.h" #include "mozilla/AntiTrackingUtils.h" #include "mozilla/BasePrincipal.h" #include "mozilla/CycleCollectedJSContext.h" @@ -39,6 +40,7 @@ #include "mozilla/ScopeExit.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_javascript.h" #include "mozilla/StorageAccess.h" #include "mozilla/StoragePrincipalHelper.h" #include "mozilla/ThreadEventQueue.h" @@ -5605,12 +5607,24 @@ void WorkerPrivate::EnterDebuggerEventLoop() { { MutexAutoLock lock(mMutex); - std::deque<RefPtr<MicroTaskRunnable>>& debuggerMtQueue = - ccjscx->GetDebuggerMicroTaskQueue(); - while (mControlQueue.IsEmpty() && - !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) && - debuggerMtQueue.empty()) { - WaitForWorkerEvents(); + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + // When JS microtask queue is enabled, check for debugger microtasks + // directly from the JS engine + while (mControlQueue.IsEmpty() && + !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) && + !JS::HasDebuggerMicroTasks(cx)) { + WaitForWorkerEvents(); + } + } else { + // Legacy path: check the debugger microtask queue in + // CycleCollectedJSContext + std::deque<RefPtr<MicroTaskRunnable>>& debuggerMtQueue = + ccjscx->GetDebuggerMicroTaskQueue(); + while (mControlQueue.IsEmpty() && + !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) && + debuggerMtQueue.empty()) { + WaitForWorkerEvents(); + } } ProcessAllControlRunnablesLocked(); diff --git a/dom/worklet/WorkletThread.cpp b/dom/worklet/WorkletThread.cpp @@ -10,10 +10,12 @@ #include "js/ContextOptions.h" #include "js/Exception.h" #include "js/Initialization.h" +#include "js/friend/MicroTask.h" #include "mozilla/Attributes.h" #include "mozilla/CycleCollectedJSRuntime.h" #include "mozilla/EventQueue.h" #include "mozilla/FlowMarkers.h" +#include "mozilla/StaticPrefs_javascript.h" #include "mozilla/ThreadEventQueue.h" #include "mozilla/dom/AtomList.h" #include "mozilla/dom/WorkletGlobalScope.h" @@ -163,14 +165,22 @@ class WorkletJSContext final : public CycleCollectedJSContext { #endif JS::JobQueueMayNotBeEmpty(cx); - if (!runnable->isInList()) { - // A recycled object may be in the list already. - mMicrotasksToTrace.insertBack(runnable); + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + PROFILER_MARKER_FLOW_ONLY("WorkletJSContext::DispatchToMicroTask", OTHER, + {}, FlowMarker, + Flow::FromPointer(runnable.get())); + bool ret = mozilla::EnqueueMicroTask(cx, std::move(aRunnable)); + MOZ_RELEASE_ASSERT(ret); + } else { + if (!runnable->isInList()) { + // A recycled object may be in the list already. + mMicrotasksToTrace.insertBack(runnable); + } + PROFILER_MARKER_FLOW_ONLY("WorkletJSContext::DispatchToMicroTask", OTHER, + {}, FlowMarker, + Flow::FromPointer(runnable.get())); + GetMicroTaskQueue().push_back(std::move(runnable)); } - PROFILER_MARKER_FLOW_ONLY("WorkletJSContext::DispatchToMicroTask", OTHER, - {}, FlowMarker, - Flow::FromPointer(runnable.get())); - GetMicroTaskQueue().push_back(std::move(runnable)); } bool IsSystemCaller() const override { diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -9375,6 +9375,9 @@ mirror: always do_not_use_directly: true +# Use the JS microtask queue for Promise jobs and other microtasks. +# DO NOT TURN THIS ON BY DEFAULT WITHOUT THE BUGS BLOCKING +# BUG 1990841 being fixed (especially Bug 1990842). - name: javascript.options.use_js_microtask_queue type: RelaxedAtomicBool value: false diff --git a/xpcom/base/CycleCollectedJSContext.cpp b/xpcom/base/CycleCollectedJSContext.cpp @@ -10,6 +10,8 @@ #include <utility> #include "js/Debug.h" +#include "js/friend/DumpFunctions.h" +#include "js/friend/MicroTask.h" #include "js/GCAPI.h" #include "js/Utility.h" #include "jsapi.h" @@ -23,6 +25,7 @@ #include "mozilla/ProfilerMarkers.h" #include "mozilla/ProfilerRunnable.h" #include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_javascript.h" #include "mozilla/Unused.h" #include "mozilla/dom/DOMException.h" #include "mozilla/dom/DOMJSClass.h" @@ -82,6 +85,8 @@ CycleCollectedJSContext::~CycleCollectedJSContext() { JS_SetContextPrivate(mJSContext, nullptr); + MOZ_ASSERT(!JS::HasAnyMicroTasks(mJSContext)); + mRuntime->SetContext(nullptr); mRuntime->Shutdown(mJSContext); @@ -364,10 +369,12 @@ bool CycleCollectedJSContext::getHostDefinedData( } bool CycleCollectedJSContext::enqueuePromiseJob( - JSContext* aCx, JS::HandleObject aPromise, JS::HandleObject aJob, - JS::HandleObject aAllocationSite, JS::HandleObject hostDefinedData) { + JSContext* aCx, JS::Handle<JSObject*> aPromise, JS::Handle<JSObject*> aJob, + JS::Handle<JSObject*> aAllocationSite, + JS::Handle<JSObject*> hostDefinedData) { MOZ_ASSERT(aCx == Context()); MOZ_ASSERT(Get() == this); + MOZ_ASSERT(!StaticPrefs::javascript_options_use_js_microtask_queue()); nsIGlobalObject* global = nullptr; WebTaskSchedulingState* schedulingState = nullptr; @@ -419,12 +426,60 @@ void CycleCollectedJSContext::runJobs(JSContext* aCx) { } bool CycleCollectedJSContext::empty() const { + // MG:XXX: This is debug only and only used by + // ~AutoDebuggerJobQueueInterruption; probably can be removed eventually. // This is our override of JS::JobQueue::empty. Since that interface is only // concerned with the ordinary microtask queue, not the debugger microtask // queue, we only report on the former. return mPendingMicroTaskRunnables.empty(); } +// Unwrap (without interacting with refcounting) a Gecko MicroTaskRunnable if +// the task is not a JS MicroTask; otherwise, return nullptr. +// +// This is a non-owning conversion: the JS::MicroTask still owns the refcount. +static MicroTaskRunnable* MaybeUnwrapTaskToRunnable( + JS::Handle<JS::MicroTask> task) { + if (!JS::IsJSMicroTask(task)) { + void* nonJSTask = task.toPrivate(); + MicroTaskRunnable* task = reinterpret_cast<MicroTaskRunnable*>(nonJSTask); + return task; + } + + return nullptr; +} + +// Take ownership of a task inside a JS::MicroTask - This clears the +// contents of the value to make it clear that we've transfered ownership. +// Task is only edited if unwrapping succeeds. +// +// Note: this is not foolproof because JS::MicroTask is a copyable type, and +// so nothing currently prevents: +// +// Rooted<JS::MicroTask> mt(cx, JS::DequeueNextMicroTask(cx)); +// JS::MicroTask c = mt; // This is a JS::Value copy -- we don't really have +// mechanism to prevent this RefPtr<Runnable> r = +// MaybeUnwrapTaskToOwnedRunnable(&mt); +// +// At this point c still has a private value pointer to the microtask; +// conceivably one could do: +// +// Rooted<JS::MicroTask> cr(cx, c); +// RefPtr<Runnable> rFromC = MaybeUnwrapTaskToOwnedRunnable(&cr); +// +// Which would result in a double free. +// +// This will be fixed in Bug 1990842, which will make this safer. +static already_AddRefed<MicroTaskRunnable> MaybeUnwrapTaskToOwnedRunnable( + JS::MutableHandle<JS::MicroTask> task) { + auto* mtr = MaybeUnwrapTaskToRunnable(task); + if (!mtr) { + return nullptr; + } + task.setUndefined(); + return already_AddRefed(mtr); +} + // Preserve a debuggee's microtask queue while it is interrupted by the // debugger. See the comments for JS::AutoDebuggerJobQueueInterruption. class CycleCollectedJSContext::SavedMicroTaskQueue @@ -432,7 +487,11 @@ class CycleCollectedJSContext::SavedMicroTaskQueue public: explicit SavedMicroTaskQueue(CycleCollectedJSContext* ccjs) : ccjs(ccjs) { ccjs->mDebuggerRecursionDepth++; - ccjs->mPendingMicroTaskRunnables.swap(mQueue); + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + mSavedQueue = JS::SaveMicroTaskQueue(ccjs->Context()); + } else { + ccjs->mPendingMicroTaskRunnables.swap(mQueue); + } } ~SavedMicroTaskQueue() { @@ -462,28 +521,50 @@ class CycleCollectedJSContext::SavedMicroTaskQueue // preferrable to crashing. MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.size() <= 1); MOZ_RELEASE_ASSERT(ccjs->mDebuggerRecursionDepth); - RefPtr<MicroTaskRunnable> maybeSuppressedTasks; + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + JSContext* cx = ccjs->Context(); + + JS::Rooted<JS::MicroTask> suppressedTasks(cx); + MOZ_ASSERT(JS::GetRegularMicroTaskCount(cx) <= 1); + if (JS::HasRegularMicroTasks(cx)) { + suppressedTasks = JS::DequeueNextRegularMicroTask(cx); + MOZ_ASSERT(MaybeUnwrapTaskToRunnable(suppressedTasks) == + ccjs->mSuppressedMicroTaskList); + } + MOZ_RELEASE_ASSERT(!JS::HasRegularMicroTasks(cx)); + JS::RestoreMicroTaskQueue(cx, std::move(mSavedQueue)); - // Handle the case where there is a SuppressedMicroTask still in the queue. - if (!ccjs->mPendingMicroTaskRunnables.empty()) { - maybeSuppressedTasks = ccjs->mPendingMicroTaskRunnables.front(); - ccjs->mPendingMicroTaskRunnables.pop_front(); - } + if (!suppressedTasks.isNullOrUndefined()) { + JS::EnqueueMicroTask(cx, suppressedTasks.get()); + } + } else { + MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.size() <= 1); + + RefPtr<MicroTaskRunnable> maybeSuppressedTasks; + + // Handle the case where there is a SuppressedMicroTask still in the + // queue. + if (!ccjs->mPendingMicroTaskRunnables.empty()) { + maybeSuppressedTasks = ccjs->mPendingMicroTaskRunnables.front(); + ccjs->mPendingMicroTaskRunnables.pop_front(); + } - MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.empty()); - ccjs->mDebuggerRecursionDepth--; - ccjs->mPendingMicroTaskRunnables.swap(mQueue); + MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.empty()); + ccjs->mDebuggerRecursionDepth--; + ccjs->mPendingMicroTaskRunnables.swap(mQueue); - // Re-enqueue the suppressed task now that we've put the original microtask - // queue back. - if (maybeSuppressedTasks) { - ccjs->mPendingMicroTaskRunnables.push_back(maybeSuppressedTasks); + // Re-enqueue the suppressed task now that we've put the original + // microtask queue back. + if (maybeSuppressedTasks) { + ccjs->mPendingMicroTaskRunnables.push_back(maybeSuppressedTasks); + } } } private: CycleCollectedJSContext* ccjs; std::deque<RefPtr<MicroTaskRunnable>> mQueue; + js::UniquePtr<JS::SavedMicroTaskQueue> mSavedQueue; }; js::UniquePtr<JS::JobQueue::SavedJobQueue> @@ -577,12 +658,14 @@ void CycleCollectedJSContext::SetPendingException(Exception* aException) { std::deque<RefPtr<MicroTaskRunnable>>& CycleCollectedJSContext::GetMicroTaskQueue() { MOZ_ASSERT(mJSContext); + MOZ_ASSERT(!StaticPrefs::javascript_options_use_js_microtask_queue()); return mPendingMicroTaskRunnables; } std::deque<RefPtr<MicroTaskRunnable>>& CycleCollectedJSContext::GetDebuggerMicroTaskQueue() { MOZ_ASSERT(mJSContext); + MOZ_ASSERT(!StaticPrefs::javascript_options_use_js_microtask_queue()); return mDebuggerMicroTaskQueue; } @@ -690,8 +773,8 @@ void CycleCollectedJSContext::AfterProcessMicrotasks() { // Clear kept alive objects in JS WeakRef. // https://whatpr.org/html/4571/webappapis.html#perform-a-microtask-checkpoint // - // ECMAScript implementations are expected to call ClearKeptObjects when a - // synchronous sequence of ECMAScript execution completes. + // ECMAScript implementations are expected to call ClearKeptObjects when + // a synchronous sequence of ECMAScript execution completes. // // https://tc39.es/proposal-weakrefs/#sec-clear-kept-objects JS::ClearKeptObjects(mJSContext); @@ -764,12 +847,43 @@ void CycleCollectedJSContext::AddPendingIDBTransaction( mPendingIDBTransactions.AppendElement(std::move(data)); } +// MicroTaskRunnables and the JS MicroTask Queue: +// +// The following describes our refcounting scheme: +// +// - A runnable wrapped in a JS::Value (RunnableToValue) is always created from +// an already_AddRefed (so has a positive refcount) and it holds onto that ref +// count until it is finally eventually unwrapped to an owning reference +// (MaybeUnwrapTaskToOwnedRunnable) +// +// - This means runnables in the queue have their refcounts stay above zero for +// the duration of the time they are in the queue. +JS::MicroTask RunnableToMicroTask( + already_AddRefed<MicroTaskRunnable>& aRunnable) { + JS::MicroTask v; + auto* r = aRunnable.take(); + MOZ_ASSERT(r); + v.setPrivate(r); + return v; +} + +bool EnqueueMicroTask(JSContext* aCx, + already_AddRefed<MicroTaskRunnable> aRunnable) { + MOZ_ASSERT(StaticPrefs::javascript_options_use_js_microtask_queue()); + JS::MicroTask v = RunnableToMicroTask(aRunnable); + return JS::EnqueueMicroTask(aCx, v); +} +bool EnqueueDebugMicroTask(JSContext* aCx, + already_AddRefed<MicroTaskRunnable> aRunnable) { + MOZ_ASSERT(StaticPrefs::javascript_options_use_js_microtask_queue()); + JS::MicroTask v = RunnableToMicroTask(aRunnable); + return JS::EnqueueDebugMicroTask(aCx, v); +} + void CycleCollectedJSContext::DispatchToMicroTask( already_AddRefed<MicroTaskRunnable> aRunnable) { RefPtr<MicroTaskRunnable> runnable(aRunnable); - MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(runnable); JS::JobQueueMayNotBeEmpty(Context()); PROFILER_MARKER_FLOW_ONLY("CycleCollectedJSContext::DispatchToMicroTask", @@ -777,11 +891,15 @@ void CycleCollectedJSContext::DispatchToMicroTask( Flow::FromPointer(runnable.get())); LogMicroTaskRunnable::LogDispatch(runnable.get()); - if (!runnable->isInList()) { - // A recycled object may be in the list already. - mMicrotasksToTrace.insertBack(runnable); + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + EnqueueMicroTask(Context(), runnable.forget()); + } else { + if (!runnable->isInList()) { + // A recycled object may be in the list already. + mMicrotasksToTrace.insertBack(runnable); + } + mPendingMicroTaskRunnables.push_back(std::move(runnable)); } - mPendingMicroTaskRunnables.push_back(std::move(runnable)); } class AsyncMutationHandler final : public mozilla::Runnable { @@ -819,12 +937,179 @@ bool SuppressedMicroTasks::Suppressed() { return false; } -bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) { - if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) { - AfterProcessMicrotasks(); - // Nothing to do, return early. +LazyLogModule gLog("mtq"); + +SuppressedMicroTaskList::SuppressedMicroTaskList( + CycleCollectedJSContext* aContext) + : mContext(aContext), + mSuppressionGeneration(aContext->mSuppressionGeneration), + mSuppressedMicroTaskRunnables(aContext->Context(), aContext->Context()) {} + +bool SuppressedMicroTaskList::Suppressed() { + if (mSuppressionGeneration == mContext->mSuppressionGeneration) { + return true; + } + + MOZ_ASSERT(StaticPrefs::javascript_options_use_js_microtask_queue()); + MOZ_ASSERT(mContext->mSuppressedMicroTaskList == this); + + MOZ_LOG_FMT(gLog, LogLevel::Verbose, "Prepending %zu suppressed microtasks", + mSuppressedMicroTaskRunnables.get().length()); + for (size_t i = mSuppressedMicroTaskRunnables.get().length(); i > 0; i--) { + JS::PrependMicroTask(mContext->Context(), + mSuppressedMicroTaskRunnables.get()[i - 1]); + } + + mSuppressedMicroTaskRunnables.get().clear(); + + mContext->mSuppressedMicroTaskList = nullptr; + + // Return false: We are -not- ourselves suppressed, so, + // in PerformMicroTasks we will end up in the branch where + // we can drop the final refcount. + return false; +} + +SuppressedMicroTaskList::~SuppressedMicroTaskList() { + MOZ_ASSERT(mContext->mSuppressedMicroTaskList == nullptr); + MOZ_ASSERT(mSuppressedMicroTaskRunnables.get().empty()); +}; + +// Run a microtask. Handles both non-JS (enqueued MicroTaskRunnables) and JS +// microtasks. +static bool MOZ_CAN_RUN_SCRIPT +RunMicroTask(JSContext* aCx, JS::MutableHandle<JS::MicroTask> task) { + if (RefPtr<MicroTaskRunnable> runnable = + MaybeUnwrapTaskToOwnedRunnable(task)) { + AutoSlowOperation aso; + runnable->Run(aso); + return true; + } + + JS::Rooted<JSObject*> maybePromise(aCx, + JS::MaybeGetPromiseFromJSMicroTask(task)); + auto state = maybePromise + ? JS::GetPromiseUserInputEventHandlingState(maybePromise) + : JS::PromiseUserInputEventHandlingState::DontCare; + bool propagate = + state == + JS::PromiseUserInputEventHandlingState::HadUserInteractionAtCreation; + AutoHandlingUserInputStatePusher userInputStateSwitcher(propagate); + + JS::RootedTuple<JSObject*, JSObject*, JSObject*> roots(aCx); + + JS::RootedField<JSObject*, 0> callbackGlobal( + roots, JS::GetExecutionGlobalFromJSMicroTask(task)); + JS::RootedField<JSObject*, 1> hostDefinedData( + roots, JS::MaybeGetHostDefinedDataFromJSMicroTask(task)); + JS::RootedField<JSObject*, 2> allocStack( + roots, JS::MaybeGetAllocationSiteFromJSMicroTask(task)); + + nsIGlobalObject* incumbentGlobal = nullptr; + + WebTaskSchedulingState* schedulingState = nullptr; + if (hostDefinedData) { + MOZ_RELEASE_ASSERT(JS::GetClass(hostDefinedData.get()) == + &sHostDefinedDataClass); + JS::Value incumbentGlobalVal = + JS::GetReservedSlot(hostDefinedData, INCUMBENT_SETTING_SLOT); + // hostDefinedData is only created when incumbent global exists. + MOZ_ASSERT(incumbentGlobalVal.isObject()); + incumbentGlobal = xpc::NativeGlobal(&incumbentGlobalVal.toObject()); + + JS::Value state = + JS::GetReservedSlot(hostDefinedData, SCHEDULING_STATE_SLOT); + if (!state.isUndefined()) { + schedulingState = static_cast<WebTaskSchedulingState*>(state.toPrivate()); + } + } else { + // There are two possible causes for hostDefinedData to be missing. + // 1. It's optimized out, the SpiderMonkey expects the embedding to + // retrieve it on their own. + // 2. It's the special case for debugger usage. + // + // MG:XXX: The handling of incumbent global can be made appreciably more + // harmonious through co-evolution with the JS engine, but I have tried to + // avoid doing too much divergence for now. + JSObject* incumbentGlobalJS = + JS::MaybeGetHostDefinedGlobalFromJSMicroTask(task); + MOZ_ASSERT_IF(incumbentGlobalJS, !js::IsWrapper(incumbentGlobalJS)); + if (incumbentGlobalJS) { + incumbentGlobal = xpc::NativeGlobal(incumbentGlobalJS); + } + } + + if (incumbentGlobal && schedulingState) { + // https://wicg.github.io/scheduling-apis/#sec-patches-html-hostcalljobcallback + // 2. Set event loop’s current scheduling state to + // callback.[[HostDefined]].[[SchedulingState]]. + incumbentGlobal->SetWebTaskSchedulingState(schedulingState); + } + + // MG:XXX: It would be worth revisiting the design of CallSetup here to try + // and reduce JS microtask overheads that turn out to be superflous. For + // example, in at least some circumstances we end up having multiple realm + // changes here that don't need to happen. + // + // Similarly, IgnoredErrorResult! + IgnoredErrorResult rv; + CallSetup setup(callbackGlobal, incumbentGlobal, allocStack, rv, + "promise callback" /* Some tests care about this string. */, + dom::CallbackObject::eReportExceptions); + if (!setup.GetContext()) { return false; } + bool v = JS::RunJSMicroTask(aCx, task); + + // (The step after step 7): Set event loop’s current scheduling + // state to null + if (incumbentGlobal && schedulingState) { + incumbentGlobal->SetWebTaskSchedulingState(nullptr); + } + + return v; +} + +static bool IsSuppressed(JS::Handle<JS::MicroTask> task) { + if (JS::IsJSMicroTask(task)) { + JSObject* jsGlobal = JS::GetExecutionGlobalFromJSMicroTask(task); + if (!jsGlobal) { + return false; + } + nsIGlobalObject* global = xpc::NativeGlobal(jsGlobal); + return global && global->IsInSyncOperation(); + } + + MicroTaskRunnable* runnable = MaybeUnwrapTaskToRunnable(task); + + // If it's not a JS microtask, it must be a MicroTaskRunnable, + // and so MaybeUnwrapTaskToRunnable must return non-null. + MOZ_ASSERT(runnable, "Unexpected task type"); + + return runnable->Suppressed(); +} + +bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) { + MOZ_LOG_FMT(gLog, LogLevel::Verbose, "Called PerformMicroTaskCheckpoint"); + + JSContext* cx = Context(); + + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + if (!JS::HasAnyMicroTasks(cx)) { + MOZ_ASSERT(mDebuggerMicroTaskQueue.empty()); + MOZ_ASSERT(mPendingMicroTaskRunnables.empty()); + + // Nothing to do, return early. + AfterProcessMicrotasks(); + return false; + } + } else { + if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) { + AfterProcessMicrotasks(); + // Nothing to do, return early. + return false; + } + } uint32_t currentDepth = RecursionDepth(); if (mMicroTaskRecursionDepth && *mMicroTaskRecursionDepth >= currentDepth && @@ -854,56 +1139,139 @@ bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) { bool didProcess = false; AutoSlowOperation aso; - for (;;) { - RefPtr<MicroTaskRunnable> runnable; - if (!mDebuggerMicroTaskQueue.empty()) { - runnable = std::move(mDebuggerMicroTaskQueue.front()); - mDebuggerMicroTaskQueue.pop_front(); - } else if (!mPendingMicroTaskRunnables.empty()) { - runnable = std::move(mPendingMicroTaskRunnables.front()); - mPendingMicroTaskRunnables.pop_front(); - } else { - break; + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + // Make sure we don't leak tasks into the Gecko MicroTask queues. + MOZ_ASSERT(mDebuggerMicroTaskQueue.empty()); + MOZ_ASSERT(mPendingMicroTaskRunnables.empty()); + MOZ_ASSERT(!mSuppressedMicroTasks); + JS::Rooted<JS::MicroTask> job(cx); + while (JS::HasAnyMicroTasks(cx)) { + MOZ_ASSERT(mDebuggerMicroTaskQueue.empty()); + MOZ_ASSERT(mPendingMicroTaskRunnables.empty()); + job = JS::DequeueNextMicroTask(cx); + + // To avoid us accidentally re-enqueing a SuppressionMicroTaskList in + // itself, we determine here if the job is actually the suppression task + // list. + bool isSuppressionJob = + mSuppressedMicroTaskList + ? MaybeUnwrapTaskToRunnable(job) == mSuppressedMicroTaskList + : false; + + // No need to check Suppressed if there aren't ongoing sync operations nor + // pending mSuppressedMicroTasks.s + if ((IsInSyncOperation() || mSuppressedMicroTaskList) && + IsSuppressed(job)) { + // Microtasks in worker shall never be suppressed. + // Otherwise, the micro tasks queue will be replaced later with + // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly. + MOZ_ASSERT(NS_IsMainThread()); + JS::JobQueueMayNotBeEmpty(Context()); + + // To avoid re-enqueing a suppressed SuppressionMicroTaskList in itself. + if (!isSuppressionJob) { + if (!mSuppressedMicroTaskList) { + mSuppressedMicroTaskList = new SuppressedMicroTaskList(this); + } + + mSuppressedMicroTaskList->mSuppressedMicroTaskRunnables.get().append( + std::move(job.get())); + } + } else { + // MG:XXX: It's sort of too bad that we can't handle the JobQueueIsEmpty + // note entirely within the JS engine, but in order to do that we'd need + // to move the suppressed micro task handling inside and that's more + // divergence than I would like. + if (!JS::HasAnyMicroTasks(cx) && !mSuppressedMicroTaskList) { + JS::JobQueueIsEmpty(Context()); + } + didProcess = true; + + // Bug 1991164: Need to support LogMicroTaskQueue Entry for + // LogMicroTaskQueueEntry::Run log(job.get().get()); + + // Bug 1990870: Need to support flow markers with JS Micro Tasks. + // AUTO_PROFILER_TERMINATING_FLOW_MARKER_FLOW_ONLY( + // "CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint", + // OTHER, Flow::FromPointer(runnable.get())); + + // Note: We're dropping the return value on the floor here. This is + // consistent with the previous implementation, which left the + // exception if it was there pending on the context, but likely should + // be changed. + (void)RunMicroTask(cx, &job); + } } - // No need to check Suppressed if there aren't ongoing sync operations nor - // pending mSuppressedMicroTasks. - if ((IsInSyncOperation() || mSuppressedMicroTasks) && - runnable->Suppressed()) { - // Microtasks in worker shall never be suppressed. - // Otherwise, mPendingMicroTaskRunnables will be replaced later with - // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly. - MOZ_ASSERT(NS_IsMainThread()); - JS::JobQueueMayNotBeEmpty(Context()); - if (runnable != mSuppressedMicroTasks) { - if (!mSuppressedMicroTasks) { - mSuppressedMicroTasks = new SuppressedMicroTasks(this); - } - mSuppressedMicroTasks->mSuppressedMicroTaskRunnables.push_back( - runnable); + // Put back the suppressed microtasks so that they will be run later. + // Note, it is possible that we end up keeping these suppressed tasks around + // for some time, but no longer than spinning the event loop nestedly + // (sync XHR, alert, etc.) + if (mSuppressedMicroTaskList) { + // Like everywhere else, do_AddRef when enqueing. Then the refcount in the + // queue is 2; when ->Suppressed is called, mSuppressedMicroTaskList will + // be nulled out, dropping the refcount to 1, then when the conversion to + // owned happens, inside of RunMicroTask, the remaining ref will be + // dropped, and the code will be cleaned up. + // + // This should work generally, as if you re-enqueue the task list (we have + // no code to prevent this!) you'll just have more refs in the queue, + // all of which is good. + if (!EnqueueMicroTask(cx, do_AddRef(mSuppressedMicroTaskList))) { + MOZ_CRASH("Failed to re-enqueue suppressed microtask list"); } - } else { - if (mPendingMicroTaskRunnables.empty() && - mDebuggerMicroTaskQueue.empty() && !mSuppressedMicroTasks) { - JS::JobQueueIsEmpty(Context()); + } + } else { + for (;;) { + RefPtr<MicroTaskRunnable> runnable; + if (!mDebuggerMicroTaskQueue.empty()) { + runnable = std::move(mDebuggerMicroTaskQueue.front()); + mDebuggerMicroTaskQueue.pop_front(); + } else if (!mPendingMicroTaskRunnables.empty()) { + runnable = std::move(mPendingMicroTaskRunnables.front()); + mPendingMicroTaskRunnables.pop_front(); + } else { + break; } - didProcess = true; - LogMicroTaskRunnable::Run log(runnable.get()); - AUTO_PROFILER_TERMINATING_FLOW_MARKER_FLOW_ONLY( - "CycleCollectedJSContext::PerformMicroTaskCheckPoint", OTHER, - Flow::FromPointer(runnable.get())); - runnable->Run(aso); - runnable = nullptr; + // No need to check Suppressed if there aren't ongoing sync operations nor + // pending mSuppressedMicroTasks. + if ((IsInSyncOperation() || mSuppressedMicroTasks) && + runnable->Suppressed()) { + // Microtasks in worker shall never be suppressed. + // Otherwise, mPendingMicroTaskRunnables will be replaced later with + // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly. + MOZ_ASSERT(NS_IsMainThread()); + JS::JobQueueMayNotBeEmpty(Context()); + if (runnable != mSuppressedMicroTasks) { + if (!mSuppressedMicroTasks) { + mSuppressedMicroTasks = new SuppressedMicroTasks(this); + } + mSuppressedMicroTasks->mSuppressedMicroTaskRunnables.push_back( + runnable); + } + } else { + if (mPendingMicroTaskRunnables.empty() && + mDebuggerMicroTaskQueue.empty() && !mSuppressedMicroTasks) { + JS::JobQueueIsEmpty(Context()); + } + didProcess = true; + AUTO_PROFILER_TERMINATING_FLOW_MARKER_FLOW_ONLY( + "CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint", + OTHER, Flow::FromPointer(runnable.get())); + LogMicroTaskRunnable::Run log(runnable.get()); + runnable->Run(aso); + runnable = nullptr; + } } - } - // Put back the suppressed microtasks so that they will be run later. - // Note, it is possible that we end up keeping these suppressed tasks around - // for some time, but no longer than spinning the event loop nestedly - // (sync XHR, alert, etc.) - if (mSuppressedMicroTasks) { - mPendingMicroTaskRunnables.push_back(mSuppressedMicroTasks); + // Put back the suppressed microtasks so that they will be run later. + // Note, it is possible that we end up keeping these suppressed tasks around + // for some time, but no longer than spinning the event loop nestedly + // (sync XHR, alert, etc.) + if (mSuppressedMicroTasks) { + mPendingMicroTaskRunnables.push_back(mSuppressedMicroTasks); + } } AfterProcessMicrotasks(); @@ -915,33 +1283,63 @@ void CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() { // Don't do normal microtask handling checks here, since whoever is calling // this method is supposed to know what they are doing. - AutoSlowOperation aso; - for (;;) { - // For a debugger microtask checkpoint, we always use the debugger microtask - // queue. - std::deque<RefPtr<MicroTaskRunnable>>* microtaskQueue = - &GetDebuggerMicroTaskQueue(); - - if (microtaskQueue->empty()) { - break; + JSContext* cx = Context(); + + if (StaticPrefs::javascript_options_use_js_microtask_queue()) { + MOZ_ASSERT(GetDebuggerMicroTaskQueue().empty()); + + while (JS::HasDebuggerMicroTasks(cx)) { + MOZ_ASSERT(mDebuggerMicroTaskQueue.empty()); + MOZ_ASSERT(mPendingMicroTaskRunnables.empty()); + + JS::Rooted<JS::MicroTask> job(cx, JS::DequeueNextDebuggerMicroTask(cx)); + // Bug 1991164: Need to support LogMicroTaskQueueEntry with JS micro + // tasks. LogMicroTaskQueueEntry::Run log(job); + + // Bug 1990870: Need to support flows with JS microtasks + // AUTO_PROFILER_TERMINATING_FLOW_MARKER_FLOW_ONLY( + // "CycleCollectedJSContext::PerformMicroTaskCheckPoint", OTHER, + // Flow::FromPointer(runnable.get())); + + // Note: We're dropping the return value on the floor here. This is + // consistent with the previous implementation, which left the exception + // if it was there pending on the context, but likely should be + // changed. + (void)RunMicroTask(cx, &job); } + } else { + MOZ_ASSERT(!JS::HasAnyMicroTasks(cx)); + AutoSlowOperation aso; + for (;;) { + // For a debugger microtask checkpoint, we always use the debugger + // microtask queue. + std::deque<RefPtr<MicroTaskRunnable>>* microtaskQueue = + &GetDebuggerMicroTaskQueue(); + + if (microtaskQueue->empty()) { + break; + } - RefPtr<MicroTaskRunnable> runnable = std::move(microtaskQueue->front()); - MOZ_ASSERT(runnable); + RefPtr<MicroTaskRunnable> runnable = std::move(microtaskQueue->front()); + MOZ_ASSERT(runnable); - LogMicroTaskRunnable::Run log(runnable.get()); + LogMicroTaskRunnable::Run log(runnable.get()); - // This function can re-enter, so we remove the element before calling. - microtaskQueue->pop_front(); + // This function can re-enter, so we remove the element before calling. + microtaskQueue->pop_front(); - if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) { - JS::JobQueueIsEmpty(Context()); + if (mPendingMicroTaskRunnables.empty() && + mDebuggerMicroTaskQueue.empty()) { + JS::JobQueueIsEmpty(Context()); + } + + AUTO_PROFILER_TERMINATING_FLOW_MARKER_FLOW_ONLY( + "CycleCollectedJSContext::PerformMicroTaskCheckPoint", OTHER, + Flow::FromPointer(runnable.get())); + + runnable->Run(aso); + runnable = nullptr; } - AUTO_PROFILER_TERMINATING_FLOW_MARKER_FLOW_ONLY( - "CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint", OTHER, - Flow::FromPointer(runnable.get())); - runnable->Run(aso); - runnable = nullptr; } AfterProcessMicrotasks(); @@ -974,8 +1372,8 @@ NS_IMETHODIMP CycleCollectedJSContext::NotifyUnhandledRejections::Run() { RefPtr<PromiseRejectionEvent> event = PromiseRejectionEvent::Constructor(target, u"unhandledrejection"_ns, init); - // We don't use the result of dispatching event here to check whether to - // report the Promise to console. + // We don't use the result of dispatching event here to check whether + // to report the Promise to console. target->DispatchEvent(*event); } } diff --git a/xpcom/base/CycleCollectedJSContext.h b/xpcom/base/CycleCollectedJSContext.h @@ -16,6 +16,7 @@ #include "mozilla/dom/Promise.h" #include "js/GCVector.h" #include "js/Promise.h" +#include "js/friend/MicroTask.h" #include "nsCOMPtr.h" #include "nsRefPtrHashtable.h" @@ -104,6 +105,26 @@ class SuppressedMicroTasks : public MicroTaskRunnable { std::deque<RefPtr<MicroTaskRunnable>> mSuppressedMicroTaskRunnables; }; +class SuppressedMicroTaskList final : public MicroTaskRunnable { + public: + SuppressedMicroTaskList() = delete; + explicit SuppressedMicroTaskList(CycleCollectedJSContext* aContext); + + virtual bool Suppressed() override; + virtual void Run(AutoSlowOperation& aso) override { + // Does nothing; the only action occurs as part of the + // call to Suppressed(). + } + + CycleCollectedJSContext* mContext = nullptr; + uint64_t mSuppressionGeneration = 0; + JS::PersistentRooted<JS::GCVector<JS::MicroTask>> + mSuppressedMicroTaskRunnables; + + private: + ~SuppressedMicroTaskList(); +}; + // Support for JS FinalizationRegistry objects, which allow a JS callback to be // registered that is called when objects die. // @@ -138,9 +159,15 @@ class FinalizationRegistryCleanup { JS::PersistentRooted<CallbackVector> mCallbacks; }; -class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue { +bool EnqueueMicroTask(JSContext* aCx, + already_AddRefed<MicroTaskRunnable> aRunnable); +bool EnqueueDebugMicroTask(JSContext* aCx, + already_AddRefed<MicroTaskRunnable> aRunnable); + +class CycleCollectedJSContext : dom::PerThreadAtomCache, public JS::JobQueue { friend class CycleCollectedJSRuntime; friend class SuppressedMicroTasks; + friend class SuppressedMicroTaskList; protected: CycleCollectedJSContext(); @@ -367,6 +394,9 @@ class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue { std::deque<RefPtr<MicroTaskRunnable>> mPendingMicroTaskRunnables; std::deque<RefPtr<MicroTaskRunnable>> mDebuggerMicroTaskQueue; RefPtr<SuppressedMicroTasks> mSuppressedMicroTasks; + + RefPtr<SuppressedMicroTaskList> mSuppressedMicroTaskList; + uint64_t mSuppressionGeneration; protected: