Bug 1145201: Replace EnqueuePromiseJobCallback and GetIncumbentGlobalCallback with new JobQueue abstract base class. r=arai,smaug
authorJim Blandy <jimb@mozilla.com>
Tue, 12 Feb 2019 08:16:16 +0000
changeset 458932 414b2c238839
parent 458931 56f7fee6d139
child 458933 b6216c391d41
push id35551
push usershindli@mozilla.com
push dateWed, 13 Feb 2019 21:34:09 +0000
treeherdermozilla-central@08f794a4928e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai, smaug
bugs1145201
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1145201: Replace EnqueuePromiseJobCallback and GetIncumbentGlobalCallback with new JobQueue abstract base class. r=arai,smaug While the behavior of ECMAScript Promises and their associated job queue is covered by the ECMAScript standard, the HTML specification amends that with additional behavior the web platform requires. To support this, SpiderMonkey provides hooks the embedding can set to replace SpiderMonkey's queue with its own implementation. At present, these hooks are C-style function-pointer-and-void-pointer pairs, which are awkward to handle and mistake-prone, as passing a function the wrong void* is not a type error. Later patches in this series must add new hooks, making a bad situation worse. A C++ abstract base class is a well-typed alternative. This introduces a new `JS::JobQueue` abstract class, and adapts SpiderMonkey's internal job queue and Gecko's customization to use it. `GetIncumbentGlobalCallback` and `EnqueuePromiseJobCallback` become virtual methods. Within SpiderMonkey, the patch gathers the various fields of JSContext that implement the internal queue into their own type, js::InternalJobQueue. Various jsfriendapi functions become veneers for calls to methods specific to the derived class. The InternalJobQueue type itself remains private to SpiderMonkey, as it uses types like TraceableFifo, derived from Fifo, that are not part of SpiderMonkey's public API. Within Gecko, CycleCollectedJSContext acquires JS::JobQueue as a private base class, and a few static methods are cleaned up nicely. There are a few other hooks defined in js/public/Promise.h that might make sense to turn into virtual methods on JobQueue. For example, DispatchToEventLoopCallback, used for resolving promises of results from off-main-thread tasks, is probably necessarily connected to the JobQueue implementation in use, so it might not be sensible to set one without the other. But it was left unchanged to reduce this patch's size. Differential Revision: https://phabricator.services.mozilla.com/D17544
js/public/Promise.h
js/src/jit-test/tests/debug/job-queue-02.js
js/src/jsapi.cpp
js/src/shell/js.cpp
js/src/vm/JSContext.cpp
js/src/vm/JSContext.h
js/src/vm/Runtime.cpp
xpcom/base/CycleCollectedJSContext.cpp
xpcom/base/CycleCollectedJSContext.h
--- a/js/public/Promise.h
+++ b/js/public/Promise.h
@@ -8,54 +8,84 @@
 #define js_Promise_h
 
 #include "jspubtd.h"
 #include "js/RootingAPI.h"
 #include "js/TypeDecls.h"
 
 namespace JS {
 
-typedef JSObject* (*GetIncumbentGlobalCallback)(JSContext* cx);
+/**
+ * Abstract base class for an ECMAScript Job Queue:
+ * https://www.ecma-international.org/ecma-262/9.0/index.html#sec-jobs-and-job-queues
+ *
+ * SpiderMonkey doesn't schedule Promise resolution jobs itself; instead, the
+ * embedding can provide an instance of this class SpiderMonkey can use to do
+ * that scheduling.
+ *
+ * The JavaScript shell includes a simple implementation adequate for running
+ * tests. Browsers need to augment job handling to meet their own additional
+ * requirements, so they can provide their own implementation.
+ */
+class JS_PUBLIC_API JobQueue {
+ public:
+  virtual ~JobQueue() = default;
+
+  /**
+   * Ask the embedding for the incumbent global.
+   *
+   * SpiderMonkey doesn't itself have a notion of incumbent globals as defined
+   * by the HTML spec, so we need the embedding to provide this. See
+   * dom/script/ScriptSettings.h for details.
+   */
+  virtual JSObject* getIncumbentGlobal(JSContext* cx) = 0;
 
-typedef bool (*EnqueuePromiseJobCallback)(JSContext* cx,
-                                          JS::HandleObject promise,
-                                          JS::HandleObject job,
-                                          JS::HandleObject allocationSite,
-                                          JS::HandleObject incumbentGlobal,
-                                          void* data);
+  /**
+   * Enqueue a reaction job `job` for `promise`, which was allocated at
+   * `allocationSite`. Provide `incumbentGlobal` as the incumbent global for
+   * the reaction job's execution.
+   */
+  virtual bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
+                                 JS::HandleObject job,
+                                 JS::HandleObject allocationSite,
+                                 JS::HandleObject incumbentGlobal) = 0;
+
+  /**
+   * Run all jobs in the queue. Running one job may enqueue others; continue to
+   * run jobs until the queue is empty.
+   *
+   * Calling this method at the wrong time can break the web. The HTML spec
+   * indicates exactly when the job queue should be drained (in HTML jargon,
+   * when it should "perform a microtask checkpoint"), and doing so at other
+   * times can incompatibly change the semantics of programs that use promises
+   * or other microtask-based features.
+   */
+  virtual void runJobs(JSContext* cx) = 0;
+
+  /**
+   * Return true if the job queue is empty, false otherwise.
+   */
+  virtual bool empty() const = 0;
+};
+
+/**
+ * Tell SpiderMonkey to use `queue` to schedule promise reactions.
+ *
+ * SpiderMonkey does not take ownership of the queue; it is the embedding's
+ * responsibility to clean it up after the runtime is destroyed.
+ */
+extern JS_PUBLIC_API void SetJobQueue(JSContext* cx, JobQueue* queue);
 
 enum class PromiseRejectionHandlingState { Unhandled, Handled };
 
 typedef void (*PromiseRejectionTrackerCallback)(
     JSContext* cx, JS::HandleObject promise,
     JS::PromiseRejectionHandlingState state, void* data);
 
 /**
- * Sets the callback that's invoked whenever an incumbent global is required.
- *
- * SpiderMonkey doesn't itself have a notion of incumbent globals as defined
- * by the html spec, so we need the embedding to provide this.
- * See dom/base/ScriptSettings.h for details.
- */
-extern JS_PUBLIC_API void SetGetIncumbentGlobalCallback(
-    JSContext* cx, GetIncumbentGlobalCallback callback);
-
-/**
- * Sets the callback that's invoked whenever a Promise job should be enqeued.
- *
- * SpiderMonkey doesn't schedule Promise resolution jobs itself; instead,
- * using this function the embedding can provide a callback to do that
- * scheduling. The provided `callback` is invoked with the promise job,
- * the corresponding Promise's allocation stack, and the `data` pointer
- * passed here as arguments.
- */
-extern JS_PUBLIC_API void SetEnqueuePromiseJobCallback(
-    JSContext* cx, EnqueuePromiseJobCallback callback, void* data = nullptr);
-
-/**
  * Sets the callback that's invoked whenever a Promise is rejected without
  * a rejection handler, and when a Promise that was previously rejected
  * without a handler gets a handler attached.
  */
 extern JS_PUBLIC_API void SetPromiseRejectionTrackerCallback(
     JSContext* cx, PromiseRejectionTrackerCallback callback,
     void* data = nullptr);
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/job-queue-02.js
@@ -0,0 +1,82 @@
+// |jit-test| error: "async tests completed successfully"
+// Test that the shell's job queue doesn't skip calls to JS::JobQueueMayNotBeEmpty.
+
+// For expressions like `await 1`, or `await P` for some already-resolved
+// promise P, there's no need to suspend the async call to determine the
+// expression's value. Suspension and resumption are expensive, so it would be
+// nice if we could avoid them.
+//
+// But of course, even when the value is known, the act of suspension itself is
+// visible: an async call's first suspension returns to its (synchronous)
+// caller; subsequent suspensions let other jobs run. So in general, we can't
+// short-circuit such `await` expressions.
+//
+// However, if an async call has been resumed from the job queue (that is, this
+// isn't the initial execution, with a synchronous caller expecting a promise of
+// the call's final return value), and there are no other jobs following that,
+// then the `await`'s reaction job would run immediately following this job ---
+// which *is* indistinguishable from skipping the suspension altogether.
+//
+// A JS::JobQueue implementation may call JS::JobQueueIsEmpty to indicate to the
+// engine that the currently running job is the last job in the queue, so this
+// optimization may be considered (there are further conditions that must be met
+// as well). If the JobQueue calls JobQueueIsEmpty, then it must also call
+// JS::JobQueueMayNotBeEmpty when jobs are enqueued, to indicate when the
+// opportunity has passed.
+
+var log = '';
+async function f(label, k) {
+  log += label + '1';
+  await 1;
+  log += label + '2';
+  await 1;
+  log += label + '3';
+
+  return k();
+}
+
+// Call `f` with `label` and `k`. If `skippable` is true, exercise the path that
+// skips the suspension and resumption; otherwise exercise the
+// non-short-circuited path.
+function test(skippable, label, k) {
+  var resolve;
+  (new Promise(r => { resolve = r; }))
+    .then(v => { log += v + 't'; });
+  assertEq(log, '');
+  f(label, k);
+  // job queue now: f(label)'s first await's continuation
+  assertEq(log, label + '1');
+
+  if (!skippable) {
+    resolve('p');
+    assertEq(log, label + '1');
+    // job queue now: f(label)'s first await's continuation, explicit promise's reaction
+  }
+
+  // Resuming f(label) will reach the second await, which should skip suspension
+  // or not, depending on whether we resolved that promise.
+}
+
+// SpiderMonkey's internal 'queue is empty' flag is initially false, even though
+// the queue is initially empty, because we don't yet know whether the embedding
+// is going to participate in the optimization by calling
+// JS::JobQueueMayNotBeEmpty and JS::JobQueueIsEmpty. But since the shell uses
+// SpiderMonkey's internal job queue implementation, this call to
+// `drainJobQueue` calls `JS::JobQueueIsEmpty`, and we are ready to play.
+Promise.resolve(42).then(v => assertEq(v, 42));
+drainJobQueue();
+
+log = '';
+test(true, 'b', continuation1);
+
+function continuation1() {
+  assertEq(log, 'b1b2b3');
+
+  log = '';
+  test(false, 'c', continuation2);
+}
+
+function continuation2() {
+  assertEq(log, 'c1c2ptc3');
+  throw "async tests completed successfully"; // proof that we actually finished
+}
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -3867,26 +3867,18 @@ JS_PUBLIC_API void JS_ResetInterruptCall
   cx->interruptCallbackDisabled = enable;
 }
 
 /************************************************************************/
 
 /*
  * Promises.
  */
