/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * 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 vm_OffThreadPromiseRuntimeState_h #define vm_OffThreadPromiseRuntimeState_h #include // size_t #include "jstypes.h" // JS_PUBLIC_API #include "ds/Fifo.h" // js::Fifo #include "ds/PriorityQueue.h" // js::PriorityQueue #include "js/AllocPolicy.h" // js::SystemAllocPolicy #include "js/HashTable.h" // js::DefaultHasher, js::HashSet #include "js/Promise.h" // JS::Dispatchable, JS::Dispatchable::MaybeShuttingDown, // JS::DispatchToEventLoopCallback, // JS::DelayedDispatchToEventLoopCallback #include "js/RootingAPI.h" // JS::Handle, JS::PersistentRooted #include "threading/ConditionVariable.h" // js::ConditionVariable #include "vm/PromiseObject.h" // js::PromiseObject struct JS_PUBLIC_API JSContext; struct JS_PUBLIC_API JSRuntime; namespace js { class AutoLockHelperThreadState; class OffThreadPromiseRuntimeState; // [SMDOC] OffThreadPromiseTask: an off-main-thread task that resolves a promise // // An OffThreadPromiseTask is an abstract base class holding a JavaScript // promise that will be resolved (fulfilled or rejected) with the results of a // task possibly performed by some other thread. OffThreadPromiseTasks can be // undispatched (meaning that they can be dropped if the JSContext owning the // promise shuts down before the tasks resolves) or dispatched (meaning that // shutdown should wait for the task to join). // // An OffThreadPromiseTask's lifecycle is as follows: // // - Some JavaScript native wishes to return a promise of the result of some // computation that might be performed by other threads (say, helper threads // or the embedding's I/O threads), so it creates a PromiseObject to represent // the result, and an OffThreadPromiseTask referring to it. After handing the // OffThreadPromiseTask to the code doing the actual work, the native is free // to return the PromiseObject to its caller. // // - When the computation is done, successfully or otherwise, it populates the // OffThreadPromiseTask—which is actually an instance of some concrete // subclass specific to the task—with the information needed to resolve the // promise, and calls OffThreadPromiseTask::dispatchResolveAndDestroy. This // enqueues a runnable on the JavaScript thread to which the promise belongs. // // - When it gets around to the runnable, the JavaScript thread calls the // OffThreadPromiseTask's `resolve` method, which the concrete subclass has // overriden to resolve the promise appropriately. This probably enqueues a // promise reaction job. // // - The JavaScript thread then deletes the OffThreadPromiseTask. // // During shutdown, the process is slightly different. Enqueuing runnables to // the JavaScript thread begins to fail. Undispatched tasks are immediately // destroyed. JSRuntime shutdown waits for all outstanding dispatched tasks // to call their run methods, and then deletes them on the main thread, // without calling `resolve`. // // For example, the JavaScript function WebAssembly.compile uses // OffThreadPromiseTask to manage the result of a helper thread task, accepting // binary WebAssembly code and returning a promise of a compiled // WebAssembly.Module. It would like to do this compilation work on a helper // thread. When called by JavaScript, WebAssembly.compile creates a promise, // builds a CompileBufferTask (the OffThreadPromiseTask concrete subclass) to // keep track of it, and then hands that to a helper thread. When the helper // thread is done, successfully or otherwise, it calls the CompileBufferTask's // dispatchResolveAndDestroy method, which enqueues a runnable to the JavaScript // thread to resolve the promise and delete the CompileBufferTask. // (CompileBufferTask actually implements PromiseHelperTask, which implements // OffThreadPromiseTask; PromiseHelperTask is what our helper thread scheduler // requires.) // // OffThreadPromiseTasks are not limited to use with helper threads. For // example, a function returning a promise of the result of a network operation // could provide the code collecting the incoming data with an // OffThreadPromiseTask for the promise, and let the embedding's network I/O // threads call dispatchResolveAndDestroy. // // OffThreadPromiseTask may also be used purely on the main thread, as a way to // "queue a task" in HTML terms. Note that a "task" is not the same as a // "microtask" and there are separate queues for tasks and microtasks that are // drained at separate times in the browser. The task queue is implemented by // the browser's main event loop. The microtask queue is implemented // by JS::JobQueue, used for promises and gets drained before returning to // the event loop. Thus OffThreadPromiseTask can only be used when the spec // says "queue a task", as the WebAssembly APIs do. In some cases, like // Atomics.waitAsync, the choice between queuing a task or a microtask depends // on whether the promise is being resolved from the owning thread or another // thread. In such cases, ExtractAndForget can be used from the owning thread to // cancel the task and return the underlying promise, which can then be resolved // the normal way. // // An OffThreadPromiseTask has a JSContext, and must be constructed and have its // 'init' method called on that JSContext's thread. Once // initialized, its dispatchResolveAndDestroy method may be called from any // thread. Other than calling `ExtractAndForget`, or `DestroyUndispatchedTask` // during shutdown, this is the only safe way to destruct an // OffThreadPromiseTask; doing so ensures the OffThreadPromiseTask's destructor // will run on the JSContext's thread, either from the event loop or during // shutdown. // // OffThreadPromiseTask::dispatchResolveAndDestroy uses the // JS::DispatchToEventLoopCallback provided by the embedding to enqueue // runnables on the JavaScript thread. See the comments for // DispatchToEventLoopCallback for details. class OffThreadPromiseTask : public JS::Dispatchable { friend class OffThreadPromiseRuntimeState; JSRuntime* runtime_; JS::PersistentRooted promise_; // Indicates that this is an undispatched cancellable task, which is a member // of the Cancellable list. If cancellable is set to false, we can no longer // terminate the task early, this means it is no longer tracked by the // Cancellable list, and a dispatch has been attempted. bool cancellable_; void operator=(const OffThreadPromiseTask&) = delete; OffThreadPromiseTask(const OffThreadPromiseTask&) = delete; void unregister(OffThreadPromiseRuntimeState& state); // Used when we want to reuse a lock for unregistration and deletion. void unregister(OffThreadPromiseRuntimeState& state, const AutoLockHelperThreadState& lock); protected: OffThreadPromiseTask(JSContext* cx, JS::Handle promise); // To be called by OffThreadPromiseTask and implemented by the derived class. virtual bool resolve(JSContext* cx, JS::Handle promise) { MOZ_CRASH("Tasks should override resolve"); }; // JS::Dispatchable override implementation. // Runs the task, and ends with 'js_delete(this)'. void run(JSContext* cx, MaybeShuttingDown maybeShuttingDown) final; // JS::Dispatchable override implementation, moves ownership to // OffThreadPromiseRuntimeState's failed_ list, for cleanup on shutdown. void transferToRuntime() final; // To be called by `destroy` during shutdown and implemented by the derived // class (for undispatched tasks only). Gives the task a chance to clean up // before being deleted. virtual void prepareForCancel() { MOZ_CRASH("Undispatched tasks should override prepareForCancel"); } public: ~OffThreadPromiseTask() override; static void DestroyUndispatchedTask(OffThreadPromiseTask* task, OffThreadPromiseRuntimeState& state, const AutoLockHelperThreadState& lock); JSRuntime* runtime() { return runtime_; } // Calling `init` on an OffThreadPromiseTask informs the runtime that it must // wait on shutdown for this task to rejoin the active JSContext by calling // dispatchResolveAndDestroy(). bool init(JSContext* cx); bool init(JSContext* cx, const AutoLockHelperThreadState& lock); // Cancellable initialization track the task in case it needs to be aborted // before it is dispatched. Cancellable tasks are owned by the cancellable_ // hashMap until they are dispatched. static bool InitCancellable(JSContext* cx, js::UniquePtr&& task); static bool InitCancellable(JSContext* cx, const AutoLockHelperThreadState& lock, js::UniquePtr&& task); // Remove the cancellable task from the runtime cancellable list and // call DispatchResolveAndDestroy with a newly created UniquePtr, // so that ownership moves to the embedding. // // If a task is never removed from the cancellable list, it is deleted on // shutdown without running. void removeFromCancellableListAndDispatch(); void removeFromCancellableListAndDispatch( const AutoLockHelperThreadState& lock); // These first two methods will wrap a pointer in a uniquePtr for the purpose // of passing it's ownership eventually to the embedding. These are used by // WASM and PromiseHelperTask. void dispatchResolveAndDestroy(); void dispatchResolveAndDestroy(const AutoLockHelperThreadState& lock); // An initialized OffThreadPromiseTask can be dispatched to an active // JSContext of its Promise's JSRuntime from any thread. Normally, this will // lead to resolve() being called on JSContext thread, given the Promise. // However, if shutdown interrupts, resolve() may not be called, though the // OffThreadPromiseTask will be destroyed on a JSContext thread. static void DispatchResolveAndDestroy( js::UniquePtr&& task); static void DispatchResolveAndDestroy( js::UniquePtr&& task, const AutoLockHelperThreadState& lock); static PromiseObject* ExtractAndForget(OffThreadPromiseTask* task, const AutoLockHelperThreadState& lock); }; using OffThreadPromiseTaskSet = HashSet, SystemAllocPolicy>; using DispatchableFifo = Fifo, 0, SystemAllocPolicy>; class DelayedDispatchable { js::UniquePtr dispatchable_; mozilla::TimeStamp endTime_; public: DelayedDispatchable(DelayedDispatchable&& other) : dispatchable_(other.dispatchable()), endTime_(other.endTime()) {} DelayedDispatchable(js::UniquePtr&& dispatchable, mozilla::TimeStamp endTime) : dispatchable_(std::move(dispatchable)), endTime_(endTime) {} void operator=(DelayedDispatchable&& other) { dispatchable_ = other.dispatchable(); endTime_ = other.endTime(); } js::UniquePtr dispatchable() { return std::move(dispatchable_); } mozilla::TimeStamp endTime() const { return endTime_; } static bool higherPriority(const DelayedDispatchable& a, const DelayedDispatchable& b) { return a.endTime_ < b.endTime_; } }; using DelayedDispatchablePriorityQueue = PriorityQueue; class OffThreadPromiseRuntimeState { friend class OffThreadPromiseTask; // These fields are initialized once before any off-thread usage and thus do // not require a lock. JS::DispatchToEventLoopCallback dispatchToEventLoopCallback_; JS::DelayedDispatchToEventLoopCallback delayedDispatchToEventLoopCallback_; JS::AsyncTaskStartedCallback asyncTaskStartedCallback_; JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback_; void* dispatchToEventLoopClosure_; #ifdef DEBUG // Set to true when the JS shell is force-quitting. // In this case the tasks won't be drained and the destructor cannot // assert anything. HelperThreadLockData forceQuitting_; #endif // A set of all OffThreadPromiseTasks that have successfully called 'init'. // This set doesn't own tasks. OffThreadPromiseTask's destructor decrements // this counter. HelperThreadLockData numRegistered_; // Currently, we have a subset of tasks which are not registered with // OffThreadPromiseRuntimeState (as they do not contain a promise), but // still depend on the internalDispatchQueue. These are JS::Dispatchables // that do not inherit from OffThreadPromiseTask, namely // WaitAsyncTimeoutTask. As a result, our assertions are broken. In order // to handle these non-registered tasks, we keep a separate counter for // dispatchables that are passed from the delayed dispatch queue to the // internal dispatch queue. // // This is a temporary fix that allows us to track these tasks. // TODO: remove this once we clean up this behavior. HelperThreadLockData numDelayed_; // The cancellable hashmap tracks the registered, but thusfar // undispatched tasks. Not all undispatched tasks are cancellable, namely // webassembly helpers are held in an undispatched state, but are not // cancellable. Until the task is dispatched, this hashmap acts as the owner // of the task. In order to place something in the cancellable list, use // InitCancellable. Any task owned by the cancellable list cannot be // dispatched using DispatchResolveAndDestroy. Instead, use the method on // OffThreadPromiseTask removeFromCancellableAndDispatch. HelperThreadLockData cancellable_; // This list owns tasks that have failed to dispatch or failed to execute. // The list is cleared on shutdown. HelperThreadLockData failed_; // The allFailed_ condition is waited on and notified during engine // shutdown, communicating when all off-thread tasks in failed_ are safe to be // destroyed from the (shutting down) main thread. This condition is met when // numRegistered_ == failed().count(), where the collection of failed tasks // mean "the DispatchToEventLoopCallback failed after this task was dispatched // for execution". HelperThreadLockData allFailed_; // The queue of JS::Dispatchables used by the DispatchToEventLoopCallback that // calling js::UseInternalJobQueues installs. HelperThreadLockData internalDispatchQueue_; HelperThreadLockData internalDispatchQueueAppended_; HelperThreadLockData internalDispatchQueueClosed_; HelperThreadLockData internalDelayedDispatchPriorityQueue_; ConditionVariable& allFailed() { return allFailed_.ref(); } DispatchableFifo& failed() { return failed_.ref(); } OffThreadPromiseTaskSet& cancellable() { return cancellable_.ref(); } DispatchableFifo& internalDispatchQueue() { return internalDispatchQueue_.ref(); } ConditionVariable& internalDispatchQueueAppended() { return internalDispatchQueueAppended_.ref(); } DelayedDispatchablePriorityQueue& internalDelayedDispatchPriorityQueue() { return internalDelayedDispatchPriorityQueue_.ref(); } void dispatchDelayedTasks(); static bool internalDispatchToEventLoop(void*, js::UniquePtr&&); static bool internalDelayedDispatchToEventLoop( void*, js::UniquePtr&&, uint32_t); bool usingInternalDispatchQueue() const; void operator=(const OffThreadPromiseRuntimeState&) = delete; OffThreadPromiseRuntimeState(const OffThreadPromiseRuntimeState&) = delete; // Used by OffThreadPromiseTask void registerTask(JSContext* cx, OffThreadPromiseTask* task); void unregisterTask(OffThreadPromiseTask* task); public: OffThreadPromiseRuntimeState(); ~OffThreadPromiseRuntimeState(); void init(JS::DispatchToEventLoopCallback dispatchCallback, JS::DelayedDispatchToEventLoopCallback delayedDispatchCallback, JS::AsyncTaskStartedCallback asyncTaskStartedCallback, JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback, void* closure); void initInternalDispatchQueue(); bool initialized() const; // If initInternalDispatchQueue() was called, internalDrain() can be // called to periodically drain the dispatch queue before shutdown. void internalDrain(JSContext* cx); bool internalHasPending(); bool internalHasPending(AutoLockHelperThreadState& lock); void stealFailedTask(JS::Dispatchable* dispatchable); bool dispatchToEventLoop(js::UniquePtr&& dispatchable); bool delayedDispatchToEventLoop( js::UniquePtr&& dispatchable, uint32_t delay); void cancelTasks(JSContext* cx); void cancelTasks(AutoLockHelperThreadState& lock, JSContext* cx); // shutdown() must be called by the JSRuntime while the JSRuntime is valid. void shutdown(JSContext* cx); #ifdef DEBUG void setForceQuitting() { forceQuitting_ = true; } #endif }; } // namespace js #endif // vm_OffThreadPromiseRuntimeState_h