tor-browser

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

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:
Mjs/public/Promise.h | 10++++++++++
Ajs/public/friend/MicroTask.h | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/src/builtin/Promise.cpp | 607++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Ajs/src/jit-test/tests/js_microtask/microtask-smoke-test.js | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mjs/src/moz.build | 1+
Mjs/src/shell/js.cpp | 35++++++++++++++++++++++++++---------
Mjs/src/vm/AsyncFunction.cpp | 1+
Mjs/src/vm/JSContext.cpp | 320++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mjs/src/vm/JSContext.h | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mjs/src/vm/Logging.h | 5+++--
Mjs/src/vm/Runtime.cpp | 1+
Mxpcom/base/CycleCollectedJSContext.cpp | 11+++++++++++
Mxpcom/base/CycleCollectedJSContext.h | 6++++++
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,