summaryrefslogtreecommitdiffstats
path: root/js/src/vm/OffThreadPromiseRuntimeState.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/vm/OffThreadPromiseRuntimeState.cpp')
-rw-r--r--js/src/vm/OffThreadPromiseRuntimeState.cpp299
1 files changed, 299 insertions, 0 deletions
diff --git a/js/src/vm/OffThreadPromiseRuntimeState.cpp b/js/src/vm/OffThreadPromiseRuntimeState.cpp
new file mode 100644
index 0000000000..004c50492a
--- /dev/null
+++ b/js/src/vm/OffThreadPromiseRuntimeState.cpp
@@ -0,0 +1,299 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "vm/OffThreadPromiseRuntimeState.h"
+
+#include "mozilla/Assertions.h" // MOZ_ASSERT{,_IF}
+
+#include <utility> // mozilla::Swap
+
+#include "jspubtd.h" // js::CurrentThreadCanAccessRuntime
+
+#include "js/AllocPolicy.h" // js::ReportOutOfMemory
+#include "js/HeapAPI.h" // JS::shadow::Zone
+#include "js/Promise.h" // JS::Dispatchable, JS::DispatchToEventLoopCallback
+#include "js/Utility.h" // js_delete, js::AutoEnterOOMUnsafeRegion
+#include "threading/ProtectedData.h" // js::UnprotectedData
+#include "vm/HelperThreads.h" // js::AutoLockHelperThreadState
+#include "vm/JSContext.h" // JSContext
+#include "vm/PromiseObject.h" // js::PromiseObject
+#include "vm/Realm.h" // js::AutoRealm
+#include "vm/Runtime.h" // JSRuntime
+
+#include "vm/Realm-inl.h" // js::AutoRealm::AutoRealm
+
+using JS::Handle;
+
+using js::OffThreadPromiseRuntimeState;
+using js::OffThreadPromiseTask;
+
+OffThreadPromiseTask::OffThreadPromiseTask(JSContext* cx,
+ JS::Handle<PromiseObject*> promise)
+ : runtime_(cx->runtime()), promise_(cx, promise), registered_(false) {
+ MOZ_ASSERT(runtime_ == promise_->zone()->runtimeFromMainThread());
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
+ MOZ_ASSERT(cx->runtime()->offThreadPromiseState.ref().initialized());
+}
+
+OffThreadPromiseTask::~OffThreadPromiseTask() {
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
+
+ OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
+ MOZ_ASSERT(state.initialized());
+
+ if (registered_) {
+ unregister(state);
+ }
+}
+
+bool OffThreadPromiseTask::init(JSContext* cx) {
+ MOZ_ASSERT(cx->runtime() == runtime_);
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
+
+ OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
+ MOZ_ASSERT(state.initialized());
+
+ AutoLockHelperThreadState lock;
+
+ if (!state.live().putNew(this)) {
+ ReportOutOfMemory(cx);
+ return false;
+ }
+
+ registered_ = true;
+ return true;
+}
+
+void OffThreadPromiseTask::unregister(OffThreadPromiseRuntimeState& state) {
+ MOZ_ASSERT(registered_);
+ AutoLockHelperThreadState lock;
+ state.live().remove(this);
+ registered_ = false;
+}
+
+void OffThreadPromiseTask::run(JSContext* cx,
+ MaybeShuttingDown maybeShuttingDown) {
+ MOZ_ASSERT(cx->runtime() == runtime_);
+ MOZ_ASSERT(CurrentThreadCanAccessRuntime(runtime_));
+ MOZ_ASSERT(registered_);
+
+ // Remove this task from live_ before calling `resolve`, so that if `resolve`
+ // itself drains the queue reentrantly, the queue will not think this task is
+ // yet to be queued and block waiting for it.
+ //
+ // The unregister method synchronizes on the helper thread lock and ensures
+ // that we don't delete the task while the helper thread is still running.
+ OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
+ MOZ_ASSERT(state.initialized());
+ unregister(state);
+
+ if (maybeShuttingDown == JS::Dispatchable::NotShuttingDown) {
+ // We can't leave a pending exception when returning to the caller so do
+ // the same thing as Gecko, which is to ignore the error. This should
+ // only happen due to OOM or interruption.
+ AutoRealm ar(cx, promise_);
+ if (!resolve(cx, promise_)) {
+ cx->clearPendingException();
+ }
+ }
+
+ js_delete(this);
+}
+
+void OffThreadPromiseTask::dispatchResolveAndDestroy() {
+ AutoLockHelperThreadState lock;
+ dispatchResolveAndDestroy(lock);
+}
+
+void OffThreadPromiseTask::dispatchResolveAndDestroy(
+ const AutoLockHelperThreadState& lock) {
+ MOZ_ASSERT(registered_);
+
+ OffThreadPromiseRuntimeState& state = runtime_->offThreadPromiseState.ref();
+ MOZ_ASSERT(state.initialized());
+ MOZ_ASSERT(state.live().has(this));
+
+ // If the dispatch succeeds, then we are guaranteed that run() will be
+ // called on an active JSContext of runtime_.
+ if (state.dispatchToEventLoopCallback_(state.dispatchToEventLoopClosure_,
+ this)) {
+ return;
+ }
+
+ // The DispatchToEventLoopCallback has rejected this task, indicating that
+ // shutdown has begun. Count the number of rejected tasks that have called
+ // dispatchResolveAndDestroy, and when they account for the entire contents of
+ // live_, notify OffThreadPromiseRuntimeState::shutdown that it is safe to
+ // destruct them.
+ state.numCanceled_++;
+ if (state.numCanceled_ == state.live().count()) {
+ state.allCanceled().notify_one();
+ }
+}
+
+OffThreadPromiseRuntimeState::OffThreadPromiseRuntimeState()
+ : dispatchToEventLoopCallback_(nullptr),
+ dispatchToEventLoopClosure_(nullptr),
+ numCanceled_(0),
+ internalDispatchQueueClosed_(false) {}
+
+OffThreadPromiseRuntimeState::~OffThreadPromiseRuntimeState() {
+ MOZ_ASSERT(live_.refNoCheck().empty());
+ MOZ_ASSERT(numCanceled_ == 0);
+ MOZ_ASSERT(internalDispatchQueue_.refNoCheck().empty());
+ MOZ_ASSERT(!initialized());
+}
+
+void OffThreadPromiseRuntimeState::init(
+ JS::DispatchToEventLoopCallback callback, void* closure) {
+ MOZ_ASSERT(!initialized());
+
+ dispatchToEventLoopCallback_ = callback;
+ dispatchToEventLoopClosure_ = closure;
+
+ MOZ_ASSERT(initialized());
+}
+
+/* static */
+bool OffThreadPromiseRuntimeState::internalDispatchToEventLoop(
+ void* closure, JS::Dispatchable* d) {
+ OffThreadPromiseRuntimeState& state =
+ *reinterpret_cast<OffThreadPromiseRuntimeState*>(closure);
+ MOZ_ASSERT(state.usingInternalDispatchQueue());
+ gHelperThreadLock.assertOwnedByCurrentThread();
+
+ if (state.internalDispatchQueueClosed_) {
+ return false;
+ }
+
+ // The JS API contract is that 'false' means shutdown, so be infallible
+ // here (like Gecko).
+ AutoEnterOOMUnsafeRegion noOOM;
+ if (!state.internalDispatchQueue().pushBack(d)) {
+ noOOM.crash("internalDispatchToEventLoop");
+ }
+
+ // Wake up internalDrain() if it is waiting for a job to finish.
+ state.internalDispatchQueueAppended().notify_one();
+ return true;
+}
+
+bool OffThreadPromiseRuntimeState::usingInternalDispatchQueue() const {
+ return dispatchToEventLoopCallback_ == internalDispatchToEventLoop;
+}
+
+void OffThreadPromiseRuntimeState::initInternalDispatchQueue() {
+ init(internalDispatchToEventLoop, this);
+ MOZ_ASSERT(usingInternalDispatchQueue());
+}
+
+bool OffThreadPromiseRuntimeState::initialized() const {
+ return !!dispatchToEventLoopCallback_;
+}
+
+void OffThreadPromiseRuntimeState::internalDrain(JSContext* cx) {
+ MOZ_ASSERT(usingInternalDispatchQueue());
+
+ for (;;) {
+ JS::Dispatchable* d;
+ {
+ AutoLockHelperThreadState lock;
+
+ MOZ_ASSERT(!internalDispatchQueueClosed_);
+ MOZ_ASSERT_IF(!internalDispatchQueue().empty(), !live().empty());
+ if (live().empty()) {
+ return;
+ }
+
+ // There are extant live OffThreadPromiseTasks. If none are in the queue,
+ // block until one of them finishes and enqueues a dispatchable.
+ while (internalDispatchQueue().empty()) {
+ internalDispatchQueueAppended().wait(lock);
+ }
+
+ d = internalDispatchQueue().popCopyFront();
+ }
+
+ // Don't call run() with lock held to avoid deadlock.
+ d->run(cx, JS::Dispatchable::NotShuttingDown);
+ }
+}
+
+bool OffThreadPromiseRuntimeState::internalHasPending() {
+ MOZ_ASSERT(usingInternalDispatchQueue());
+
+ AutoLockHelperThreadState lock;
+ MOZ_ASSERT(!internalDispatchQueueClosed_);
+ MOZ_ASSERT_IF(!internalDispatchQueue().empty(), !live().empty());
+ return !live().empty();
+}
+
+void OffThreadPromiseRuntimeState::shutdown(JSContext* cx) {
+ if (!initialized()) {
+ return;
+ }
+
+ AutoLockHelperThreadState lock;
+
+ // When the shell is using the internal event loop, we must simulate our
+ // requirement of the embedding that, before shutdown, all successfully-
+ // dispatched-to-event-loop tasks have been run.
+ if (usingInternalDispatchQueue()) {
+ DispatchableFifo dispatchQueue;
+ {
+ std::swap(dispatchQueue, internalDispatchQueue());
+ MOZ_ASSERT(internalDispatchQueue().empty());
+ internalDispatchQueueClosed_ = true;
+ }
+
+ // Don't call run() with lock held to avoid deadlock.
+ AutoUnlockHelperThreadState unlock(lock);
+ for (JS::Dispatchable* d : dispatchQueue) {
+ d->run(cx, JS::Dispatchable::ShuttingDown);
+ }
+ }
+
+ // An OffThreadPromiseTask may only be safely deleted on its JSContext's
+ // thread (since it contains a PersistentRooted holding its promise), and
+ // only after it has called dispatchResolveAndDestroy (since that is our
+ // only indication that its owner is done writing into it).
+ //
+ // OffThreadPromiseTasks accepted by the DispatchToEventLoopCallback are
+ // deleted by their 'run' methods. Only dispatchResolveAndDestroy invokes
+ // the callback, and the point of the callback is to call 'run' on the
+ // JSContext's thread, so the conditions above are met.
+ //
+ // But although the embedding's DispatchToEventLoopCallback promises to run
+ // every task it accepts before shutdown, when shutdown does begin it starts
+ // rejecting tasks; we cannot count on 'run' to clean those up for us.
+ // Instead, dispatchResolveAndDestroy keeps a count of rejected ('canceled')
+ // tasks; once that count covers everything in live_, this function itself
+ // runs only on the JSContext's thread, so we can delete them all here.
+ while (live().count() != numCanceled_) {
+ MOZ_ASSERT(numCanceled_ < live().count());
+ allCanceled().wait(lock);
+ }
+
+ // Now that live_ contains only cancelled tasks, we can just delete
+ // everything.
+ for (OffThreadPromiseTaskSet::Range r = live().all(); !r.empty();
+ r.popFront()) {
+ OffThreadPromiseTask* task = r.front();
+
+ // We don't want 'task' to unregister itself (which would mutate live_ while
+ // we are iterating over it) so reset its internal registered_ flag.
+ MOZ_ASSERT(task->registered_);
+ task->registered_ = false;
+ js_delete(task);
+ }
+ live().clear();
+ numCanceled_ = 0;
+
+ // After shutdown, there should be no OffThreadPromiseTask activity in this
+ // JSRuntime. Revert to the !initialized() state to catch bugs.
+ dispatchToEventLoopCallback_ = nullptr;
+ MOZ_ASSERT(!initialized());
+}