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:
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: