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