-JS_PUBLIC_API void JS::SetGetIncumbentGlobalCallback(
-    JSContext* cx, GetIncumbentGlobalCallback callback) {
-  cx->getIncumbentGlobalCallback = callback;
-}
-
-JS_PUBLIC_API void JS::SetEnqueuePromiseJobCallback(
-    JSContext* cx, EnqueuePromiseJobCallback callback,
-    void* data /* = nullptr */) {
-  cx->enqueuePromiseJobCallback = callback;
-  cx->enqueuePromiseJobCallbackData = data;
+JS_PUBLIC_API void JS::SetJobQueue(JSContext* cx, JobQueue* queue) {
+  cx->jobQueue = queue;
 }
 
 extern JS_PUBLIC_API void JS::SetPromiseRejectionTrackerCallback(
     JSContext* cx, PromiseRejectionTrackerCallback callback,
     void* data /* = nullptr */) {
   cx->promiseRejectionTrackerCallback = callback;
   cx->promiseRejectionTrackerCallbackData = data;
 }
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -1035,22 +1035,22 @@ static bool DrainJobQueue(JSContext* cx,
 
   args.rval().setUndefined();
   return true;
 }
 
 static bool GlobalOfFirstJobInQueue(JSContext* cx, unsigned argc, Value* vp) {
   CallArgs args = CallArgsFromVp(argc, vp);
 
-  if (cx->jobQueue->empty()) {
+  RootedObject job(cx, cx->internalJobQueue->maybeFront());
+  if (!job) {
     JS_ReportErrorASCII(cx, "Job queue is empty");
     return false;
   }
 
-  RootedObject job(cx, cx->jobQueue->front());
   RootedObject global(cx, &job->nonCCWGlobal());
   if (!cx->compartment()->wrap(cx, &global)) {
     return false;
   }
 
   args.rval().setObject(*global);
   return true;
 }
--- a/js/src/vm/JSContext.cpp
+++ b/js/src/vm/JSContext.cpp
@@ -168,38 +168,27 @@ JSContext* js::NewContext(uint32_t maxBy
     js_delete(cx);
     js_delete(runtime);
     return nullptr;
   }
 
   return cx;
 }
 
-static void FreeJobQueueHandling(JSContext* cx) {
-  if (!cx->jobQueue) {
-    return;
-  }
-
-  FreeOp* fop = cx->defaultFreeOp();
-  fop->delete_(cx->jobQueue.ref());
-  cx->getIncumbentGlobalCallback = nullptr;
-  cx->enqueuePromiseJobCallback = nullptr;
-  cx->enqueuePromiseJobCallbackData = nullptr;
-}
-
 void js::DestroyContext(JSContext* cx) {
   JS_AbortIfWrongThread(cx);
 
   cx->checkNoGCRooters();
 
   // Cancel all off thread Ion compiles. Completed Ion compiles may try to
   // interrupt this context. See HelperThread::handleIonWorkload.
   CancelOffThreadIonCompile(cx->runtime());
 
-  FreeJobQueueHandling(cx);
+  cx->jobQueue = nullptr;
+  cx->internalJobQueue = nullptr;
 
   // Flush promise tasks executing in helper threads early, before any parts
   // of the JSRuntime that might be visible to helper threads are torn down.
   cx->runtime()->offThreadPromiseState.ref().shutdown(cx);
 
   // Destroy the runtime along with its last context.
   cx->runtime()->destroyRuntime();
   js_delete(cx->runtime());
@@ -1014,104 +1003,106 @@ void JSContext::recoverFromOutOfMemory()
   } else {
     if (isExceptionPending()) {
       MOZ_ASSERT(isThrowingOutOfMemory());
       clearPendingException();
     }
   }
 }
 
