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