commit 811324ab997c958182ef1b339cd0088dca7d545b
parent d8e3b23cd602a8fcd13221a42f31608b98e1256f
Author: Matthew Gaudet <mgaudet@mozilla.com>
Date: Wed, 1 Oct 2025 21:38:51 +0000
Bug 1983154 - Shell support for a JS MicroTaskQueue. r=arai
Also adds a method to CycleCollectedJSContext (reviewed in D261179) to
make the browser build with these changes.
Differential Revision: https://phabricator.services.mozilla.com/D261178
Diffstat:
13 files changed, 1216 insertions(+), 126 deletions(-)
diff --git a/js/public/Promise.h b/js/public/Promise.h
@@ -57,6 +57,16 @@ class JS_PUBLIC_API JobQueue {
JS::MutableHandle<JSObject*> data) const = 0;
/**
+ * If the embedding has a host-defined global, return it. This is used when
+ * we are able to optimize out the host defined data, as the embedding may
+ * still require this when running jobs.
+ *
+ * In Gecko, this is used for dealing with the incumbent global.
+ */
+ virtual bool getHostDefinedGlobal(
+ JSContext* cx, JS::MutableHandle<JSObject*> data) const = 0;
+
+ /**
* Enqueue a reaction job `job` for `promise`, which was allocated at
* `allocationSite`. Provide `hostDefineData` as the host defined data for
* the reaction job's execution.
diff --git a/js/public/friend/MicroTask.h b/js/public/friend/MicroTask.h
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * vim: set ts=8 sts=4 et sw=4 tw=99:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef js_friend_MicroTask_h
+#define js_friend_MicroTask_h
+
+#include "mozilla/Attributes.h"
+
+#include "jstypes.h"
+
+#include "js/GCPolicyAPI.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "js/UniquePtr.h"
+#include "js/ValueArray.h"
+
+namespace JS {
+
+// [SMDOC] MicroTasks in SpiderMonkey
+//
+// To enable higher performance, this header allows an embedding to work with
+// a MicroTask queue stored inside the JS engine. This allows optimization of
+// tasks by avoiding allocations in important cases.
+//
+// To make this work, we need some cooperation with the embedding.
+//
+// The high level thrust of this is that rather than managing the JobQueue
+// themselves, embeddings assume that there's a JobQueue available to them
+// inside the engine. When 'runJobs' happens, the embedding is responsible
+// for pulling jobs of the queue, doing any setup required, then calling
+// them.
+//
+// Embedding jobs are trivially supportable, since a MicroTask job is
+// represented as a JS::Value, and thus an embedding job may be put on
+// the queue by wrapping it in a JS::Value (e.g. using Private to store
+// C++ pointers).
+//
+// The major requirement is that if a MicroTask identifies as a "JS"
+// MicroTask, by passing the IsJSMicrotask predicate, the job must be
+// run by calling RunJSMicroTask, while in the realm specified by the
+// global returned by GetExecutionGlobalFromJSMicroTask, e.g
+//
+// AutoRealm ar(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
+// if (!JS::RunJSMicroTask(cx, job)) {
+// ...
+// }
+
+// A MicroTask is a JS::Value. Using this MicroTask system allows
+// embedders to put whatever pointer they would like into the queue.
+// The task will be dequeued unchanged.
+//
+// The major requirement here is that if the MicroTask is a JS
+// MicroTask (as determined by IsJSMicroTask), it must be run
+// by calling RunJSMicroTask, while in the realm specified by
+// GetExecutionGlobalFromJSMicroTask.
+//
+// An embedding is free to do with non-JS MicroTasks as it
+// sees fit.
+using MicroTask = JS::Value;
+
+JS_PUBLIC_API bool IsJSMicroTask(Handle<JS::Value> hv);
+
+// Run a MicroTask that is known to be a JS MicroTask. This will crash
+// if provided an invalid task kind.
+//
+// This will return true except on OOM.
+JS_PUBLIC_API bool RunJSMicroTask(JSContext* cx, Handle<MicroTask> entry);
+
+// Queue Management. This is done per-JSContext.
+//
+// Internally we maintain two queues, one for 'debugger' microtasks. These
+// are expected in normal operation to either be popped off the queue first,
+// or processed separately.
+//
+// Non-debugger MicroTasks are "regular" microtasks, and go to the regular
+// microtask queue.
+//
+// In general, we highly recommend that most embeddings use only the regular
+// microtask queue. The debugger microtask queue mostly exists to support
+// patterns used by Gecko.
+JS_PUBLIC_API bool EnqueueMicroTask(JSContext* cx, const MicroTask& entry);
+JS_PUBLIC_API bool EnqueueDebugMicroTask(JSContext* cx, const MicroTask& entry);
+JS_PUBLIC_API bool PrependMicroTask(JSContext* cx, const MicroTask& entry);
+
+// Dequeue the next MicroTask. If there are no MicroTasks of the appropriate
+// kind, each of the below API returns JS::NullValue().
+//
+// The generic DequeueNext will always pull a debugger microtask first,
+// if one exists, then a regular microtask if one exists.
+// - DequeueNextDebuggerMicroTask only pulls from the debugger queue.
+// - DequeueNextRegularMicroTask only pulls from the regular queue.
+//
+// Internally, these basically do
+//
+// if (HasXMicroTask()) { return X.popFront(); } return NullValue()
+//
+// so checking for emptiness before calling these is not required, and is
+// very slightly less efficient.
+JS_PUBLIC_API MicroTask DequeueNextMicroTask(JSContext* cx);
+JS_PUBLIC_API MicroTask DequeueNextDebuggerMicroTask(JSContext* cx);
+JS_PUBLIC_API MicroTask DequeueNextRegularMicroTask(JSContext* cx);
+
+// Returns true if there are -any- microtasks pending in the queue.
+JS_PUBLIC_API bool HasAnyMicroTasks(JSContext* cx);
+
+// Returns true if there are any debugger microtasks pending in the queue.
+JS_PUBLIC_API bool HasDebuggerMicroTasks(JSContext* cx);
+
+// Returns true if there are any regular (non-debugger) microtasks pending in
+// the queue.
+JS_PUBLIC_API bool HasRegularMicroTasks(JSContext* cx);
+
+// Returns the length of the regular microtask queue.
+JS_PUBLIC_API size_t GetRegularMicroTaskCount(JSContext* cx);
+
+// This is the global associated with the realm RunJSMicroTask expects to be in.
+JS_PUBLIC_API JSObject* GetExecutionGlobalFromJSMicroTask(
+ const MicroTask& entry);
+
+// To handle cases where the queue needs to be set aside for some reason
+// (mostly the Debugger API), we provide a Save and Restore API.
+//
+// When restoring the saved queue, the JSContext microtask queue must be
+// empty -- you cannot drop items by restoring over a non-empty queue
+// (so HasAnyMicroTasks must be false).
+class SavedMicroTaskQueue {
+ public:
+ SavedMicroTaskQueue() = default;
+ virtual ~SavedMicroTaskQueue() = default;
+ SavedMicroTaskQueue(const SavedMicroTaskQueue&) = delete;
+ SavedMicroTaskQueue& operator=(const SavedMicroTaskQueue&) = delete;
+};
+
+// This will return nullptr (and set OutOfMemory) if the save operation
+// fails.
+JS_PUBLIC_API js::UniquePtr<SavedMicroTaskQueue> SaveMicroTaskQueue(
+ JSContext* cx);
+JS_PUBLIC_API void RestoreMicroTaskQueue(
+ JSContext* cx, js::UniquePtr<SavedMicroTaskQueue> savedQueue);
+
+// Via the following API functions various host defined data is exposed to the
+// embedder (see JobQueue::getHostDefinedData).
+//
+// All of these may return null if there's no data, or if there's a
+// security error.
+JS_PUBLIC_API JSObject* MaybeGetHostDefinedDataFromJSMicroTask(
+ const MicroTask& entry);
+JS_PUBLIC_API JSObject* MaybeGetAllocationSiteFromJSMicroTask(
+ const MicroTask& entry);
+
+// In some circumstances an entry may not have host defined data but may
+// still have a host defined global;
+JS_PUBLIC_API JSObject* MaybeGetHostDefinedGlobalFromJSMicroTask(
+ const MicroTask& entry);
+
+JS_PUBLIC_API JSObject* MaybeGetPromiseFromJSMicroTask(const MicroTask& entry);
+
+} // namespace JS
+#endif /* js_friend_MicroTask_h */
diff --git a/js/src/builtin/Promise.cpp b/js/src/builtin/Promise.cpp
@@ -685,13 +685,62 @@ static bool AbruptRejectPromise(JSContext* cx, CallArgs& args,
capability.reject());
}
+class MicroTaskEntry : public NativeObject {
+ protected:
+ enum Slots {
+ // Shared slots:
+ Promise = 0, // see comment in PromiseReactionRecord
+ HostDefinedData, // See comment in PromiseReactionRecord
+
+ // Only needed for microtask jobs
+ AllocationStack,
+ HostDefinedGlobalRepresentative,
+ SlotCount,
+ };
+
+ public:
+ JSObject* promise() const {
+ return getFixedSlot(Slots::Promise).toObjectOrNull();
+ }
+
+ void setPromise(JSObject* obj) {
+ setFixedSlot(Slots::Promise, ObjectOrNullValue(obj));
+ }
+
+ Value getHostDefinedData() const {
+ return getFixedSlot(Slots::HostDefinedData);
+ }
+
+ void setHostDefinedData(const Value& val) {
+ setFixedSlot(Slots::HostDefinedData, val);
+ }
+
+ JSObject* allocationStack() const {
+ return getFixedSlot(Slots::AllocationStack).toObjectOrNull();
+ }
+
+ void setAllocationStack(JSObject* stack) {
+ setFixedSlot(Slots::AllocationStack, ObjectOrNullValue(stack));
+ }
+
+ JSObject* hostDefinedGlobalRepresentative() const {
+ Value v = getFixedSlot(Slots::HostDefinedGlobalRepresentative);
+ return v.isObjectOrNull() ? v.toObjectOrNull() : nullptr;
+ }
+
+ void setHostDefinedGlobalRepresentative(JSObject* global) {
+ setFixedSlot(Slots::HostDefinedGlobalRepresentative,
+ ObjectOrNullValue(global));
+ }
+};
+
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
* PromiseReaction Records
* https://tc39.es/ecma262/#sec-promisereaction-records
*/
-class PromiseReactionRecord : public NativeObject {
+class PromiseReactionRecord : public MicroTaskEntry {
// If this flag is set, this reaction record is already enqueued to the
// job queue, and the spec's [[Type]] field is represented by
// REACTION_FLAG_FULFILLED flag.
@@ -775,7 +824,20 @@ class PromiseReactionRecord : public NativeObject {
// reaction to be created. (These functions act as if they had created a
// promise to invoke the appropriate provided reaction function, without
// actually allocating a promise for them.)
- Promise = 0,
+ Promise = MicroTaskEntry::Slots::Promise,
+
+ // The host defined data for this reaction record. Can be null.
+ // See step 5 in https://html.spec.whatwg.org/#hostmakejobcallback
+ HostDefinedData = MicroTaskEntry::Slots::HostDefinedData,
+
+ // < Invisibly here are the microtask job slots from the parent class
+ // MicroTask. >
+
+ // A slot holding an object from the realm where we need to execute
+ // the reaction job. This may be a CCW. We don't store the global
+ // of the realm directly because wrappers to globals can change
+ // globals, which breaks code.
+ EnqueueGlobalRepresentative = MicroTaskEntry::Slots::SlotCount,
// The [[Handler]] field(s) of a PromiseReaction record. We create a
// single reaction record for fulfillment and rejection, therefore our
@@ -804,10 +866,6 @@ class PromiseReactionRecord : public NativeObject {
Resolve,
Reject,
- // The host defined data for this reaction record. Can be null.
- // See step 5 in https://html.spec.whatwg.org/#hostmakejobcallback
- HostDefinedData,
-
// Bitmask of the REACTION_FLAG values.
Flags,
@@ -855,12 +913,7 @@ class PromiseReactionRecord : public NativeObject {
public:
static const JSClass class_;
- JSObject* promise() const {
- return getFixedSlot(Slots::Promise).toObjectOrNull();
- }
-
int32_t flags() const { return getFixedSlot(Slots::Flags).toInt32(); }
-
JS::PromiseState targetState() const {
int32_t flags = this->flags();
if (!(flags & REACTION_FLAG_RESOLVED)) {
@@ -911,6 +964,7 @@ class PromiseReactionRecord : public NativeObject {
}
void setIsAsyncFunction(AsyncFunctionGeneratorObject* genObj) {
+ MOZ_ASSERT(realm() == genObj->nonCCWRealm());
setFlagOnInitialState(REACTION_FLAG_ASYNC_FUNCTION);
setFixedSlot(Slots::GeneratorOrPromiseToResolveOrAsyncFromSyncIterator,
ObjectValue(*genObj));
@@ -923,7 +977,10 @@ class PromiseReactionRecord : public NativeObject {
MOZ_ASSERT(isAsyncFunction());
const Value& generator =
getFixedSlot(Slots::GeneratorOrPromiseToResolveOrAsyncFromSyncIterator);
- return &generator.toObject().as<AsyncFunctionGeneratorObject>();
+ AsyncFunctionGeneratorObject* res =
+ &generator.toObject().as<AsyncFunctionGeneratorObject>();
+ MOZ_RELEASE_ASSERT(realm() == res->realm());
+ return res;
}
void setIsAsyncGenerator(AsyncGeneratorObject* generator) {
@@ -981,6 +1038,13 @@ class PromiseReactionRecord : public NativeObject {
return obj;
}
+ JSObject* enqueueGlobalRepresentative() const {
+ return getFixedSlot(Slots::EnqueueGlobalRepresentative).toObjectOrNull();
+ }
+ void setEnqueueGlobalRepresentative(JSObject* obj) {
+ setFixedSlot(Slots::EnqueueGlobalRepresentative, ObjectOrNullValue(obj));
+ }
+
#if defined(DEBUG) || defined(JS_JITSPEW)
void dumpOwnFields(js::JSONPrinter& json) const;
#endif
@@ -991,6 +1055,77 @@ const JSClass PromiseReactionRecord::class_ = {
JSCLASS_HAS_RESERVED_SLOTS(Slots::SlotCount),
};
+class ThenableJob : public MicroTaskEntry {
+ protected:
+ enum Slots {
+ // These slots come directoy after the MicroTaskEntry slots.
+ Thenable = MicroTaskEntry::Slots::SlotCount,
+ Then,
+ Callback,
+ SlotCount
+ };
+
+ public:
+ static const JSClass class_;
+
+ enum TargetFunction : int32_t {
+ PromiseResolveThenableJob,
+ PromiseResolveBuiltinThenableJob
+ };
+
+ Value thenable() const { return getFixedSlot(Slots::Thenable); }
+
+ void setThenable(const Value& val) { setFixedSlot(Slots::Thenable, val); }
+
+ JSObject* then() const { return getFixedSlot(Slots::Then).toObjectOrNull(); }
+
+ void setThen(JSObject* obj) {
+ setFixedSlot(Slots::Then, ObjectOrNullValue(obj));
+ }
+
+ TargetFunction targetFunction() const {
+ return static_cast<TargetFunction>(getFixedSlot(Slots::Callback).toInt32());
+ }
+ void setTargetFunction(TargetFunction target) {
+ setFixedSlot(Slots::Callback, JS::Int32Value(static_cast<int32_t>(target)));
+ }
+};
+
+const JSClass ThenableJob::class_ = {
+ "ThenableJob",
+ JSCLASS_HAS_RESERVED_SLOTS(ThenableJob::SlotCount),
+};
+
+ThenableJob* NewThenableJob(JSContext* cx, ThenableJob::TargetFunction target,
+ HandleObject promise, HandleValue thenable,
+ HandleObject then, HandleObject hostDefinedData) {
+ // MG:XXX: Boy isn't it silly that we have to root here, only to get the
+ // allocation site...
+ RootedObject stack(
+ cx, JS::MaybeGetPromiseAllocationSiteFromPossiblyWrappedPromise(promise));
+ if (!cx->compartment()->wrap(cx, &stack)) {
+ return nullptr;
+ }
+
+ // MG:XXX: Wrapping needs to be delegated to callers I think.
+ RootedObject hostDefined(cx, hostDefinedData);
+ if (!cx->compartment()->wrap(cx, &hostDefined)) {
+ return nullptr;
+ }
+ auto* job = NewBuiltinClassInstance<ThenableJob>(cx);
+ if (!job) {
+ return nullptr;
+ }
+ job->setPromise(promise);
+ job->setThen(then);
+ job->setThenable(thenable);
+ job->setTargetFunction(target);
+ job->setHostDefinedData(ObjectOrNullValue(hostDefined));
+ job->setAllocationStack(stack);
+
+ return job;
+}
+
static void AddPromiseFlags(PromiseObject& promise, int32_t flag) {
int32_t flags = promise.flags();
promise.setFixedSlot(PromiseSlot_Flags, Int32Value(flags | flag));
@@ -1656,29 +1791,23 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
RootedField<JSObject*, 4> handlerObj(roots, &handler.toObject());
ar2.emplace(cx, handlerObj);
- // We need to wrap the reaction to store it on the job function.
+ // This is wrapped here because it may be a cross comaprtment
+ // reference, and so should be wrapped to be stored on the job function.
+ // (it's also important because this indicates to PromiseReactionJob
+ // that it needs to switch realms).
if (!cx->compartment()->wrap(cx, &reactionVal)) {
return false;
}
}
- // NewPromiseReactionJob
- // Step 1. Let job be a new Job Abstract Closure with no parameters that
- // captures reaction and argument and performs the following steps
- // when called:
- Handle<PropertyName*> funName = cx->names().empty_;
- RootedField<JSFunction*, 5> job(
- roots,
- NewNativeFunction(cx, PromiseReactionJob, 0, funName,
- gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
- if (!job) {
- return false;
- }
-
- job->setExtendedSlot(ReactionJobSlot_ReactionRecord, reactionVal);
-
// When using JS::AddPromiseReactions{,IgnoringUnHandledRejection}, no actual
// promise is created, so we might not have one here.
+ //
+ // Bug 1977691: This comment needs updating; I don't think
+ // JS::AddPromiseReactions happens without a promise anymore, _however_ async
+ // functions may not have a promise.
+ //
+ //
// Additionally, we might have an object here that isn't an instance of
// Promise. This can happen if content overrides the value of
// Promise[@@species] (or invokes Promise#then on a Promise subclass
@@ -1710,6 +1839,73 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
}
}
+ // NewPromiseReactionJob
+ // Step 1 (reordered). Let job be a new Job Abstract Closure with no
+ // parameters that captures reaction and argument
+ // and performs the following steps when called:
+ if (JS::Prefs::use_js_microtask_queue()) {
+ MOZ_ASSERT(reactionVal.isObject());
+
+ // Get a representative object for this global: We will use this later
+ // to extract the target global for execution. We don't store the global
+ // directly because CCWs to globals can change identity.
+ //
+ // So instead we simply store Object.prototype from the target global,
+ // an object which always exists.
+ RootedObject globalRepresentative(cx, &cx->global()->getObjectPrototype());
+
+ // PromiseReactionJob job will use the existence of a CCW as a signal
+ // to change to the reactionVal's realm for execution. I believe
+ // this is the right thing to do. As a result however we don't actually
+ // need to track the global. We simply allow PromiseReactionJob to
+ // do the right thing. We will need to enqueue a CCW however
+ {
+ AutoRealm ar(cx, reaction);
+
+ RootedObject stack(
+ cx,
+ JS::MaybeGetPromiseAllocationSiteFromPossiblyWrappedPromise(promise));
+ if (!cx->compartment()->wrap(cx, &stack)) {
+ return false;
+ }
+ reaction->setAllocationStack(stack);
+
+ if (!reaction->getHostDefinedData().isObject()) {
+ // We do need to still provide an incumbentGlobal here
+ // MG:XXX: I'm pretty sure this can be appreciably more elegant later.
+ RootedObject hostGlobal(cx);
+ if (!cx->jobQueue->getHostDefinedGlobal(cx, &hostGlobal)) {
+ return false;
+ }
+
+ if (hostGlobal) {
+ MOZ_ASSERT(hostGlobal->is<GlobalObject>());
+ // Recycle the root -- we store the prototype for the same
+ // reason as EnqueueGlobalRepresentative.
+ hostGlobal = &hostGlobal->as<GlobalObject>().getObjectPrototype();
+ }
+
+ if (!cx->compartment()->wrap(cx, &hostGlobal)) {
+ return false;
+ }
+ reaction->setHostDefinedGlobalRepresentative(hostGlobal);
+ }
+
+ if (!cx->compartment()->wrap(cx, &globalRepresentative)) {
+ return false;
+ }
+ reaction->setEnqueueGlobalRepresentative(globalRepresentative);
+ }
+
+ if (!cx->compartment()->wrap(cx, &reactionVal)) {
+ return false;
+ }
+
+ // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
+ return cx->microTaskQueues->enqueueRegularMicroTask(
+ cx, std::move(reactionVal.get()));
+ }
+
RootedField<JSObject*, 7> hostDefinedData(roots);
if (JSObject* hostDefined = reaction->getAndClearHostDefinedData()) {
hostDefined = CheckedUnwrapStatic(hostDefined);
@@ -1723,7 +1919,17 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp);
hostDefinedData = hostDefined;
}
- // HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
+ Handle<PropertyName*> funName = cx->names().empty_;
+ RootedField<JSFunction*, 5> job(
+ roots,
+ NewNativeFunction(cx, PromiseReactionJob, 0, funName,
+ gc::AllocKind::FUNCTION_EXTENDED, GenericObject));
+ if (!job) {
+ return false;
+ }
+
+ job->setExtendedSlot(ReactionJobSlot_ReactionRecord, reactionVal);
+
return cx->runtime()->enqueuePromiseJob(cx, job, promise, hostDefinedData);
}
@@ -2298,17 +2504,8 @@ static bool ForEachReaction(JSContext* cx, HandleValue reactionsVal, F f) {
* JSFunction object, with all information required for the job's
* execution stored in in a reaction record in its first extended slot.
*/
-static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
-
- RootedFunction job(cx, &args.callee().as<JSFunction>());
-
- // Promise reactions don't return any value.
- args.rval().setUndefined();
-
- RootedObject reactionObj(
- cx, &job->getExtendedSlot(ReactionJobSlot_ReactionRecord).toObject());
-
+static bool PromiseReactionJob(JSContext* cx, HandleObject reactionObjIn) {
+ RootedObject reactionObj(cx, reactionObjIn);
// To ensure that the embedding ends up with the right entry global, we're
// guaranteeing that the reaction job function gets created in the same
// compartment as the handler function. That's not necessarily the global
@@ -2316,6 +2513,9 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
// We can find the triggering global via the job's reaction record. To go
// back, we check if the reaction is a wrapper and if so, unwrap it and
// enter its compartment.
+ //
+ // MG:XXX: I think that when we switch over to using the JS microtask
+ // queue exclusively there's some cleanup around realm handling possible.
mozilla::Maybe<AutoRealm> ar;
if (!IsProxy(reactionObj)) {
MOZ_RELEASE_ASSERT(reactionObj->is<PromiseReactionRecord>());
@@ -2337,6 +2537,8 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
return DefaultResolvingPromiseReactionJob(cx, reaction);
}
if (reaction->isAsyncFunction()) {
+ MOZ_RELEASE_ASSERT(reaction->asyncFunctionGenerator()->realm() ==
+ cx->realm());
return AsyncFunctionPromiseReactionJob(cx, reaction);
}
if (reaction->isAsyncGenerator()) {
@@ -2460,6 +2662,19 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
reaction->unhandledRejectionBehavior());
}
+static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ RootedFunction job(cx, &args.callee().as<JSFunction>());
+
+ // Promise reactions don't return any value.
+ args.rval().setUndefined();
+
+ RootedObject reactionObj(
+ cx, &job->getExtendedSlot(ReactionJobSlot_ReactionRecord).toObject());
+ return PromiseReactionJob(cx, reactionObj);
+}
+
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
@@ -2471,20 +2686,9 @@ static bool PromiseReactionJob(JSContext* cx, unsigned argc, Value* vp) {
* A PromiseResolveThenableJob is set as the native function of an extended
* JSFunction object, with all information required for the job's
* execution stored in the function's extended slots.
- *
- * Usage of the function's extended slots is described in the ThenableJobSlots
- * enum.
*/
-static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
-
- RootedFunction job(cx, &args.callee().as<JSFunction>());
- RootedObject promise(
- cx, &job->getExtendedSlot(ThenableJobSlot_Promise).toObject());
- RootedValue thenable(cx, job->getExtendedSlot(ThenableJobSlot_Thenable));
- RootedValue then(cx, job->getExtendedSlot(ThenableJobSlot_Handler));
- MOZ_ASSERT(then.isObject());
-
+static bool PromiseResolveThenableJob(JSContext* cx, HandleObject promise,
+ HandleValue thenable, HandleObject then) {
// Step 1.a. Let resolvingFunctions be
// CreateResolvingFunctions(promiseToResolve).
RootedObject resolveFn(cx);
@@ -2503,7 +2707,7 @@ static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
// In difference to the usual pattern, we return immediately on success.
RootedValue rval(cx);
- if (Call(cx, then, thenable, args2, &rval)) {
+ if (Call(cx, thenable, then, args2, &rval)) {
// Step 1.d. Return Completion(thenCallResult).
return true;
}
@@ -2523,6 +2727,23 @@ static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
return Call(cx, rejectVal, UndefinedHandleValue, rval, &rval);
}
+/*
+ * Usage of the function's extended slots is described in the ThenableJobSlots
+ * enum.
+ */
+static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ RootedFunction job(cx, &args.callee().as<JSFunction>());
+ RootedObject promise(
+ cx, &job->getExtendedSlot(ThenableJobSlot_Promise).toObject());
+ RootedValue thenable(cx, job->getExtendedSlot(ThenableJobSlot_Thenable));
+ RootedObject then(cx,
+ &job->getExtendedSlot(ThenableJobSlot_Handler).toObject());
+
+ return PromiseResolveThenableJob(cx, promise, thenable, then);
+}
+
[[nodiscard]] static bool OriginalPromiseThenWithoutSettleHandlers(
JSContext* cx, Handle<PromiseObject*> promise,
Handle<PromiseObject*> promiseToResolve);
@@ -2546,18 +2767,9 @@ static bool PromiseResolveThenableJob(JSContext* cx, unsigned argc, Value* vp) {
* Usage of the function's extended slots is described in the ThenableJobSlots
* enum.
*/
-static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
- Value* vp) {
- CallArgs args = CallArgsFromVp(argc, vp);
-
- RootedFunction job(cx, &args.callee().as<JSFunction>());
- RootedObject promise(
- cx, &job->getExtendedSlot(ThenableJobSlot_Promise).toObject());
- RootedObject thenable(
- cx, &job->getExtendedSlot(ThenableJobSlot_Thenable).toObject());
- // The handler slot is not used for builtin `then`.
- MOZ_ASSERT(job->getExtendedSlot(ThenableJobSlot_Handler).isUndefined());
-
+static bool PromiseResolveBuiltinThenableJob(JSContext* cx,
+ HandleObject promise,
+ HandleObject thenable) {
cx->check(promise, thenable);
MOZ_ASSERT(promise->is<PromiseObject>());
MOZ_ASSERT(thenable->is<PromiseObject>());
@@ -2602,6 +2814,21 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
stack);
}
+static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
+ Value* vp) {
+ CallArgs args = CallArgsFromVp(argc, vp);
+
+ RootedFunction job(cx, &args.callee().as<JSFunction>());
+ RootedObject promise(
+ cx, &job->getExtendedSlot(ThenableJobSlot_Promise).toObject());
+ RootedObject thenable(
+ cx, &job->getExtendedSlot(ThenableJobSlot_Thenable).toObject());
+ // The handler slot is not used for builtin `then`.
+ MOZ_ASSERT(job->getExtendedSlot(ThenableJobSlot_Handler).isUndefined());
+
+ return PromiseResolveBuiltinThenableJob(cx, promise, thenable);
+}
+
/**
* ES2022 draft rev d03c1ec6e235a5180fa772b6178727c17974cb14
*
@@ -2660,6 +2887,43 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
return false;
}
+ // At this point the promise is guaranteed to be wrapped into the job's
+ // compartment.
+ RootedObject promise(cx, &promiseToResolve.toObject());
+
+ if (JS::Prefs::use_js_microtask_queue()) {
+ RootedObject hostDefinedGlobalRepresentative(cx);
+ {
+ RootedObject hostDefinedGlobal(cx);
+ if (!cx->jobQueue->getHostDefinedGlobal(cx, &hostDefinedGlobal)) {
+ return false;
+ }
+
+ MOZ_ASSERT_IF(hostDefinedGlobal, hostDefinedGlobal->is<GlobalObject>());
+ if (hostDefinedGlobal) {
+ hostDefinedGlobalRepresentative =
+ &hostDefinedGlobal->as<GlobalObject>().getObjectPrototype();
+ }
+ }
+
+ // Wrap the representative.
+ if (!cx->compartment()->wrap(cx, &hostDefinedGlobalRepresentative)) {
+ return false;
+ }
+
+ ThenableJob* thenableJob =
+ NewThenableJob(cx, ThenableJob::PromiseResolveThenableJob, promise,
+ thenable, then, HostDefinedDataIsOptimizedOut);
+ if (!thenableJob) {
+ return false;
+ }
+
+ thenableJob->setHostDefinedGlobalRepresentative(
+ hostDefinedGlobalRepresentative);
+ return cx->microTaskQueues->enqueueRegularMicroTask(
+ cx, ObjectValue(*thenableJob));
+ }
+
// Step 1. Let job be a new Job Abstract Closure with no parameters that
// captures promiseToResolve, thenable, and then and performs the
// following steps when called:
@@ -2677,10 +2941,6 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
job->setExtendedSlot(ThenableJobSlot_Thenable, thenable);
job->setExtendedSlot(ThenableJobSlot_Handler, ObjectValue(*then));
- // At this point the promise is guaranteed to be wrapped into the job's
- // compartment.
- RootedObject promise(cx, &promiseToResolve.toObject());
-
// Step X. HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
return cx->runtime()->enqueuePromiseJob(cx, job, promise,
HostDefinedDataIsOptimizedOut);
@@ -2705,6 +2965,26 @@ static bool PromiseResolveBuiltinThenableJob(JSContext* cx, unsigned argc,
MOZ_ASSERT(promiseToResolve->is<PromiseObject>());
MOZ_ASSERT(thenable->is<PromiseObject>());
+ if (JS::Prefs::use_js_microtask_queue()) {
+ // Step 1. Let job be a new Job Abstract Closure with no parameters that
+
+ Rooted<JSObject*> hostDefinedData(cx);
+ if (!cx->runtime()->getHostDefinedData(cx, &hostDefinedData)) {
+ return false;
+ }
+
+ RootedValue thenableValue(cx, ObjectValue(*thenable));
+ ThenableJob* thenableJob = NewThenableJob(
+ cx, ThenableJob::PromiseResolveBuiltinThenableJob, promiseToResolve,
+ thenableValue, nullptr, hostDefinedData);
+ if (!thenableJob) {
+ return false;
+ }
+
+ return cx->microTaskQueues->enqueueRegularMicroTask(
+ cx, ObjectValue(*thenableJob));
+ }
+
// Step 1. Let job be a new Job Abstract Closure with no parameters that
// captures promiseToResolve, thenable, and then and performs the
// following steps when called:
@@ -5863,6 +6143,8 @@ template <typename T>
JSContext* cx, Handle<AsyncFunctionGeneratorObject*> genObj,
HandleValue value) {
auto extra = [&](Handle<PromiseReactionRecord*> reaction) {
+ MOZ_ASSERT(genObj->realm() == reaction->realm());
+ MOZ_ASSERT(genObj->realm() == cx->realm());
reaction->setIsAsyncFunction(genObj);
};
if (!InternalAwait(cx, value, nullptr,
@@ -7333,6 +7615,191 @@ void PromiseObject::dumpOwnStringContent(js::GenericPrinter& out) const {}
return true;
}
+JS_PUBLIC_API bool JS::RunJSMicroTask(JSContext* cx, Handle<MicroTask> entry) {
+#ifdef DEBUG
+ MOZ_ASSERT(entry.isObject());
+ JSObject* global = JS::GetExecutionGlobalFromJSMicroTask(entry);
+ MOZ_ASSERT(global == cx->global());
+#endif
+
+ RootedObject task(cx, &entry.toObject());
+ MOZ_ASSERT(!JS_IsDeadWrapper(task));
+
+ RootedObject unwrappedTask(cx, UncheckedUnwrap(&entry.toObject()));
+ MOZ_ASSERT(unwrappedTask);
+
+ if (unwrappedTask->is<PromiseReactionRecord>()) {
+ // Note: We don't store a callback for promise reaction records because they
+ // always call back into PromiseReactionJob.
+ //
+ // Note: We pass the (maybe)wrapped task here since PromiseReactionJob will
+ // decide what realm to be in based on the wrapper if it exists.
+ return PromiseReactionJob(cx, task);
+ }
+
+ if (unwrappedTask->is<ThenableJob>()) {
+ ThenableJob* job = &unwrappedTask->as<ThenableJob>();
+ ThenableJob::TargetFunction target = job->targetFunction();
+
+ // MG:XXX: Note: Because we don't care about the result of these values
+ // after the call, do these really have to be rooted (I don't think so?)
+ RootedTuple<JSObject*, Value, JSObject*, JSObject*> roots(cx);
+ RootedField<JSObject*, 0> promise(roots, job->promise());
+ RootedField<Value, 1> thenable(roots, job->thenable());
+
+ switch (target) {
+ case ThenableJob::PromiseResolveThenableJob: {
+ // MG:XXX: Unify naming: is it `then` or `handler` make up your mind.
+ RootedField<JSObject*, 3> then(roots, job->then());
+ return PromiseResolveThenableJob(cx, promise, thenable, then);
+ }
+ case ThenableJob::PromiseResolveBuiltinThenableJob: {
+ RootedField<JSObject*, 2> thenableObj(roots,
+ &job->thenable().toObject());
+ return PromiseResolveBuiltinThenableJob(cx, promise, thenableObj);
+ }
+ }
+ MOZ_CRASH("Corrupted Target Function");
+ return false;
+ }
+
+ MOZ_CRASH("Unknown Job type");
+ return false;
+}
+
+template <>
+inline bool JSObject::is<MicroTaskEntry>() const {
+ return is<ThenableJob>() || is<PromiseReactionRecord>();
+}
+
+JS_PUBLIC_API JSObject* JS::MaybeGetHostDefinedDataFromJSMicroTask(
+ const MicroTask& entry) {
+ if (!entry.isObject()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(!JS_IsDeadWrapper(&entry.toObject()));
+ JSObject* task = CheckedUnwrapStatic(&entry.toObject());
+ if (!task) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(task->is<MicroTaskEntry>());
+ JSObject* maybeHostDefined =
+ task->as<MicroTaskEntry>().getHostDefinedData().toObjectOrNull();
+
+ if (!maybeHostDefined) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!JS_IsDeadWrapper(maybeHostDefined));
+ return CheckedUnwrapStatic(maybeHostDefined);
+}
+
+JS_PUBLIC_API JSObject* JS::MaybeGetAllocationSiteFromJSMicroTask(
+ const MicroTask& entry) {
+ if (!entry.isObject()) {
+ return nullptr;
+ }
+ JSObject* task = UncheckedUnwrap(&entry.toObject());
+ MOZ_ASSERT(task);
+ if (JS_IsDeadWrapper(task)) {
+ return nullptr;
+ };
+
+ MOZ_ASSERT(task->is<MicroTaskEntry>());
+ JSObject* maybeWrappedStack = task->as<MicroTaskEntry>().allocationStack();
+
+ // If the stack is in a compartment which has gone away, best we
+ // can do is return nullptr.
+ if (!maybeWrappedStack || JS_IsDeadWrapper(maybeWrappedStack)) {
+ return nullptr;
+ }
+
+ JSObject* unwrapped = UncheckedUnwrap(maybeWrappedStack);
+ MOZ_ASSERT(unwrapped->is<SavedFrame>());
+ return unwrapped;
+}
+
+JS_PUBLIC_API JSObject* JS::MaybeGetHostDefinedGlobalFromJSMicroTask(
+ const MicroTask& entry) {
+ if (!entry.isObject()) {
+ return nullptr;
+ }
+ JSObject* task = UncheckedUnwrap(&entry.toObject());
+ MOZ_ASSERT(task->is<MicroTaskEntry>());
+
+ JSObject* maybeWrappedHostDefinedRepresentative =
+ task->as<MicroTaskEntry>().hostDefinedGlobalRepresentative();
+
+ if (maybeWrappedHostDefinedRepresentative) {
+ return &UncheckedUnwrap(maybeWrappedHostDefinedRepresentative)
+ ->nonCCWGlobal();
+ }
+
+ return nullptr;
+}
+
+JS_PUBLIC_API JSObject* JS::GetExecutionGlobalFromJSMicroTask(
+ const MicroTask& entry) {
+ MOZ_RELEASE_ASSERT(entry.isObject(), "Only use on JSMicroTasks");
+
+ JSObject* unwrapped = UncheckedUnwrap(&entry.toObject());
+ if (unwrapped->is<PromiseReactionRecord>()) {
+ // Use the stored equeue representative (which may need to be unwrapped)
+ JSObject* enqueueGlobalRepresentative =
+ unwrapped->as<PromiseReactionRecord>().enqueueGlobalRepresentative();
+ JSObject* unwrappedRepresentative =
+ UncheckedUnwrap(enqueueGlobalRepresentative);
+
+ // We shouldn't lose the representative object as the global should remain
+ // alive while this job is pending; the global should be entrained because
+ // it will either be the global of the PromiseReactionRecord (which)
+ // should have been kept alive by being in the queue or rooted, or
+ // it should be the global of the handler function, which should
+ // be entrained by the PromiseReactionRecord.
+ MOZ_RELEASE_ASSERT(!JS_IsDeadWrapper(unwrappedRepresentative));
+
+ return &unwrappedRepresentative->nonCCWGlobal();
+ }
+
+ // Thenable jobs are allocated in the right realm+global and so we
+ // can just use nonCCWGlobal;
+ if (unwrapped->is<ThenableJob>()) {
+ return &unwrapped->nonCCWGlobal();
+ }
+
+ MOZ_CRASH("Somehow we lost the execution global");
+}
+
+JS_PUBLIC_API JSObject* JS::MaybeGetPromiseFromJSMicroTask(
+ const MicroTask& entry) {
+ MOZ_RELEASE_ASSERT(entry.isObject(), "Only use on JSMicroTasks");
+
+ JSObject* unwrapped = UncheckedUnwrap(&entry.toObject());
+
+ // We don't expect to ever lose the record a job points to.
+ MOZ_RELEASE_ASSERT(!JS_IsDeadWrapper(unwrapped));
+
+ if (unwrapped->is<MicroTaskEntry>()) {
+ return unwrapped->as<MicroTaskEntry>().promise();
+ }
+ return nullptr;
+}
+
+JS_PUBLIC_API bool JS::IsJSMicroTask(Handle<JS::Value> hv) {
+ if (!hv.isObject()) {
+ return false;
+ }
+
+ JSObject* unwrapped = UncheckedUnwrap(&hv.toObject());
+
+ // On the off chance someone hands us a dead wrapper.
+ if (JS_IsDeadWrapper(unwrapped)) {
+ return false;
+ }
+ return unwrapped->is<MicroTaskEntry>();
+}
+
JS::AutoDebuggerJobQueueInterruption::AutoDebuggerJobQueueInterruption()
: cx(nullptr) {}
diff --git a/js/src/jit-test/tests/js_microtask/microtask-smoke-test.js b/js/src/jit-test/tests/js_microtask/microtask-smoke-test.js
@@ -0,0 +1,136 @@
+// |jit-test| --setpref=use_js_microtask_queue=true;
+
+// Promise microtask queue smoke tests
+// Test basic promise resolution and microtask ordering
+
+// Test 1: Basic promise resolution
+let resolved = false;
+Promise.resolve(42).then(value => {
+ assertEq(value, 42);
+ resolved = true;
+});
+assertEq(resolved, false); // Should not be resolved synchronously
+drainJobQueue();
+assertEq(resolved, true); // Should be resolved after draining
+
+// Test 2: Promise rejection
+let rejected = false;
+let rejectionValue = null;
+Promise.reject("error").catch(err => {
+ rejectionValue = err;
+ rejected = true;
+});
+drainJobQueue();
+assertEq(rejected, true);
+assertEq(rejectionValue, "error");
+
+// Test 3: Chained promises
+let chainResult = [];
+Promise.resolve(1)
+ .then(x => {
+ chainResult.push(x);
+ return x + 1;
+ })
+ .then(x => {
+ chainResult.push(x);
+ return x * 2;
+ })
+ .then(x => {
+ chainResult.push(x);
+ });
+drainJobQueue();
+assertEq(chainResult.length, 3);
+assertEq(chainResult[0], 1);
+assertEq(chainResult[1], 2);
+assertEq(chainResult[2], 4);
+
+// Test 4: Multiple independent promises
+let results = [];
+Promise.resolve("A").then(x => results.push(x));
+Promise.resolve("B").then(x => results.push(x));
+Promise.resolve("C").then(x => results.push(x));
+drainJobQueue();
+assertEq(results.length, 3);
+assertEq(results.includes("A"), true);
+assertEq(results.includes("B"), true);
+assertEq(results.includes("C"), true);
+
+// Test 5: Promise.all
+let allResolved = false;
+let allResults = null;
+Promise.all([
+ Promise.resolve(10),
+ Promise.resolve(20),
+ Promise.resolve(30)
+]).then(values => {
+ allResults = values;
+ allResolved = true;
+});
+drainJobQueue();
+assertEq(allResolved, true);
+assertEq(allResults.length, 3);
+assertEq(allResults[0], 10);
+assertEq(allResults[1], 20);
+assertEq(allResults[2], 30);
+
+// Test 6: Promise.race
+let raceWinner = null;
+Promise.race([
+ Promise.resolve("first"),
+ Promise.resolve("second")
+]).then(winner => {
+ raceWinner = winner;
+});
+drainJobQueue();
+assertEq(raceWinner, "first");
+
+// Test 7: Mixed sync/async execution order
+let executionOrder = [];
+executionOrder.push("sync1");
+Promise.resolve().then(() => executionOrder.push("async1"));
+executionOrder.push("sync2");
+Promise.resolve().then(() => executionOrder.push("async2"));
+executionOrder.push("sync3");
+drainJobQueue();
+assertEq(executionOrder[0], "sync1");
+assertEq(executionOrder[1], "sync2");
+assertEq(executionOrder[2], "sync3");
+assertEq(executionOrder[3], "async1");
+assertEq(executionOrder[4], "async2");
+
+// Test 8: Nested promise creation
+let nestedResults = [];
+Promise.resolve().then(() => {
+ nestedResults.push("outer");
+ Promise.resolve().then(() => {
+ nestedResults.push("inner");
+ });
+});
+drainJobQueue();
+assertEq(nestedResults.length, 2);
+assertEq(nestedResults[0], "outer");
+assertEq(nestedResults[1], "inner");
+
+// Test 9: Error handling in chains
+let errorCaught = false;
+let errorMessage = null;
+Promise.resolve()
+ .then(() => {
+ throw new Error("test error");
+ })
+ .catch(e => {
+ errorCaught = true;
+ errorMessage = e.message;
+ });
+drainJobQueue();
+assertEq(errorCaught, true);
+assertEq(errorMessage, "test error");
+
+// Test 10: Promise constructor execution
+let constructorExecuted = false;
+let constructorResolve = null;
+new Promise((resolve, reject) => {
+ constructorExecuted = true;
+ constructorResolve = resolve;
+});
+assertEq(constructorExecuted, true);
diff --git a/js/src/moz.build b/js/src/moz.build
@@ -265,6 +265,7 @@ EXPORTS.js.friend += [
"../public/friend/ErrorMessages.h",
"../public/friend/ErrorNumbers.msg",
"../public/friend/JSMEnvironment.h",
+ "../public/friend/MicroTask.h",
"../public/friend/PerformanceHint.h",
"../public/friend/StackLimits.h",
"../public/friend/UsageStatistics.h",
diff --git a/js/src/shell/js.cpp b/js/src/shell/js.cpp
@@ -1452,18 +1452,35 @@ static bool DrainJobQueue(JSContext* cx, unsigned argc, Value* vp) {
static bool GlobalOfFirstJobInQueue(JSContext* cx, unsigned argc, Value* vp) {
CallArgs args = CallArgsFromVp(argc, vp);
- RootedObject job(cx, cx->internalJobQueue->maybeFront());
- if (!job) {
- JS_ReportErrorASCII(cx, "Job queue is empty");
- return false;
- }
+ if (JS::Prefs::use_js_microtask_queue()) {
+ if (cx->microTaskQueues->microTaskQueue.empty()) {
+ JS_ReportErrorASCII(cx, "Job queue is empty");
+ return false;
+ }
- RootedObject global(cx, &job->nonCCWGlobal());
- if (!cx->compartment()->wrap(cx, &global)) {
- return false;
+ auto& job = cx->microTaskQueues->microTaskQueue.front();
+ RootedObject global(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
+ MOZ_ASSERT(global);
+ if (!cx->compartment()->wrap(cx, &global)) {
+ return false;
+ }
+
+ args.rval().setObject(*global);
+ } else {
+ RootedObject job(cx, cx->internalJobQueue->maybeFront());
+ if (!job) {
+ JS_ReportErrorASCII(cx, "Job queue is empty");
+ return false;
+ }
+
+ RootedObject global(cx, &job->nonCCWGlobal());
+ if (!cx->compartment()->wrap(cx, &global)) {
+ return false;
+ }
+
+ args.rval().setObject(*global);
}
- args.rval().setObject(*global);
return true;
}
diff --git a/js/src/vm/AsyncFunction.cpp b/js/src/vm/AsyncFunction.cpp
@@ -153,6 +153,7 @@ static bool AsyncFunctionResume(JSContext* cx,
FixedInvokeArgs<1> args(cx);
args[0].set(valueOrReason);
RootedValue generatorOrValue(cx, ObjectValue(*generator));
+ MOZ_RELEASE_ASSERT(cx->realm() == generator->nonCCWRealm());
if (!CallSelfHostedFunction(cx, funName, generatorOrValue, args,
&generatorOrValue)) {
if (!generator->isClosed()) {
diff --git a/js/src/vm/JSContext.cpp b/js/src/vm/JSContext.cpp
@@ -44,7 +44,8 @@
#include "js/ContextOptions.h" // JS::ContextOptions
#include "js/ErrorInterceptor.h" // JSErrorInterceptor
#include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_*
-#include "js/friend/StackLimits.h" // js::ReportOverRecursed
+#include "js/friend/MicroTask.h"
+#include "js/friend/StackLimits.h" // js::ReportOverRecursed
#include "js/MemoryCallbacks.h"
#include "js/Prefs.h"
#include "js/Printf.h"
@@ -133,6 +134,11 @@ bool JSContext::init() {
return false;
}
+ this->microTaskQueues = js::MakeUnique<js::MicroTaskQueueSet>(this);
+ if (!this->microTaskQueues) {
+ return false;
+ }
+
#ifdef DEBUG
// Set the initialized_ last, so that ProtectedData checks will allow us to
// initialize this context before it becomes the runtime's active context.
@@ -214,6 +220,7 @@ void js::DestroyContext(JSContext* cx) {
cx->jobQueue = nullptr;
cx->internalJobQueue = nullptr;
+ cx->microTaskQueues = nullptr;
SetContextProfilingStack(cx, nullptr);
JSRuntime* rt = cx->runtime();
@@ -772,15 +779,42 @@ JSObject* InternalJobQueue::copyJobs(JSContext* cx) {
return nullptr;
}
- for (const JSObject* unwrappedJob : queue.get()) {
- RootedObject job(cx, const_cast<JSObject*>(unwrappedJob));
- if (!cx->compartment()->wrap(cx, &job)) {
+ if (JS::Prefs::use_js_microtask_queue()) {
+ auto& queues = cx->microTaskQueues;
+ auto addToArray = [&](auto& queue) -> bool {
+ for (const auto& e : queue) {
+ if (JS::GetExecutionGlobalFromJSMicroTask(e)) {
+ // All any test cares about is the global of the job so let's do it.
+ RootedObject global(cx, JS::GetExecutionGlobalFromJSMicroTask(e));
+ if (!cx->compartment()->wrap(cx, &global)) {
+ return false;
+ }
+ if (!NewbornArrayPush(cx, jobs, ObjectValue(*global))) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ };
+
+ if (!addToArray(queues->debugMicroTaskQueue)) {
return nullptr;
}
-
- if (!NewbornArrayPush(cx, jobs, ObjectValue(*job))) {
+ if (!addToArray(queues->microTaskQueue)) {
return nullptr;
}
+ } else {
+ for (const JSObject* unwrappedJob : queue.get()) {
+ RootedObject job(cx, const_cast<JSObject*>(unwrappedJob));
+ if (!cx->compartment()->wrap(cx, &job)) {
+ return nullptr;
+ }
+
+ if (!NewbornArrayPush(cx, jobs, ObjectValue(*job))) {
+ return nullptr;
+ }
+ }
}
return jobs;
@@ -814,6 +848,11 @@ JS_PUBLIC_API void js::RunJobs(JSContext* cx) {
JS::ClearKeptObjects(cx);
}
+bool InternalJobQueue::getHostDefinedGlobal(
+ JSContext* cx, MutableHandle<JSObject*> out) const {
+ return true;
+}
+
bool InternalJobQueue::getHostDefinedData(
JSContext* cx, JS::MutableHandle<JSObject*> data) const {
data.set(nullptr);
@@ -849,46 +888,91 @@ void InternalJobQueue::runJobs(JSContext* cx) {
// so we simply ignore nested calls of drainJobQueue.
draining_ = true;
- RootedObject job(cx);
- JS::HandleValueArray args(JS::HandleValueArray::empty());
- RootedValue rval(cx);
+ if (JS::Prefs::use_js_microtask_queue()) {
+ // Execute jobs in a loop until we've reached the end of the queue.
+ JS::Rooted<JS::MicroTask> job(cx);
+ while (JS::HasAnyMicroTasks(cx)) {
+ MOZ_ASSERT(queue.empty());
+ // A previous job might have set this flag. E.g., the js shell
+ // sets it if the `quit` builtin function is called.
+ if (interrupted_) {
+ break;
+ }
- // Execute jobs in a loop until we've reached the end of the queue.
- while (!queue.empty()) {
- // A previous job might have set this flag. E.g., the js shell
- // sets it if the `quit` builtin function is called.
- if (interrupted_) {
- break;
- }
+ cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
- cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
+ job = JS::DequeueNextMicroTask(cx);
+ MOZ_ASSERT(!job.isNull());
- job = queue.front();
- queue.popFront();
+ // If the next job is the last job in the job queue, allow
+ // skipping the standard job queuing behavior.
+ if (!JS::HasAnyMicroTasks(cx)) {
+ JS::JobQueueIsEmpty(cx);
+ }
- // If the next job is the last job in the job queue, allow
- // skipping the standard job queuing behavior.
- if (queue.empty()) {
- JS::JobQueueIsEmpty(cx);
+ MOZ_ASSERT(JS::GetExecutionGlobalFromJSMicroTask(job) != nullptr);
+ AutoRealm ar(cx, JS::GetExecutionGlobalFromJSMicroTask(job));
+ {
+ if (!JS::RunJSMicroTask(cx, job)) {
+ // Nothing we can do about uncatchable exceptions.
+ if (!cx->isExceptionPending()) {
+ continue;
+ }
+
+ // Always clear the exception, because
+ // PrepareScriptEnvironmentAndInvoke will assert that we don't have
+ // one.
+ RootedValue exn(cx);
+ bool success = cx->getPendingException(&exn);
+ cx->clearPendingException();
+ if (success) {
+ js::ReportExceptionClosure reportExn(exn);
+ PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
+ }
+ }
+ }
}
+ } else {
+ RootedObject job(cx);
+ JS::HandleValueArray args(JS::HandleValueArray::empty());
+ RootedValue rval(cx);
+ // Execute jobs in a loop until we've reached the end of the queue.
+ while (!queue.empty()) {
+ // A previous job might have set this flag. E.g., the js shell
+ // sets it if the `quit` builtin function is called.
+ if (interrupted_) {
+ break;
+ }
- AutoRealm ar(cx, &job->as<JSFunction>());
- {
- if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval)) {
- // Nothing we can do about uncatchable exceptions.
- if (!cx->isExceptionPending()) {
- continue;
- }
+ cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
+
+ job = queue.front();
+ queue.popFront();
+
+ // If the next job is the last job in the job queue, allow
+ // skipping the standard job queuing behavior.
+ if (queue.empty()) {
+ JS::JobQueueIsEmpty(cx);
+ }
- // Always clear the exception, because
- // PrepareScriptEnvironmentAndInvoke will assert that we don't have
- // one.
- RootedValue exn(cx);
- bool success = cx->getPendingException(&exn);
- cx->clearPendingException();
- if (success) {
- js::ReportExceptionClosure reportExn(exn);
- PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
+ AutoRealm ar(cx, &job->as<JSFunction>());
+ {
+ if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval)) {
+ // Nothing we can do about uncatchable exceptions.
+ if (!cx->isExceptionPending()) {
+ continue;
+ }
+
+ // Always clear the exception, because
+ // PrepareScriptEnvironmentAndInvoke will assert that we don't have
+ // one.
+ RootedValue exn(cx);
+ bool success = cx->getPendingException(&exn);
+ cx->clearPendingException();
+ if (success) {
+ js::ReportExceptionClosure reportExn(exn);
+ PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
+ }
}
}
}
@@ -900,7 +984,12 @@ void InternalJobQueue::runJobs(JSContext* cx) {
break;
}
- queue.clear();
+ if (JS::Prefs::use_js_microtask_queue()) {
+ // MG:XXX: Should use public API here.
+ cx->microTaskQueues->clear();
+ } else {
+ queue.clear();
+ }
// It's possible a job added a new off-thread promise task.
if (!cx->runtime()->offThreadPromiseState.ref().internalHasPending()) {
@@ -921,27 +1010,38 @@ JSObject* InternalJobQueue::maybeFront() const {
class js::InternalJobQueue::SavedQueue : public JobQueue::SavedJobQueue {
public:
- SavedQueue(JSContext* cx, Queue&& saved, bool draining)
- : cx(cx), saved(cx, std::move(saved)), draining_(draining) {
+ SavedQueue(JSContext* cx, Queue&& saved, MicroTaskQueueSet&& queueSet,
+ bool draining)
+ : cx(cx),
+ saved(cx, std::move(saved)),
+ savedQueues(cx, std::move(queueSet)),
+ draining_(draining) {
MOZ_ASSERT(cx->internalJobQueue.ref());
+ if (JS::Prefs::use_js_microtask_queue()) {
+ MOZ_ASSERT(saved.empty());
+ } else {
+ MOZ_ASSERT(queueSet.empty());
+ }
}
~SavedQueue() {
MOZ_ASSERT(cx->internalJobQueue.ref());
cx->internalJobQueue->queue = std::move(saved.get());
cx->internalJobQueue->draining_ = draining_;
+ *cx->microTaskQueues.get() = std::move(savedQueues.get());
}
private:
JSContext* cx;
PersistentRooted<Queue> saved;
+ PersistentRooted<MicroTaskQueueSet> savedQueues;
bool draining_;
};
js::UniquePtr<JS::JobQueue::SavedJobQueue> InternalJobQueue::saveJobQueue(
JSContext* cx) {
- auto saved =
- js::MakeUnique<SavedQueue>(cx, std::move(queue.get()), draining_);
+ auto saved = js::MakeUnique<SavedQueue>(
+ cx, std::move(queue.get()), std::move(*cx->microTaskQueues), draining_);
if (!saved) {
// When MakeUnique's allocation fails, the SavedQueue constructor is never
// called, so this->queue is still initialized. (The move doesn't occur
@@ -955,6 +1055,135 @@ js::UniquePtr<JS::JobQueue::SavedJobQueue> InternalJobQueue::saveJobQueue(
return saved;
}
+JS::MicroTask js::MicroTaskQueueSet::popDebugFront() {
+ JS_LOG(mtq, Info, "JS Drain Queue: popDebugFront");
+ if (!debugMicroTaskQueue.empty()) {
+ JS::Value p = debugMicroTaskQueue.front();
+ debugMicroTaskQueue.popFront();
+ return p;
+ }
+ return JS::NullValue();
+}
+
+JS::MicroTask js::MicroTaskQueueSet::popFront() {
+ JS_LOG(mtq, Info, "JS Drain Queue");
+ if (!debugMicroTaskQueue.empty()) {
+ JS::Value p = debugMicroTaskQueue.front();
+ debugMicroTaskQueue.popFront();
+ return p;
+ }
+ if (!microTaskQueue.empty()) {
+ JS::Value p = microTaskQueue.front();
+ microTaskQueue.popFront();
+ return p;
+ }
+
+ return JS::NullValue();
+}
+
+bool js::MicroTaskQueueSet::enqueueRegularMicroTask(
+ JSContext* cx, const JS::MicroTask& entry) {
+ JS_LOG(mtq, Verbose, "JS: Enqueue Regular MT");
+ JS::JobQueueMayNotBeEmpty(cx);
+ return microTaskQueue.pushBack(entry);
+}
+
+bool js::MicroTaskQueueSet::prependRegularMicroTask(
+ JSContext* cx, const JS::MicroTask& entry) {
+ JS_LOG(mtq, Verbose, "JS: Prepend Regular MT");
+ JS::JobQueueMayNotBeEmpty(cx);
+ return microTaskQueue.emplaceFront(entry);
+}
+
+bool js::MicroTaskQueueSet::enqueueDebugMicroTask(JSContext* cx,
+ const JS::MicroTask& entry) {
+ JS_LOG(mtq, Verbose, "JS: Enqueue Debug MT");
+ return debugMicroTaskQueue.pushBack(entry);
+}
+
+JS_PUBLIC_API bool JS::EnqueueMicroTask(JSContext* cx, const MicroTask& entry) {
+ JS_LOG(mtq, Info, "Enqueue of non JS MT");
+
+ return cx->microTaskQueues->enqueueRegularMicroTask(cx, entry);
+}
+
+JS_PUBLIC_API bool JS::EnqueueDebugMicroTask(JSContext* cx,
+ const MicroTask& entry) {
+ JS_LOG(mtq, Info, "Enqueue of non JS MT");
+
+ return cx->microTaskQueues->enqueueDebugMicroTask(cx, entry);
+}
+
+JS_PUBLIC_API bool JS::PrependMicroTask(JSContext* cx, const MicroTask& entry) {
+ JS_LOG(mtq, Info, "Prepend job to MTQ");
+
+ return cx->microTaskQueues->prependRegularMicroTask(cx, entry);
+}
+
+JS_PUBLIC_API JS::MicroTask JS::DequeueNextMicroTask(JSContext* cx) {
+ return cx->microTaskQueues->popFront();
+}
+
+JS_PUBLIC_API JS::MicroTask JS::DequeueNextDebuggerMicroTask(JSContext* cx) {
+ return cx->microTaskQueues->popDebugFront();
+}
+
+JS_PUBLIC_API bool JS::HasAnyMicroTasks(JSContext* cx) {
+ return !cx->microTaskQueues->empty();
+}
+
+JS_PUBLIC_API bool JS::HasDebuggerMicroTasks(JSContext* cx) {
+ return !cx->microTaskQueues->debugMicroTaskQueue.empty();
+}
+
+// Concrete implementation of the saved queue.
+struct SavedMicroTaskQueueImpl : public JS::SavedMicroTaskQueue {
+ explicit SavedMicroTaskQueueImpl(JSContext* cx) : savedQueues(cx) {
+ savedQueues = js::MakeUnique<js::MicroTaskQueueSet>(cx);
+ std::swap(cx->microTaskQueues.get(), savedQueues.get());
+ }
+ ~SavedMicroTaskQueueImpl() override = default;
+ JS::PersistentRooted<js::UniquePtr<js::MicroTaskQueueSet>> savedQueues;
+};
+
+JS_PUBLIC_API js::UniquePtr<JS::SavedMicroTaskQueue> JS::SaveMicroTaskQueue(
+ JSContext* cx) {
+ auto saved = js::MakeUnique<SavedMicroTaskQueueImpl>(cx);
+ if (!saved) {
+ ReportOutOfMemory(cx);
+ return nullptr;
+ }
+ return saved;
+}
+
+JS_PUBLIC_API void JS::RestoreMicroTaskQueue(
+ JSContext* cx, js::UniquePtr<JS::SavedMicroTaskQueue> savedQueue) {
+ MOZ_ASSERT(cx->microTaskQueues->empty(), "Don't drop jobs on the floor");
+
+ // There's only one impl, so we know this is safe.
+ SavedMicroTaskQueueImpl* savedQueueImpl =
+ static_cast<SavedMicroTaskQueueImpl*>(savedQueue.get());
+ std::swap(savedQueueImpl->savedQueues.get(), cx->microTaskQueues.get());
+}
+
+JS_PUBLIC_API size_t JS::GetRegularMicroTaskCount(JSContext* cx) {
+ return cx->microTaskQueues->microTaskQueue.length();
+}
+
+JS_PUBLIC_API bool JS::HasRegularMicroTasks(JSContext* cx) {
+ return !cx->microTaskQueues->microTaskQueue.empty();
+}
+
+JS_PUBLIC_API JS::MicroTask JS::DequeueNextRegularMicroTask(JSContext* cx) {
+ auto& queue = cx->microTaskQueues->microTaskQueue;
+ if (!queue.empty()) {
+ auto p = std::move(queue.front());
+ queue.popFront();
+ return p;
+ }
+ return JS::NullValue();
+}
+
mozilla::GenericErrorResult<OOM> JSContext::alreadyReportedOOM() {
MOZ_ASSERT(isThrowingOutOfMemory());
return mozilla::Err(JS::OOM());
@@ -1039,7 +1268,8 @@ JSContext::JSContext(JSRuntime* runtime, const JS::ContextOptions& options)
canSkipEnqueuingJobs(this, false),
promiseRejectionTrackerCallback(this, nullptr),
promiseRejectionTrackerCallbackData(this, nullptr),
- insideExclusiveDebuggerOnEval(this, nullptr) {
+ insideExclusiveDebuggerOnEval(this, nullptr),
+ microTaskQueues(this) {
MOZ_ASSERT(static_cast<JS::RootingContext*>(this) ==
JS::RootingContext::get(this));
}
diff --git a/js/src/vm/JSContext.h b/js/src/vm/JSContext.h
@@ -25,6 +25,7 @@
#include "js/ContextOptions.h" // JS::ContextOptions
#include "js/Debug.h" // JS::CustomObjectSummaryCallback
#include "js/Exception.h"
+#include "js/friend/MicroTask.h"
#include "js/GCVector.h"
#include "js/Interrupt.h"
#include "js/Promise.h"
@@ -93,6 +94,9 @@ class InternalJobQueue : public JS::JobQueue {
bool getHostDefinedData(JSContext* cx,
JS::MutableHandle<JSObject*> data) const override;
+ bool getHostDefinedGlobal(JSContext*,
+ JS::MutableHandle<JSObject*>) const override;
+
bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
JS::HandleObject job, JS::HandleObject allocationSite,
JS::HandleObject hostDefinedData) override;
@@ -150,6 +154,48 @@ enum class InterruptReason : uint32_t {
enum class ShouldCaptureStack { Maybe, Always };
+// Use TempAllocPolicy to report OOM
+// MG:XXX: It would be nice to explore the typical depth of the queue
+// to see if we can get it all inline in the common case.
+// MG:XXX: This appears to be broken for non-zero values of inline!
+using MicroTaskQueue = js::TraceableFifo<JS::Value, 0, TempAllocPolicy>;
+
+// A pair of microtask queues; one debug and one 'regular' (non-debug).
+struct MicroTaskQueueSet {
+ explicit MicroTaskQueueSet(JSContext* cx)
+ : microTaskQueue(cx), debugMicroTaskQueue(cx) {}
+
+ // We want to swap so we need move constructors
+ MicroTaskQueueSet(MicroTaskQueueSet&&) = default;
+ MicroTaskQueueSet& operator=(MicroTaskQueueSet&&) = default;
+
+ // Don't copy.
+ MicroTaskQueueSet(const MicroTaskQueueSet&) = delete;
+ MicroTaskQueueSet& operator=(const MicroTaskQueueSet&) = delete;
+
+ bool enqueueRegularMicroTask(JSContext* cx, const JS::MicroTask&);
+ bool enqueueDebugMicroTask(JSContext* cx, const JS::MicroTask&);
+ bool prependRegularMicroTask(JSContext* cx, const JS::MicroTask&);
+
+ JS::MicroTask popFront();
+ JS::MicroTask popDebugFront();
+
+ bool empty() { return microTaskQueue.empty() && debugMicroTaskQueue.empty(); }
+
+ void trace(JSTracer* trc) {
+ microTaskQueue.trace(trc);
+ debugMicroTaskQueue.trace(trc);
+ }
+
+ void clear() {
+ microTaskQueue.clear();
+ debugMicroTaskQueue.clear();
+ }
+
+ MicroTaskQueue microTaskQueue;
+ MicroTaskQueue debugMicroTaskQueue;
+};
+
} /* namespace js */
/*
@@ -1007,6 +1053,7 @@ struct JS_PUBLIC_API JSContext : public JS::RootingContext,
bool hasExecutionTracer() { return false; }
#endif
+ JS::PersistentRooted<js::UniquePtr<js::MicroTaskQueueSet>> microTaskQueues;
}; /* struct JSContext */
inline JSContext* JSRuntime::mainContextFromOwnThread() {
diff --git a/js/src/vm/Logging.h b/js/src/vm/Logging.h
@@ -93,8 +93,9 @@ class LogModule {
_(startup) /* engine startup logging */ \
_(teleporting) /* Shape Teleporting */ \
_(selfHosted) /* self-hosted script logging */ \
- JITSPEW_CHANNEL_LIST(_) /* A module for each JitSpew channel. */ \
- _(gc) /* The garbage collector */
+ _(gc) /* The garbage collector */ \
+ _(mtq) /* MicroTask queue */ \
+ JITSPEW_CHANNEL_LIST(_) /* A module for each JitSpew channel. */
// Declare Log modules
#define DECLARE_MODULE(X) inline constexpr LogModule X##Module(#X);
diff --git a/js/src/vm/Runtime.cpp b/js/src/vm/Runtime.cpp
@@ -37,6 +37,7 @@
#include "js/Wrapper.h"
#include "js/WrapperCallbacks.h"
#include "vm/DateTime.h"
+#include "vm/JSFunction.h"
#include "vm/JSObject.h"
#include "vm/JSScript.h"
#include "vm/PromiseObject.h" // js::PromiseObject
diff --git a/xpcom/base/CycleCollectedJSContext.cpp b/xpcom/base/CycleCollectedJSContext.cpp
@@ -313,6 +313,17 @@ static const JSClass sHostDefinedDataClass = {
JSCLASS_FOREGROUND_FINALIZE,
&sHostDefinedData};
+bool CycleCollectedJSContext::getHostDefinedGlobal(
+ JSContext* aCx, JS::MutableHandle<JSObject*> out) const {
+ nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
+ if (!global) {
+ return true;
+ }
+
+ out.set(global->GetGlobalJSObject());
+ return true;
+}
+
bool CycleCollectedJSContext::getHostDefinedData(
JSContext* aCx, JS::MutableHandle<JSObject*> aData) const {
nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
diff --git a/xpcom/base/CycleCollectedJSContext.h b/xpcom/base/CycleCollectedJSContext.h
@@ -318,6 +318,12 @@ class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue {
bool getHostDefinedData(JSContext* cx,
JS::MutableHandle<JSObject*> aData) const override;
+ // Fills in the JS Object used to represent the current incumbent global.
+ // Used when running MicroTasks which don't have host-defined data as
+ // they will still need an incumbent global.
+ bool getHostDefinedGlobal(JSContext* cx,
+ JS::MutableHandle<JSObject*>) const override;
+
bool enqueuePromiseJob(JSContext* cx, JS::Handle<JSObject*> promise,
JS::Handle<JSObject*> job,
JS::Handle<JSObject*> allocationSite,