-static bool InternalEnqueuePromiseJobCallback(JSContext* cx,
-                                              JS::HandleObject promise,
-                                              JS::HandleObject job,
-                                              JS::HandleObject allocationSite,
-                                              JS::HandleObject incumbentGlobal,
-                                              void* data) {
-  MOZ_ASSERT(job);
-  JS::JobQueueMayNotBeEmpty(cx);
-  if (!cx->jobQueue->pushBack(job)) {
-    ReportOutOfMemory(cx);
-    return false;
-  }
-  return true;
-}
-
 JS_FRIEND_API bool js::UseInternalJobQueues(JSContext* cx) {
   // Internal job queue handling must be set up very early. Self-hosting
   // initialization is as good a marker for that as any.
   MOZ_RELEASE_ASSERT(
       !cx->runtime()->hasInitializedSelfHosting(),
       "js::UseInternalJobQueues must be called early during runtime startup.");
   MOZ_ASSERT(!cx->jobQueue);
-  auto* queue =
-      js_new<PersistentRooted<JobQueue>>(cx, JobQueue(SystemAllocPolicy()));
+  auto queue = MakeUnique<InternalJobQueue>(cx);
   if (!queue) {
     return false;
   }
 
-  cx->jobQueue = queue;
+  cx->internalJobQueue = std::move(queue);
+  cx->jobQueue = cx->internalJobQueue.ref().get();
 
   cx->runtime()->offThreadPromiseState.ref().initInternalDispatchQueue();
   MOZ_ASSERT(cx->runtime()->offThreadPromiseState.ref().initialized());
 
-  JS::SetEnqueuePromiseJobCallback(cx, InternalEnqueuePromiseJobCallback);
-
   return true;
 }
 
 JS_FRIEND_API bool js::EnqueueJob(JSContext* cx, JS::HandleObject job) {
   MOZ_ASSERT(cx->jobQueue);
-  JS::JobQueueMayNotBeEmpty(cx);
-  if (!cx->jobQueue->pushBack(job)) {
+  return cx->jobQueue->enqueuePromiseJob(cx, nullptr, job, nullptr, nullptr);
+}
+
+JS_FRIEND_API void js::StopDrainingJobQueue(JSContext* cx) {
+  MOZ_ASSERT(cx->internalJobQueue.ref());
+  cx->internalJobQueue->interrupt();
+}
+
+JS_FRIEND_API void js::RunJobs(JSContext* cx) {
+  MOZ_ASSERT(cx->jobQueue);
+  cx->jobQueue->runJobs(cx);
+}
+
+JSObject* InternalJobQueue::getIncumbentGlobal(JSContext* cx) {
+  if (!cx->compartment()) {
+    return nullptr;
+  }
+  return cx->global();
+}
+
+bool InternalJobQueue::enqueuePromiseJob(JSContext* cx,
+                                         JS::HandleObject promise,
+                                         JS::HandleObject job,
+                                         JS::HandleObject allocationSite,
+                                         JS::HandleObject incumbentGlobal) {
+  MOZ_ASSERT(job);
+  if (!queue.pushBack(job)) {
     ReportOutOfMemory(cx);
     return false;
   }
 
+  JS::JobQueueMayNotBeEmpty(cx);
   return true;
 }
 
-JS_FRIEND_API void js::StopDrainingJobQueue(JSContext* cx) {
-  MOZ_ASSERT(cx->jobQueue);
-  cx->stopDrainingJobQueue = true;
-}
-
-JS_FRIEND_API void js::RunJobs(JSContext* cx) {
-  MOZ_ASSERT(cx->jobQueue);
-
-  if (cx->drainingJobQueue || cx->stopDrainingJobQueue) {
+void InternalJobQueue::runJobs(JSContext* cx) {
+  if (draining_ || interrupted_) {
     return;
   }
 
   while (true) {
     cx->runtime()->offThreadPromiseState.ref().internalDrain(cx);
 
     // It doesn't make sense for job queue draining to be reentrant. At the
     // same time we don't want to assert against it, because that'd make
     // drainJobQueue unsafe for fuzzers. We do want fuzzers to test this,
     // so we simply ignore nested calls of drainJobQueue.
-    cx->drainingJobQueue = true;
+    draining_ = true;
 
     RootedObject job(cx);
     JS::HandleValueArray args(JS::HandleValueArray::empty());
     RootedValue rval(cx);
 
     // Execute jobs in a loop until we've reached the end of the queue.
-    while (!cx->jobQueue->empty()) {
+    while (!queue.empty()) {
       // A previous job might have set this flag. E.g., the js shell
       // sets it if the `quit` builtin function is called.
-      if (cx->stopDrainingJobQueue) {
+      if (interrupted_) {
         break;
       }
 
-      job = cx->jobQueue->front();
-      cx->jobQueue->popFront();
+      job = queue.front();
+      queue.popFront();
 
       // If the next job is the last job in the job queue, allow
       // skipping the standard job queuing behavior.
-      if (cx->jobQueue->empty()) {
+      if (queue.empty()) {
         JS::JobQueueIsEmpty(cx);
       }
 
       AutoRealm ar(cx, &job->as<JSFunction>());
       {
         if (!JS::Call(cx, UndefinedHandleValue, job, args, &rval)) {
           // Nothing we can do about uncatchable exceptions.
           if (!cx->isExceptionPending()) {
@@ -1127,32 +1118,42 @@ JS_FRIEND_API void js::RunJobs(JSContext
             cx->clearPendingException();
             js::ReportExceptionClosure reportExn(exn);
             PrepareScriptEnvironmentAndInvoke(cx, cx->global(), reportExn);
           }
         }
       }
     }
 
-    cx->drainingJobQueue = false;
+    draining_ = false;
 
-    if (cx->stopDrainingJobQueue) {
-      cx->stopDrainingJobQueue = false;
+    if (interrupted_) {
+      interrupted_ = false;
       break;
     }
 
-    cx->jobQueue->clear();
+    queue.clear();
 
     // It's possible a job added a new off-thread promise task.
     if (!cx->runtime()->offThreadPromiseState.ref().internalHasPending()) {
       break;
     }
   }
 }
 
+bool InternalJobQueue::empty() const { return queue.empty(); }
+
+JSObject* InternalJobQueue::maybeFront() const {
+  if (queue.empty()) {
+    return nullptr;
+  }
+
+  return queue.get().front();
+}
+
 JS::Error JSContext::reportedError;
 JS::OOM JSContext::reportedOOM;
 
 mozilla::GenericErrorResult<OOM&> JSContext::alreadyReportedOOM() {
 #ifdef DEBUG
   if (helperThread()) {
     // Keep in sync with addPendingOutOfMemory.
     if (ParseTask* task = helperThread()->parseTask()) {
@@ -1238,22 +1239,17 @@ JSContext::JSContext(JSRuntime* runtime,
       asyncCauseForNewActivations(nullptr),
       asyncCallIsExplicit(false),
       interruptCallbackDisabled(false),
       interruptBits_(0),
       osrTempData_(nullptr),
       ionReturnOverride_(MagicValue(JS_ARG_POISON)),
       jitStackLimit(UINTPTR_MAX),
       jitStackLimitNoInterrupt(UINTPTR_MAX),
-      getIncumbentGlobalCallback(nullptr),
-      enqueuePromiseJobCallback(nullptr),
-      enqueuePromiseJobCallbackData(nullptr),
       jobQueue(nullptr),
-      drainingJobQueue(false),
-      stopDrainingJobQueue(false),
       canSkipEnqueuingJobs(false),
       promiseRejectionTrackerCallback(nullptr),
       promiseRejectionTrackerCallbackData(nullptr)
 #ifdef JS_STRUCTURED_SPEW
       ,
       structuredSpewer_()
 #endif
 {
--- a/js/src/vm/JSContext.h
+++ b/js/src/vm/JSContext.h
@@ -67,17 +67,51 @@ class MOZ_RAII AutoCycleDetector {
   bool cyclic;
   MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
 };
 
 struct AutoResolving;
 
 struct HelperThread;
 
-using JobQueue = TraceableFifo<JSObject*, 0, SystemAllocPolicy>;
+class InternalJobQueue : public JS::JobQueue {
+ public:
+  explicit InternalJobQueue(JSContext* cx)
+      : queue(cx, SystemAllocPolicy()), draining_(false), interrupted_(false) {}
+  ~InternalJobQueue() = default;
+
+  // JS::JobQueue methods.
+  JSObject* getIncumbentGlobal(JSContext* cx) override;
+  bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
+                         JS::HandleObject job, JS::HandleObject allocationSite,
+                         JS::HandleObject incumbentGlobal) override;
+  void runJobs(JSContext* cx) override;
+  bool empty() const override;
+
+  // If we are currently in a call to runJobs(), make that call stop processing
+  // jobs once the current one finishes, and return. If we are not currently in
+  // a call to runJobs, make all future calls return immediately.
+  void interrupt() { interrupted_ = true; }
+
+  // Return the front element of the queue, or nullptr if the queue is empty.
+  // This is only used by shell testing functions.
+  JSObject* maybeFront() const;
+
+ private:
+  using Queue = js::TraceableFifo<JSObject*, 0, SystemAllocPolicy>;
+
+  JS::PersistentRooted<Queue> queue;
+
+  // True if we are in the midst of draining jobs from this queue. We use this
+  // to avoid re-entry (nested calls simply return immediately).
+  bool draining_;
+
+  // True if we've been asked to interrupt draining jobs. Set by interrupt().
+  bool interrupted_;
+};
 
 class AutoLockScriptData;
 
 void ReportOverRecursed(JSContext* cx, unsigned errorNumber);
 
 /* Thread Local Storage slot for storing the context for a thread. */
 extern MOZ_THREAD_LOCAL(JSContext*) TlsContext;
 
@@ -888,27 +922,32 @@ struct JSContext : public JS::RootingCon
 
   mozilla::Atomic<uintptr_t, mozilla::Relaxed,
                   mozilla::recordreplay::Behavior::DontPreserve>
       jitStackLimit;
 
   // Like jitStackLimit, but not reset to trigger interrupts.
   js::ThreadData<uintptr_t> jitStackLimitNoInterrupt;
 
-  // Promise callbacks.
-  js::ThreadData<JS::GetIncumbentGlobalCallback> getIncumbentGlobalCallback;
-  js::ThreadData<JS::EnqueuePromiseJobCallback> enqueuePromiseJobCallback;
-  js::ThreadData<void*> enqueuePromiseJobCallbackData;
+  // Queue of pending jobs as described in ES2016 section 8.4.
+  //
+  // This is a non-owning pointer to either:
+  // - a JobQueue implementation the embedding provided by calling
+  //   JS::SetJobQueue, owned by the embedding, or
+  // - our internal JobQueue implementation, established by calling
+  //   js::UseInternalJobQueues, owned by JSContext::internalJobQueue below.
+  js::ThreadData<JS::JobQueue*> jobQueue;
 
-  // Queue of pending jobs as described in ES2016 section 8.4.
-  // Only used if internal job queue handling was activated using
-  // `js::UseInternalJobQueues`.
-  js::ThreadData<JS::PersistentRooted<js::JobQueue>*> jobQueue;
-  js::ThreadData<bool> drainingJobQueue;
-  js::ThreadData<bool> stopDrainingJobQueue;
+  // If the embedding has called js::UseInternalJobQueues, this is the owning
+  // pointer to our internal JobQueue implementation, which JSContext::jobQueue
+  // borrows.
+  js::ThreadData<js::UniquePtr<js::InternalJobQueue>> internalJobQueue;
+
+  // True if jobQueue is empty, or we are running the last job in the queue.
+  // Such conditions permit optimizations around `await` expressions.
   js::ThreadData<bool> canSkipEnqueuingJobs;
 
   js::ThreadData<JS::PromiseRejectionTrackerCallback>
       promiseRejectionTrackerCallback;
   js::ThreadData<void*> promiseRejectionTrackerCallbackData;
 
   JSObject* getIncumbentGlobal(JSContext* cx);
   bool enqueuePromiseJob(JSContext* cx, js::HandleFunction job,
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -577,42 +577,35 @@ FreeOp::~FreeOp() {
   }
 }
 
 bool FreeOp::isDefaultFreeOp() const {
   return runtime_ && runtime_->defaultFreeOp() == this;
 }
 
 GlobalObject* JSRuntime::getIncumbentGlobal(JSContext* cx) {
-  // If the embedding didn't set a callback for getting the incumbent
-  // global, the currently active global is used.
-  if (!cx->getIncumbentGlobalCallback) {
-    if (!cx->compartment()) {
-      return nullptr;
-    }
-    return cx->global();
+  MOZ_ASSERT(cx->jobQueue);
+
+  JSObject* obj = cx->jobQueue->getIncumbentGlobal(cx);
+  if (!obj) {
+    return nullptr;
   }
 
-  if (JSObject* obj = cx->getIncumbentGlobalCallback(cx)) {
-    MOZ_ASSERT(obj->is<GlobalObject>(),
-               "getIncumbentGlobalCallback must return a global!");
-    return &obj->as<GlobalObject>();
-  }
-
-  return nullptr;
+  MOZ_ASSERT(obj->is<GlobalObject>(),
+             "getIncumbentGlobalCallback must return a global!");
+  return &obj->as<GlobalObject>();
 }
 
 bool JSRuntime::enqueuePromiseJob(JSContext* cx, HandleFunction job,
                                   HandleObject promise,
                                   Handle<GlobalObject*> incumbentGlobal) {
-  MOZ_ASSERT(cx->enqueuePromiseJobCallback,
-             "Must set a callback using JS::SetEnqueuePromiseJobCallback "
-             "before using Promises");
+  MOZ_ASSERT(cx->jobQueue,
+             "Must select a JobQueue implementation using JS::JobQueue "
+             "or js::UseInternalJobQueues before using Promises");
 
-  void* data = cx->enqueuePromiseJobCallbackData;
   RootedObject allocationSite(cx);
   if (promise) {
 #ifdef DEBUG
     AssertSameCompartment(job, promise);
 #endif
 
     RootedObject unwrappedPromise(cx, promise);
     // While the job object is guaranteed to be unwrapped, the promise
@@ -620,18 +613,18 @@ bool JSRuntime::enqueuePromiseJob(JSCont
     // builtin/Promise.cpp for details.
     if (IsWrapper(promise)) {
       unwrappedPromise = UncheckedUnwrap(promise);
     }
     if (unwrappedPromise->is<PromiseObject>()) {
       allocationSite = JS::GetPromiseAllocationSite(unwrappedPromise);
     }
   }
-  return cx->enqueuePromiseJobCallback(cx, promise, job, allocationSite,
-                                       incumbentGlobal, data);
+  return cx->jobQueue->enqueuePromiseJob(cx, promise, job, allocationSite,
+                                         incumbentGlobal);
 }
 
 void JSRuntime::addUnhandledRejectedPromise(JSContext* cx,
                                             js::HandleObject promise) {
   MOZ_ASSERT(promise->is<PromiseObject>());
   if (!cx->promiseRejectionTrackerCallback) {
     return;
   }
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -122,19 +122,18 @@ void CycleCollectedJSContext::Initialize
   mRuntime->AddContext(this);
 
   mOwningThread->SetScriptObserver(this);
   // The main thread has a base recursion depth of 0, workers of 1.
   mBaseRecursionDepth = RecursionDepth();
 
   NS_GetCurrentThread()->SetCanInvokeJS(true);
 
-  JS::SetGetIncumbentGlobalCallback(mJSContext, GetIncumbentGlobalCallback);
+  JS::SetJobQueue(mJSContext, this);
 
-  JS::SetEnqueuePromiseJobCallback(mJSContext, EnqueuePromiseJobCallback, this);
   JS::SetPromiseRejectionTrackerCallback(mJSContext,
                                          PromiseRejectionTrackerCallback, this);
   mUncaughtRejections.init(mJSContext,
                            JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>(
                                js::SystemAllocPolicy()));
   mConsumedRejections.init(mJSContext,
                            JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>(
                                js::SystemAllocPolicy()));
@@ -250,45 +249,54 @@ class PromiseJobRunnable final : public 
     return global && global->IsInSyncOperation();
   }
 
  private:
   RefPtr<PromiseJobCallback> mCallback;
   bool mPropagateUserInputEventHandling;
 };
 
-/* static */
-JSObject* CycleCollectedJSContext::GetIncumbentGlobalCallback(JSContext* aCx) {
+JSObject* CycleCollectedJSContext::getIncumbentGlobal(JSContext* aCx) {
   nsIGlobalObject* global = mozilla::dom::GetIncumbentGlobal();
   if (global) {
     return global->GetGlobalJSObject();
   }
   return nullptr;
 }
 
-/* static */
-bool CycleCollectedJSContext::EnqueuePromiseJobCallback(
+bool CycleCollectedJSContext::enqueuePromiseJob(
     JSContext* aCx, JS::HandleObject aPromise, JS::HandleObject aJob,
-    JS::HandleObject aAllocationSite, JS::HandleObject aIncumbentGlobal,
-    void* aData) {
-  CycleCollectedJSContext* self = static_cast<CycleCollectedJSContext*>(aData);
-  MOZ_ASSERT(aCx == self->Context());
-  MOZ_ASSERT(Get() == self);
+    JS::HandleObject aAllocationSite, JS::HandleObject aIncumbentGlobal) {
+  MOZ_ASSERT(aCx == Context());
+  MOZ_ASSERT(Get() == this);
 
   nsIGlobalObject* global = nullptr;
   if (aIncumbentGlobal) {
     global = xpc::NativeGlobal(aIncumbentGlobal);
   }
   JS::RootedObject jobGlobal(aCx, JS::CurrentGlobalOrNull(aCx));
   RefPtr<PromiseJobRunnable> runnable = new PromiseJobRunnable(
       aPromise, aJob, jobGlobal, aAllocationSite, global);
-  self->DispatchToMicroTask(runnable.forget());
+  DispatchToMicroTask(runnable.forget());
   return true;
 }
 
+void CycleCollectedJSContext::runJobs(JSContext* aCx) {
+  MOZ_ASSERT(aCx == Context());
+  MOZ_ASSERT(Get() == this);
+  PerformMicroTaskCheckPoint();
+}
+
+bool CycleCollectedJSContext::empty() const {
+  // This is our override of JS::JobQueue::empty. Since that interface is only
+  // concerned with the ordinary microtask queue, not the debugger microtask
+  // queue, we only report on the former.
+  return mPendingMicroTaskRunnables.empty();
+}
+
 /* static */
 void CycleCollectedJSContext::PromiseRejectionTrackerCallback(
     JSContext* aCx, JS::HandleObject aPromise,
     JS::PromiseRejectionHandlingState state, void* aData) {
 #ifdef DEBUG
   CycleCollectedJSContext* self = static_cast<CycleCollectedJSContext*>(aData);
 #endif  // DEBUG
   MOZ_ASSERT(aCx == self->Context());
@@ -588,9 +596,10 @@ void CycleCollectedJSContext::PerformDeb
     if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
       JS::JobQueueIsEmpty(Context());
     }
     runnable->Run(aso);
   }
 
   AfterProcessMicrotasks();
 }
+
 }  // namespace mozilla
--- a/xpcom/base/CycleCollectedJSContext.h
+++ b/xpcom/base/CycleCollectedJSContext.h
@@ -79,17 +79,18 @@ class MicroTaskRunnable {
   virtual bool Suppressed() { return false; }
 
  protected:
   virtual ~MicroTaskRunnable() = default;
 };
 
 class CycleCollectedJSContext
     : dom::PerThreadAtomCache,
-      public LinkedListElement<CycleCollectedJSContext> {
+      public LinkedListElement<CycleCollectedJSContext>,
+      private JS::JobQueue {
   friend class CycleCollectedJSRuntime;
 
  protected:
   CycleCollectedJSContext();
   virtual ~CycleCollectedJSContext();
 
   MOZ_IS_CLASS_INIT
   nsresult Initialize(JSRuntime* aParentRuntime, uint32_t aMaxBytes,
@@ -228,16 +229,26 @@ class CycleCollectedJSContext
   JS::PersistentRooted<JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>>
       mConsumedRejections;
   nsTArray<nsCOMPtr<nsISupports /* UncaughtRejectionObserver */>>
       mUncaughtRejectionObservers;
 
   virtual bool IsSystemCaller() const = 0;
 
  private:
+  // JS::JobQueue implementation: see js/public/Promise.h.
+  // SpiderMonkey uses this to enqueue promise resolution jobs.
+  JSObject* getIncumbentGlobal(JSContext* cx) override;
+  bool enqueuePromiseJob(JSContext* cx, JS::HandleObject promise,
+                         JS::HandleObject job, JS::HandleObject allocationSite,
+                         JS::HandleObject incumbentGlobal) override;
+  void runJobs(JSContext* cx) override;
+  bool empty() const override;
+
+ private:
   // A primary context owns the mRuntime. Non-main-thread contexts should always
   // be primary. On the main thread, the primary context should be the first one
   // created and the last one destroyed. Non-primary contexts are used for
   // cooperatively scheduled threads.
   bool mIsPrimaryContext;
 
   CycleCollectedJSRuntime* mRuntime;