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/. */
7 #include "vm/OffThreadPromiseRuntimeState.h"
9 #include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF}
11 #include <utility> // mozilla::Swap
13 #include "jspubtd.h" // js::CurrentThreadCanAccessRuntime
15 #include "js/AllocPolicy.h" // js::ReportOutOfMemory
16 #include "js/HeapAPI.h" // JS::shadow::Zone
17 #include "js/Promise.h" // JS::Dispatchable, JS::DispatchToEventLoopCallback
18 #include "js/Utility.h" // js_delete, js::AutoEnterOOMUnsafeRegion
19 #include "threading/ProtectedData.h" // js::UnprotectedData
20 #include "vm/HelperThreads.h" // js::AutoLockHelperThreadState
21 #include "vm/JSContext.h" // JSContext
22 #include "vm/PromiseObject.h" // js::PromiseObject
23 #include "vm/Realm.h" // js::AutoRealm
24 #include "vm/Runtime.h" // JSRuntime
26 #include "vm/Realm-inl.h" // js::AutoRealm::AutoRealm
30 using js::OffThreadPromiseRuntimeState
;
31 using js::OffThreadPromiseTask
;
33 OffThreadPromiseTask::OffThreadPromiseTask(JSContext
* cx
,
34 JS::Handle
<PromiseObject
*> promise
)
35 : runtime_(cx
->runtime()), promise_(cx
, promise
), registered_(false) {
36 MOZ_ASSERT(runtime_
== promise_
->zone()->runtimeFromMainThread());
37 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_
));
38 MOZ_ASSERT(cx
->runtime()->offThreadPromiseState
.ref().initialized());
41 OffThreadPromiseTask::~OffThreadPromiseTask() {
42 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_
));
44 OffThreadPromiseRuntimeState
& state
= runtime_
->offThreadPromiseState
.ref();
45 MOZ_ASSERT(state
.initialized());
52 bool OffThreadPromiseTask::init(JSContext
* cx
) {
53 MOZ_ASSERT(cx
->runtime() == runtime_
);
54 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_
));
56 OffThreadPromiseRuntimeState
& state
= runtime_
->offThreadPromiseState
.ref();
57 MOZ_ASSERT(state
.initialized());
59 AutoLockHelperThreadState lock
;
61 if (!state
.live().putNew(this)) {
62 ReportOutOfMemory(cx
);
70 void OffThreadPromiseTask::unregister(OffThreadPromiseRuntimeState
& state
) {
71 MOZ_ASSERT(registered_
);
72 AutoLockHelperThreadState lock
;
73 state
.live().remove(this);
77 void OffThreadPromiseTask::run(JSContext
* cx
,
78 MaybeShuttingDown maybeShuttingDown
) {
79 MOZ_ASSERT(cx
->runtime() == runtime_
);
80 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_
));
81 MOZ_ASSERT(registered_
);
83 // Remove this task from live_ before calling `resolve`, so that if `resolve`
84 // itself drains the queue reentrantly, the queue will not think this task is
85 // yet to be queued and block waiting for it.
87 // The unregister method synchronizes on the helper thread lock and ensures
88 // that we don't delete the task while the helper thread is still running.
89 OffThreadPromiseRuntimeState
& state
= runtime_
->offThreadPromiseState
.ref();
90 MOZ_ASSERT(state
.initialized());
93 if (maybeShuttingDown
== JS::Dispatchable::NotShuttingDown
) {
94 // We can't leave a pending exception when returning to the caller so do
95 // the same thing as Gecko, which is to ignore the error. This should
96 // only happen due to OOM or interruption.
97 AutoRealm
ar(cx
, promise_
);
98 if (!resolve(cx
, promise_
)) {
99 cx
->clearPendingException();
106 void OffThreadPromiseTask::dispatchResolveAndDestroy() {
107 AutoLockHelperThreadState lock
;
108 dispatchResolveAndDestroy(lock
);
111 void OffThreadPromiseTask::dispatchResolveAndDestroy(
112 const AutoLockHelperThreadState
& lock
) {
113 MOZ_ASSERT(registered_
);
115 OffThreadPromiseRuntimeState
& state
= runtime_
->offThreadPromiseState
.ref();
116 MOZ_ASSERT(state
.initialized());
117 MOZ_ASSERT(state
.live().has(this));
119 // If the dispatch succeeds, then we are guaranteed that run() will be
120 // called on an active JSContext of runtime_.
121 if (state
.dispatchToEventLoopCallback_(state
.dispatchToEventLoopClosure_
,
126 // The DispatchToEventLoopCallback has rejected this task, indicating that
127 // shutdown has begun. Count the number of rejected tasks that have called
128 // dispatchResolveAndDestroy, and when they account for the entire contents of
129 // live_, notify OffThreadPromiseRuntimeState::shutdown that it is safe to
131 state
.numCanceled_
++;
132 if (state
.numCanceled_
== state
.live().count()) {
133 state
.allCanceled().notify_one();
137 OffThreadPromiseRuntimeState::OffThreadPromiseRuntimeState()
138 : dispatchToEventLoopCallback_(nullptr),
139 dispatchToEventLoopClosure_(nullptr),
141 internalDispatchQueueClosed_(false) {}
143 OffThreadPromiseRuntimeState::~OffThreadPromiseRuntimeState() {
144 MOZ_ASSERT(live_
.refNoCheck().empty());
145 MOZ_ASSERT(numCanceled_
== 0);
146 MOZ_ASSERT(internalDispatchQueue_
.refNoCheck().empty());
147 MOZ_ASSERT(!initialized());
150 void OffThreadPromiseRuntimeState::init(
151 JS::DispatchToEventLoopCallback callback
, void* closure
) {
152 MOZ_ASSERT(!initialized());
154 dispatchToEventLoopCallback_
= callback
;
155 dispatchToEventLoopClosure_
= closure
;
157 MOZ_ASSERT(initialized());
161 bool OffThreadPromiseRuntimeState::internalDispatchToEventLoop(
162 void* closure
, JS::Dispatchable
* d
) {
163 OffThreadPromiseRuntimeState
& state
=
164 *reinterpret_cast<OffThreadPromiseRuntimeState
*>(closure
);
165 MOZ_ASSERT(state
.usingInternalDispatchQueue());
166 gHelperThreadLock
.assertOwnedByCurrentThread();
168 if (state
.internalDispatchQueueClosed_
) {
172 // The JS API contract is that 'false' means shutdown, so be infallible
173 // here (like Gecko).
174 AutoEnterOOMUnsafeRegion noOOM
;
175 if (!state
.internalDispatchQueue().pushBack(d
)) {
176 noOOM
.crash("internalDispatchToEventLoop");
179 // Wake up internalDrain() if it is waiting for a job to finish.
180 state
.internalDispatchQueueAppended().notify_one();
184 bool OffThreadPromiseRuntimeState::usingInternalDispatchQueue() const {
185 return dispatchToEventLoopCallback_
== internalDispatchToEventLoop
;
188 void OffThreadPromiseRuntimeState::initInternalDispatchQueue() {
189 init(internalDispatchToEventLoop
, this);
190 MOZ_ASSERT(usingInternalDispatchQueue());
193 bool OffThreadPromiseRuntimeState::initialized() const {
194 return !!dispatchToEventLoopCallback_
;
197 void OffThreadPromiseRuntimeState::internalDrain(JSContext
* cx
) {
198 MOZ_ASSERT(usingInternalDispatchQueue());
203 AutoLockHelperThreadState lock
;
205 MOZ_ASSERT(!internalDispatchQueueClosed_
);
206 MOZ_ASSERT_IF(!internalDispatchQueue().empty(), !live().empty());
207 if (live().empty()) {
211 // There are extant live OffThreadPromiseTasks. If none are in the queue,
212 // block until one of them finishes and enqueues a dispatchable.
213 while (internalDispatchQueue().empty()) {
214 internalDispatchQueueAppended().wait(lock
);
217 d
= internalDispatchQueue().popCopyFront();
220 // Don't call run() with lock held to avoid deadlock.
221 d
->run(cx
, JS::Dispatchable::NotShuttingDown
);
225 bool OffThreadPromiseRuntimeState::internalHasPending() {
226 MOZ_ASSERT(usingInternalDispatchQueue());
228 AutoLockHelperThreadState lock
;
229 MOZ_ASSERT(!internalDispatchQueueClosed_
);
230 MOZ_ASSERT_IF(!internalDispatchQueue().empty(), !live().empty());
231 return !live().empty();
234 void OffThreadPromiseRuntimeState::shutdown(JSContext
* cx
) {
235 if (!initialized()) {
239 AutoLockHelperThreadState lock
;
241 // When the shell is using the internal event loop, we must simulate our
242 // requirement of the embedding that, before shutdown, all successfully-
243 // dispatched-to-event-loop tasks have been run.
244 if (usingInternalDispatchQueue()) {
245 DispatchableFifo dispatchQueue
;
247 std::swap(dispatchQueue
, internalDispatchQueue());
248 MOZ_ASSERT(internalDispatchQueue().empty());
249 internalDispatchQueueClosed_
= true;
252 // Don't call run() with lock held to avoid deadlock.
253 AutoUnlockHelperThreadState
unlock(lock
);
254 for (JS::Dispatchable
* d
: dispatchQueue
) {
255 d
->run(cx
, JS::Dispatchable::ShuttingDown
);
259 // An OffThreadPromiseTask may only be safely deleted on its JSContext's
260 // thread (since it contains a PersistentRooted holding its promise), and
261 // only after it has called dispatchResolveAndDestroy (since that is our
262 // only indication that its owner is done writing into it).
264 // OffThreadPromiseTasks accepted by the DispatchToEventLoopCallback are
265 // deleted by their 'run' methods. Only dispatchResolveAndDestroy invokes
266 // the callback, and the point of the callback is to call 'run' on the
267 // JSContext's thread, so the conditions above are met.
269 // But although the embedding's DispatchToEventLoopCallback promises to run
270 // every task it accepts before shutdown, when shutdown does begin it starts
271 // rejecting tasks; we cannot count on 'run' to clean those up for us.
272 // Instead, dispatchResolveAndDestroy keeps a count of rejected ('canceled')
273 // tasks; once that count covers everything in live_, this function itself
274 // runs only on the JSContext's thread, so we can delete them all here.
275 while (live().count() != numCanceled_
) {
276 MOZ_ASSERT(numCanceled_
< live().count());
277 allCanceled().wait(lock
);
280 // Now that live_ contains only cancelled tasks, we can just delete
282 for (OffThreadPromiseTaskSet::Range r
= live().all(); !r
.empty();
284 OffThreadPromiseTask
* task
= r
.front();
286 // We don't want 'task' to unregister itself (which would mutate live_ while
287 // we are iterating over it) so reset its internal registered_ flag.
288 MOZ_ASSERT(task
->registered_
);
289 task
->registered_
= false;
295 // After shutdown, there should be no OffThreadPromiseTask activity in this
296 // JSRuntime. Revert to the !initialized() state to catch bugs.
297 dispatchToEventLoopCallback_
= nullptr;
298 MOZ_ASSERT(!initialized());