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 #include "vm/OffThreadPromiseRuntimeState.h"
8
9 #include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF}
10
11 #include <utility> // mozilla::Swap
12
13 #include "jspubtd.h" // js::CurrentThreadCanAccessRuntime
14
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/JSContext.h" // JSContext
21 #include "vm/PromiseObject.h" // js::PromiseObject
22 #include "vm/Realm.h" // js::AutoRealm
23 #include "vm/Runtime.h" // JSRuntime
24
25 #include "vm/Realm-inl.h" // js::AutoRealm::AutoRealm
26
27 using JS::Handle;
28
29 using js::OffThreadPromiseRuntimeState;
30 using js::OffThreadPromiseTask;
31
OffThreadPromiseTask(JSContext * cx,JS::Handle<PromiseObject * > promise)32 OffThreadPromiseTask::OffThreadPromiseTask(JSContext* cx,
33 JS::Handle<PromiseObject*> promise)
34 : runtime_(cx->runtime()), promise_(cx, promise), registered_(false) {
35 MOZ_ASSERT(runtime_ == promise_->zone()->runtimeFromMainThread());
36 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
37 MOZ_ASSERT(cx->runtime()->offThreadPromiseState.ref().initialized());
38 }
39
~OffThreadPromiseTask()40 OffThreadPromiseTask::~OffThreadPromiseTask() {
41 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
42
43 OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
44 MOZ_ASSERT(state.initialized());
45
46 if (registered_) {
47 unregister(state);
48 }
49 }
50
init(JSContext * cx)51 bool OffThreadPromiseTask::init(JSContext* cx) {
52 MOZ_ASSERT(cx->runtime() == runtime_);
53 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
54
55 OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
56 MOZ_ASSERT(state.initialized());
57
58 AutoLockHelperThreadState lock;
59
60 if (!state.live().putNew(this)) {
61 ReportOutOfMemory(cx);
62 return false;
63 }
64
65 registered_ = true;
66 return true;
67 }
68
unregister(OffThreadPromiseRuntimeState & state)69 void OffThreadPromiseTask::unregister(OffThreadPromiseRuntimeState& state) {
70 MOZ_ASSERT(registered_);
71 AutoLockHelperThreadState lock;
72 state.live().remove(this);
73 registered_ = false;
74 }
75
run(JSContext * cx,MaybeShuttingDown maybeShuttingDown)76 void OffThreadPromiseTask::run(JSContext* cx,
77 MaybeShuttingDown maybeShuttingDown) {
78 MOZ_ASSERT(cx->runtime() == runtime_);
79 MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
80 MOZ_ASSERT(registered_);
81
82 // Remove this task from live_ before calling `resolve`, so that if `resolve`
83 // itself drains the queue reentrantly, the queue will not think this task is
84 // yet to be queued and block waiting for it.
85 //
86 // The unregister method synchronizes on the helper thread lock and ensures
87 // that we don't delete the task while the helper thread is still running.
88 OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
89 MOZ_ASSERT(state.initialized());
90 unregister(state);
91
92 if (maybeShuttingDown == JS::Dispatchable::NotShuttingDown) {
93 // We can't leave a pending exception when returning to the caller so do
94 // the same thing as Gecko, which is to ignore the error. This should
95 // only happen due to OOM or interruption.
96 AutoRealm ar(cx, promise_);
97 if (!resolve(cx, promise_)) {
98 cx->clearPendingException();
99 }
100 }
101
102 js_delete(this);
103 }
104
dispatchResolveAndDestroy()105 void OffThreadPromiseTask::dispatchResolveAndDestroy() {
106 AutoLockHelperThreadState lock;
107 dispatchResolveAndDestroy(lock);
108 }
109
dispatchResolveAndDestroy(const AutoLockHelperThreadState & lock)110 void OffThreadPromiseTask::dispatchResolveAndDestroy(
111 const AutoLockHelperThreadState& lock) {
112 MOZ_ASSERT(registered_);
113
114 OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
115 MOZ_ASSERT(state.initialized());
116 MOZ_ASSERT(state.live().has(this));
117
118 // If the dispatch succeeds, then we are guaranteed that run() will be
119 // called on an active JSContext of runtime_.
120 if (state.dispatchToEventLoopCallback_(state.dispatchToEventLoopClosure_,
121 this)) {
122 return;
123 }
124
125 // The DispatchToEventLoopCallback has rejected this task, indicating that
126 // shutdown has begun. Count the number of rejected tasks that have called
127 // dispatchResolveAndDestroy, and when they account for the entire contents of
128 // live_, notify OffThreadPromiseRuntimeState::shutdown that it is safe to
129 // destruct them.
130 state.numCanceled_++;
131 if (state.numCanceled_ == state.live().count()) {
132 state.allCanceled().notify_one();
133 }
134 }
135
OffThreadPromiseRuntimeState()136 OffThreadPromiseRuntimeState::OffThreadPromiseRuntimeState()
137 : dispatchToEventLoopCallback_(nullptr),
138 dispatchToEventLoopClosure_(nullptr),
139 numCanceled_(0),
140 internalDispatchQueueClosed_(false) {}
141
~OffThreadPromiseRuntimeState()142 OffThreadPromiseRuntimeState::~OffThreadPromiseRuntimeState() {
143 MOZ_ASSERT(live_.refNoCheck().empty());
144 MOZ_ASSERT(numCanceled_ == 0);
145 MOZ_ASSERT(internalDispatchQueue_.refNoCheck().empty());
146 MOZ_ASSERT(!initialized());
147 }
148
init(JS::DispatchToEventLoopCallback callback,void * closure)149 void OffThreadPromiseRuntimeState::init(
150 JS::DispatchToEventLoopCallback callback, void* closure) {
151 MOZ_ASSERT(!initialized());
152
153 dispatchToEventLoopCallback_ = callback;
154 dispatchToEventLoopClosure_ = closure;
155
156 MOZ_ASSERT(initialized());
157 }
158
159 /* static */
internalDispatchToEventLoop(void * closure,JS::Dispatchable * d)160 bool OffThreadPromiseRuntimeState::internalDispatchToEventLoop(
161 void* closure, JS::Dispatchable* d) {
162 OffThreadPromiseRuntimeState& state =
163 *reinterpret_cast<OffThreadPromiseRuntimeState*>(closure);
164 MOZ_ASSERT(state.usingInternalDispatchQueue());
165 gHelperThreadLock.assertOwnedByCurrentThread();
166
167 if (state.internalDispatchQueueClosed_) {
168 return false;
169 }
170
171 // The JS API contract is that 'false' means shutdown, so be infallible
172 // here (like Gecko).
173 AutoEnterOOMUnsafeRegion noOOM;
174 if (!state.internalDispatchQueue().pushBack(d)) {
175 noOOM.crash("internalDispatchToEventLoop");
176 }
177
178 // Wake up internalDrain() if it is waiting for a job to finish.
179 state.internalDispatchQueueAppended().notify_one();
180 return true;
181 }
182
usingInternalDispatchQueue() const183 bool OffThreadPromiseRuntimeState::usingInternalDispatchQueue() const {
184 return dispatchToEventLoopCallback_ == internalDispatchToEventLoop;
185 }
186
initInternalDispatchQueue()187 void OffThreadPromiseRuntimeState::initInternalDispatchQueue() {
188 init(internalDispatchToEventLoop, this);
189 MOZ_ASSERT(usingInternalDispatchQueue());
190 }
191
initialized() const192 bool OffThreadPromiseRuntimeState::initialized() const {
193 return !!dispatchToEventLoopCallback_;
194 }
195
internalDrain(JSContext * cx)196 void OffThreadPromiseRuntimeState::internalDrain(JSContext* cx) {
197 MOZ_ASSERT(usingInternalDispatchQueue());
198
199 for (;;) {
200 JS::Dispatchable* d;
201 {
202 AutoLockHelperThreadState lock;
203
204 MOZ_ASSERT(!internalDispatchQueueClosed_);
205 MOZ_ASSERT_IF(!internalDispatchQueue().empty(), !live().empty());
206 if (live().empty()) {
207 return;
208 }
209
210 // There are extant live OffThreadPromiseTasks. If none are in the queue,
211 // block until one of them finishes and enqueues a dispatchable.
212 while (internalDispatchQueue().empty()) {
213 internalDispatchQueueAppended().wait(lock);
214 }
215
216 d = internalDispatchQueue().popCopyFront();
217 }
218
219 // Don't call run() with lock held to avoid deadlock.
220 d->run(cx, JS::Dispatchable::NotShuttingDown);
221 }
222 }
223
internalHasPending()224 bool OffThreadPromiseRuntimeState::internalHasPending() {
225 MOZ_ASSERT(usingInternalDispatchQueue());
226
227 AutoLockHelperThreadState lock;
228 MOZ_ASSERT(!internalDispatchQueueClosed_);
229 MOZ_ASSERT_IF(!internalDispatchQueue().empty(), !live().empty());
230 return !live().empty();
231 }
232
shutdown(JSContext * cx)233 void OffThreadPromiseRuntimeState::shutdown(JSContext* cx) {
234 if (!initialized()) {
235 return;
236 }
237
238 AutoLockHelperThreadState lock;
239
240 // When the shell is using the internal event loop, we must simulate our
241 // requirement of the embedding that, before shutdown, all successfully-
242 // dispatched-to-event-loop tasks have been run.
243 if (usingInternalDispatchQueue()) {
244 DispatchableFifo dispatchQueue;
245 {
246 std::swap(dispatchQueue, internalDispatchQueue());
247 MOZ_ASSERT(internalDispatchQueue().empty());
248 internalDispatchQueueClosed_ = true;
249 }
250
251 // Don't call run() with lock held to avoid deadlock.
252 AutoUnlockHelperThreadState unlock(lock);
253 for (JS::Dispatchable* d : dispatchQueue) {
254 d->run(cx, JS::Dispatchable::ShuttingDown);
255 }
256 }
257
258 // An OffThreadPromiseTask may only be safely deleted on its JSContext's
259 // thread (since it contains a PersistentRooted holding its promise), and
260 // only after it has called dispatchResolveAndDestroy (since that is our
261 // only indication that its owner is done writing into it).
262 //
263 // OffThreadPromiseTasks accepted by the DispatchToEventLoopCallback are
264 // deleted by their 'run' methods. Only dispatchResolveAndDestroy invokes
265 // the callback, and the point of the callback is to call 'run' on the
266 // JSContext's thread, so the conditions above are met.
267 //
268 // But although the embedding's DispatchToEventLoopCallback promises to run
269 // every task it accepts before shutdown, when shutdown does begin it starts
270 // rejecting tasks; we cannot count on 'run' to clean those up for us.
271 // Instead, dispatchResolveAndDestroy keeps a count of rejected ('canceled')
272 // tasks; once that count covers everything in live_, this function itself
273 // runs only on the JSContext's thread, so we can delete them all here.
274 while (live().count() != numCanceled_) {
275 MOZ_ASSERT(numCanceled_ < live().count());
276 allCanceled().wait(lock);
277 }
278
279 // Now that live_ contains only cancelled tasks, we can just delete
280 // everything.
281 for (OffThreadPromiseTaskSet::Range r = live().all(); !r.empty();
282 r.popFront()) {
283 OffThreadPromiseTask* task = r.front();
284
285 // We don't want 'task' to unregister itself (which would mutate live_ while
286 // we are iterating over it) so reset its internal registered_ flag.
287 MOZ_ASSERT(task->registered_);
288 task->registered_ = false;
289 js_delete(task);
290 }
291 live().clear();
292 numCanceled_ = 0;
293
294 // After shutdown, there should be no OffThreadPromiseTask activity in this
295 // JSRuntime. Revert to the !initialized() state to catch bugs.
296 dispatchToEventLoopCallback_ = nullptr;
297 MOZ_ASSERT(!initialized());
298 }
299