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