OffThreadPromiseRuntimeState.h (17706B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #ifndef vm_OffThreadPromiseRuntimeState_h 8 #define vm_OffThreadPromiseRuntimeState_h 9 10 #include <stddef.h> // size_t 11 12 #include "jstypes.h" // JS_PUBLIC_API 13 14 #include "ds/Fifo.h" // js::Fifo 15 #include "ds/PriorityQueue.h" // js::PriorityQueue 16 #include "js/AllocPolicy.h" // js::SystemAllocPolicy 17 #include "js/HashTable.h" // js::DefaultHasher, js::HashSet 18 #include "js/Promise.h" // JS::Dispatchable, JS::Dispatchable::MaybeShuttingDown, 19 // JS::DispatchToEventLoopCallback, 20 // JS::DelayedDispatchToEventLoopCallback 21 #include "js/RootingAPI.h" // JS::Handle, JS::PersistentRooted 22 #include "threading/ConditionVariable.h" // js::ConditionVariable 23 #include "vm/PromiseObject.h" // js::PromiseObject 24 25 struct JS_PUBLIC_API JSContext; 26 struct JS_PUBLIC_API JSRuntime; 27 28 namespace js { 29 30 class AutoLockHelperThreadState; 31 class OffThreadPromiseRuntimeState; 32 33 // [SMDOC] OffThreadPromiseTask: an off-main-thread task that resolves a promise 34 // 35 // An OffThreadPromiseTask is an abstract base class holding a JavaScript 36 // promise that will be resolved (fulfilled or rejected) with the results of a 37 // task possibly performed by some other thread. OffThreadPromiseTasks can be 38 // undispatched (meaning that they can be dropped if the JSContext owning the 39 // promise shuts down before the tasks resolves) or dispatched (meaning that 40 // shutdown should wait for the task to join). 41 // 42 // An OffThreadPromiseTask's lifecycle is as follows: 43 // 44 // - Some JavaScript native wishes to return a promise of the result of some 45 // computation that might be performed by other threads (say, helper threads 46 // or the embedding's I/O threads), so it creates a PromiseObject to represent 47 // the result, and an OffThreadPromiseTask referring to it. After handing the 48 // OffThreadPromiseTask to the code doing the actual work, the native is free 49 // to return the PromiseObject to its caller. 50 // 51 // - When the computation is done, successfully or otherwise, it populates the 52 // OffThreadPromiseTask—which is actually an instance of some concrete 53 // subclass specific to the task—with the information needed to resolve the 54 // promise, and calls OffThreadPromiseTask::dispatchResolveAndDestroy. This 55 // enqueues a runnable on the JavaScript thread to which the promise belongs. 56 // 57 // - When it gets around to the runnable, the JavaScript thread calls the 58 // OffThreadPromiseTask's `resolve` method, which the concrete subclass has 59 // overriden to resolve the promise appropriately. This probably enqueues a 60 // promise reaction job. 61 // 62 // - The JavaScript thread then deletes the OffThreadPromiseTask. 63 // 64 // During shutdown, the process is slightly different. Enqueuing runnables to 65 // the JavaScript thread begins to fail. Undispatched tasks are immediately 66 // destroyed. JSRuntime shutdown waits for all outstanding dispatched tasks 67 // to call their run methods, and then deletes them on the main thread, 68 // without calling `resolve`. 69 // 70 // For example, the JavaScript function WebAssembly.compile uses 71 // OffThreadPromiseTask to manage the result of a helper thread task, accepting 72 // binary WebAssembly code and returning a promise of a compiled 73 // WebAssembly.Module. It would like to do this compilation work on a helper 74 // thread. When called by JavaScript, WebAssembly.compile creates a promise, 75 // builds a CompileBufferTask (the OffThreadPromiseTask concrete subclass) to 76 // keep track of it, and then hands that to a helper thread. When the helper 77 // thread is done, successfully or otherwise, it calls the CompileBufferTask's 78 // dispatchResolveAndDestroy method, which enqueues a runnable to the JavaScript 79 // thread to resolve the promise and delete the CompileBufferTask. 80 // (CompileBufferTask actually implements PromiseHelperTask, which implements 81 // OffThreadPromiseTask; PromiseHelperTask is what our helper thread scheduler 82 // requires.) 83 // 84 // OffThreadPromiseTasks are not limited to use with helper threads. For 85 // example, a function returning a promise of the result of a network operation 86 // could provide the code collecting the incoming data with an 87 // OffThreadPromiseTask for the promise, and let the embedding's network I/O 88 // threads call dispatchResolveAndDestroy. 89 // 90 // OffThreadPromiseTask may also be used purely on the main thread, as a way to 91 // "queue a task" in HTML terms. Note that a "task" is not the same as a 92 // "microtask" and there are separate queues for tasks and microtasks that are 93 // drained at separate times in the browser. The task queue is implemented by 94 // the browser's main event loop. The microtask queue is implemented 95 // by JS::JobQueue, used for promises and gets drained before returning to 96 // the event loop. Thus OffThreadPromiseTask can only be used when the spec 97 // says "queue a task", as the WebAssembly APIs do. In some cases, like 98 // Atomics.waitAsync, the choice between queuing a task or a microtask depends 99 // on whether the promise is being resolved from the owning thread or another 100 // thread. In such cases, ExtractAndForget can be used from the owning thread to 101 // cancel the task and return the underlying promise, which can then be resolved 102 // the normal way. 103 // 104 // An OffThreadPromiseTask has a JSContext, and must be constructed and have its 105 // 'init' method called on that JSContext's thread. Once 106 // initialized, its dispatchResolveAndDestroy method may be called from any 107 // thread. Other than calling `ExtractAndForget`, or `DestroyUndispatchedTask` 108 // during shutdown, this is the only safe way to destruct an 109 // OffThreadPromiseTask; doing so ensures the OffThreadPromiseTask's destructor 110 // will run on the JSContext's thread, either from the event loop or during 111 // shutdown. 112 // 113 // OffThreadPromiseTask::dispatchResolveAndDestroy uses the 114 // JS::DispatchToEventLoopCallback provided by the embedding to enqueue 115 // runnables on the JavaScript thread. See the comments for 116 // DispatchToEventLoopCallback for details. 117 118 class OffThreadPromiseTask : public JS::Dispatchable { 119 friend class OffThreadPromiseRuntimeState; 120 121 JSRuntime* runtime_; 122 JS::PersistentRooted<PromiseObject*> promise_; 123 124 // Indicates that this is an undispatched cancellable task, which is a member 125 // of the Cancellable list. If cancellable is set to false, we can no longer 126 // terminate the task early, this means it is no longer tracked by the 127 // Cancellable list, and a dispatch has been attempted. 128 bool cancellable_; 129 130 void operator=(const OffThreadPromiseTask&) = delete; 131 OffThreadPromiseTask(const OffThreadPromiseTask&) = delete; 132 133 void unregister(OffThreadPromiseRuntimeState& state); 134 // Used when we want to reuse a lock for unregistration and deletion. 135 void unregister(OffThreadPromiseRuntimeState& state, 136 const AutoLockHelperThreadState& lock); 137 138 protected: 139 OffThreadPromiseTask(JSContext* cx, JS::Handle<PromiseObject*> promise); 140 141 // To be called by OffThreadPromiseTask and implemented by the derived class. 142 virtual bool resolve(JSContext* cx, JS::Handle<PromiseObject*> promise) { 143 MOZ_CRASH("Tasks should override resolve"); 144 }; 145 146 // JS::Dispatchable override implementation. 147 // Runs the task, and ends with 'js_delete(this)'. 148 void run(JSContext* cx, MaybeShuttingDown maybeShuttingDown) final; 149 150 // JS::Dispatchable override implementation, moves ownership to 151 // OffThreadPromiseRuntimeState's failed_ list, for cleanup on shutdown. 152 void transferToRuntime() final; 153 154 // To be called by `destroy` during shutdown and implemented by the derived 155 // class (for undispatched tasks only). Gives the task a chance to clean up 156 // before being deleted. 157 virtual void prepareForCancel() { 158 MOZ_CRASH("Undispatched tasks should override prepareForCancel"); 159 } 160 161 public: 162 ~OffThreadPromiseTask() override; 163 static void DestroyUndispatchedTask(OffThreadPromiseTask* task, 164 OffThreadPromiseRuntimeState& state, 165 const AutoLockHelperThreadState& lock); 166 167 JSRuntime* runtime() { return runtime_; } 168 169 // Calling `init` on an OffThreadPromiseTask informs the runtime that it must 170 // wait on shutdown for this task to rejoin the active JSContext by calling 171 // dispatchResolveAndDestroy(). 172 bool init(JSContext* cx); 173 bool init(JSContext* cx, const AutoLockHelperThreadState& lock); 174 175 // Cancellable initialization track the task in case it needs to be aborted 176 // before it is dispatched. Cancellable tasks are owned by the cancellable_ 177 // hashMap until they are dispatched. 178 static bool InitCancellable(JSContext* cx, 179 js::UniquePtr<OffThreadPromiseTask>&& task); 180 static bool InitCancellable(JSContext* cx, 181 const AutoLockHelperThreadState& lock, 182 js::UniquePtr<OffThreadPromiseTask>&& task); 183 184 // Remove the cancellable task from the runtime cancellable list and 185 // call DispatchResolveAndDestroy with a newly created UniquePtr, 186 // so that ownership moves to the embedding. 187 // 188 // If a task is never removed from the cancellable list, it is deleted on 189 // shutdown without running. 190 void removeFromCancellableListAndDispatch(); 191 void removeFromCancellableListAndDispatch( 192 const AutoLockHelperThreadState& lock); 193 194 // These first two methods will wrap a pointer in a uniquePtr for the purpose 195 // of passing it's ownership eventually to the embedding. These are used by 196 // WASM and PromiseHelperTask. 197 void dispatchResolveAndDestroy(); 198 void dispatchResolveAndDestroy(const AutoLockHelperThreadState& lock); 199 200 // An initialized OffThreadPromiseTask can be dispatched to an active 201 // JSContext of its Promise's JSRuntime from any thread. Normally, this will 202 // lead to resolve() being called on JSContext thread, given the Promise. 203 // However, if shutdown interrupts, resolve() may not be called, though the 204 // OffThreadPromiseTask will be destroyed on a JSContext thread. 205 static void DispatchResolveAndDestroy( 206 js::UniquePtr<OffThreadPromiseTask>&& task); 207 static void DispatchResolveAndDestroy( 208 js::UniquePtr<OffThreadPromiseTask>&& task, 209 const AutoLockHelperThreadState& lock); 210 211 static PromiseObject* ExtractAndForget(OffThreadPromiseTask* task, 212 const AutoLockHelperThreadState& lock); 213 }; 214 215 using OffThreadPromiseTaskSet = 216 HashSet<OffThreadPromiseTask*, DefaultHasher<OffThreadPromiseTask*>, 217 SystemAllocPolicy>; 218 219 using DispatchableFifo = 220 Fifo<js::UniquePtr<JS::Dispatchable>, 0, SystemAllocPolicy>; 221 222 class DelayedDispatchable { 223 js::UniquePtr<JS::Dispatchable> dispatchable_; 224 mozilla::TimeStamp endTime_; 225 226 public: 227 DelayedDispatchable(DelayedDispatchable&& other) 228 : dispatchable_(other.dispatchable()), endTime_(other.endTime()) {} 229 230 DelayedDispatchable(js::UniquePtr<JS::Dispatchable>&& dispatchable, 231 mozilla::TimeStamp endTime) 232 : dispatchable_(std::move(dispatchable)), endTime_(endTime) {} 233 234 void operator=(DelayedDispatchable&& other) { 235 dispatchable_ = other.dispatchable(); 236 endTime_ = other.endTime(); 237 } 238 js::UniquePtr<JS::Dispatchable> dispatchable() { 239 return std::move(dispatchable_); 240 } 241 mozilla::TimeStamp endTime() const { return endTime_; } 242 243 static bool higherPriority(const DelayedDispatchable& a, 244 const DelayedDispatchable& b) { 245 return a.endTime_ < b.endTime_; 246 } 247 }; 248 249 using DelayedDispatchablePriorityQueue = 250 PriorityQueue<DelayedDispatchable, DelayedDispatchable, 0, 251 SystemAllocPolicy>; 252 253 class OffThreadPromiseRuntimeState { 254 friend class OffThreadPromiseTask; 255 256 // These fields are initialized once before any off-thread usage and thus do 257 // not require a lock. 258 JS::DispatchToEventLoopCallback dispatchToEventLoopCallback_; 259 JS::DelayedDispatchToEventLoopCallback delayedDispatchToEventLoopCallback_; 260 JS::AsyncTaskStartedCallback asyncTaskStartedCallback_; 261 JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback_; 262 void* dispatchToEventLoopClosure_; 263 264 #ifdef DEBUG 265 // Set to true when the JS shell is force-quitting. 266 // In this case the tasks won't be drained and the destructor cannot 267 // assert anything. 268 HelperThreadLockData<bool> forceQuitting_; 269 #endif 270 271 // A set of all OffThreadPromiseTasks that have successfully called 'init'. 272 // This set doesn't own tasks. OffThreadPromiseTask's destructor decrements 273 // this counter. 274 HelperThreadLockData<size_t> numRegistered_; 275 276 // Currently, we have a subset of tasks which are not registered with 277 // OffThreadPromiseRuntimeState (as they do not contain a promise), but 278 // still depend on the internalDispatchQueue. These are JS::Dispatchables 279 // that do not inherit from OffThreadPromiseTask, namely 280 // WaitAsyncTimeoutTask. As a result, our assertions are broken. In order 281 // to handle these non-registered tasks, we keep a separate counter for 282 // dispatchables that are passed from the delayed dispatch queue to the 283 // internal dispatch queue. 284 // 285 // This is a temporary fix that allows us to track these tasks. 286 // TODO: remove this once we clean up this behavior. 287 HelperThreadLockData<size_t> numDelayed_; 288 289 // The cancellable hashmap tracks the registered, but thusfar 290 // undispatched tasks. Not all undispatched tasks are cancellable, namely 291 // webassembly helpers are held in an undispatched state, but are not 292 // cancellable. Until the task is dispatched, this hashmap acts as the owner 293 // of the task. In order to place something in the cancellable list, use 294 // InitCancellable. Any task owned by the cancellable list cannot be 295 // dispatched using DispatchResolveAndDestroy. Instead, use the method on 296 // OffThreadPromiseTask removeFromCancellableAndDispatch. 297 HelperThreadLockData<OffThreadPromiseTaskSet> cancellable_; 298 299 // This list owns tasks that have failed to dispatch or failed to execute. 300 // The list is cleared on shutdown. 301 HelperThreadLockData<DispatchableFifo> failed_; 302 303 // The allFailed_ condition is waited on and notified during engine 304 // shutdown, communicating when all off-thread tasks in failed_ are safe to be 305 // destroyed from the (shutting down) main thread. This condition is met when 306 // numRegistered_ == failed().count(), where the collection of failed tasks 307 // mean "the DispatchToEventLoopCallback failed after this task was dispatched 308 // for execution". 309 HelperThreadLockData<ConditionVariable> allFailed_; 310 311 // The queue of JS::Dispatchables used by the DispatchToEventLoopCallback that 312 // calling js::UseInternalJobQueues installs. 313 HelperThreadLockData<DispatchableFifo> internalDispatchQueue_; 314 HelperThreadLockData<ConditionVariable> internalDispatchQueueAppended_; 315 HelperThreadLockData<bool> internalDispatchQueueClosed_; 316 HelperThreadLockData<DelayedDispatchablePriorityQueue> 317 internalDelayedDispatchPriorityQueue_; 318 319 ConditionVariable& allFailed() { return allFailed_.ref(); } 320 321 DispatchableFifo& failed() { return failed_.ref(); } 322 OffThreadPromiseTaskSet& cancellable() { return cancellable_.ref(); } 323 324 DispatchableFifo& internalDispatchQueue() { 325 return internalDispatchQueue_.ref(); 326 } 327 ConditionVariable& internalDispatchQueueAppended() { 328 return internalDispatchQueueAppended_.ref(); 329 } 330 DelayedDispatchablePriorityQueue& internalDelayedDispatchPriorityQueue() { 331 return internalDelayedDispatchPriorityQueue_.ref(); 332 } 333 334 void dispatchDelayedTasks(); 335 336 static bool internalDispatchToEventLoop(void*, 337 js::UniquePtr<JS::Dispatchable>&&); 338 static bool internalDelayedDispatchToEventLoop( 339 void*, js::UniquePtr<JS::Dispatchable>&&, uint32_t); 340 bool usingInternalDispatchQueue() const; 341 342 void operator=(const OffThreadPromiseRuntimeState&) = delete; 343 OffThreadPromiseRuntimeState(const OffThreadPromiseRuntimeState&) = delete; 344 345 // Used by OffThreadPromiseTask 346 void registerTask(JSContext* cx, OffThreadPromiseTask* task); 347 void unregisterTask(OffThreadPromiseTask* task); 348 349 public: 350 OffThreadPromiseRuntimeState(); 351 ~OffThreadPromiseRuntimeState(); 352 void init(JS::DispatchToEventLoopCallback dispatchCallback, 353 JS::DelayedDispatchToEventLoopCallback delayedDispatchCallback, 354 JS::AsyncTaskStartedCallback asyncTaskStartedCallback, 355 JS::AsyncTaskFinishedCallback asyncTaskFinishedCallback, 356 void* closure); 357 void initInternalDispatchQueue(); 358 bool initialized() const; 359 360 // If initInternalDispatchQueue() was called, internalDrain() can be 361 // called to periodically drain the dispatch queue before shutdown. 362 void internalDrain(JSContext* cx); 363 bool internalHasPending(); 364 bool internalHasPending(AutoLockHelperThreadState& lock); 365 366 void stealFailedTask(JS::Dispatchable* dispatchable); 367 368 bool dispatchToEventLoop(js::UniquePtr<JS::Dispatchable>&& dispatchable); 369 bool delayedDispatchToEventLoop( 370 js::UniquePtr<JS::Dispatchable>&& dispatchable, uint32_t delay); 371 372 void cancelTasks(JSContext* cx); 373 void cancelTasks(AutoLockHelperThreadState& lock, JSContext* cx); 374 375 // shutdown() must be called by the JSRuntime while the JSRuntime is valid. 376 void shutdown(JSContext* cx); 377 378 #ifdef DEBUG 379 void setForceQuitting() { forceQuitting_ = true; } 380 #endif 381 }; 382 383 } // namespace js 384 385 #endif // vm_OffThreadPromiseRuntimeState_h