Bug 1539132 - Part 6: Save and restore async-generator state in debugger. r=arai
authorAndré Bargull <andre.bargull@gmail.com>
Wed, 03 Apr 2019 20:03:15 +0000
changeset 467959 e1d21ee0fa85adcacf0497e8e9b4122631241cd8
parent 467958 201c7691d719efc9b544018197c7b43b3a49ca1f
child 467960 a9aed50dd3ebc0c725736bad607845928cfbd60b
push id112667
push useraiakab@mozilla.com
push dateThu, 04 Apr 2019 16:12:45 +0000
treeherdermozilla-inbound@230bb363f2f3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1539132
milestone68.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 1539132 - Part 6: Save and restore async-generator state in debugger. r=arai Differential Revision: https://phabricator.services.mozilla.com/D25574
js/src/jit-test/tests/debug/onEnterFrame-async-resumption-09.js
js/src/jit-test/tests/debug/onEnterFrame-async-resumption-10.js
js/src/vm/AsyncFunction.cpp
js/src/vm/AsyncIteration.h
js/src/vm/Debugger.cpp
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-09.js
@@ -0,0 +1,33 @@
+// Resume execution of async generator when initially yielding.
+
+let g = newGlobal({newCompartment: true});
+let dbg = new Debugger();
+let gw = dbg.addDebuggee(g);
+
+g.eval(`
+    async function* f() {
+        await 123;
+        return "ponies";
+    }
+`);
+
+let counter = 0;
+dbg.onEnterFrame = frame => {
+    frame.onPop = completion => {
+        if (counter++ === 0) {
+            let genObj = completion.return.unsafeDereference();
+
+            genObj.next().then(({value, done}) => {
+                assertEq(value, "ponies");
+                assertEq(done, true);
+            });
+        }
+    };
+};
+
+let it = g.f();
+
+it.next().then(({value, done}) => {
+    assertEq(value, undefined);
+    assertEq(done, true);
+});
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-10.js
@@ -0,0 +1,26 @@
+// |jit-test| --enable-experimental-await-fix
+
+// Resolve async function promise when initially awaiting.
+
+let g = newGlobal({newCompartment: true});
+let dbg = new Debugger();
+let gw = dbg.addDebuggee(g);
+
+g.eval(`
+    var resolve;
+    var p = new Promise(r => {
+        resolve = r;
+    });
+
+    async function f() {
+        await p;
+    }
+`);
+
+dbg.onEnterFrame = frame => {
+    frame.onPop = completion => {
+        g.resolve(0);
+        drainJobQueue();
+    };
+};
+g.f();
--- a/js/src/vm/AsyncFunction.cpp
+++ b/js/src/vm/AsyncFunction.cpp
@@ -74,16 +74,24 @@ static bool AsyncFunctionResume(JSContex
   // of the async function. So when either the debugger or OOM errors terminate
   // the execution after JSOP_ASYNCAWAIT, but before JSOP_AWAIT, we're in an
   // inconsistent state, because we don't have a resume index set and therefore
   // don't know where to resume the async function. Return here in that case.
   if (generator->isClosed()) {
     return true;
   }
 
+  // The debugger sets the async function's generator object into the "running"
+  // state while firing debugger events to ensure the debugger can't re-enter
+  // the async function, cf. |AutoSetGeneratorRunning| in Debugger.cpp. Catch
+  // this case here by checking if the generator is already runnning.
+  if (generator->isRunning()) {
+    return true;
+  }
+
   Rooted<PromiseObject*> resultPromise(cx, generator->promise());
 
   RootedObject stack(cx);
   Maybe<JS::AutoSetAsyncStackForNewCalls> asyncStack;
   if (JSObject* allocationSite = resultPromise->allocationSite()) {
     // The promise is created within the activation of the async function, so
     // use the parent frame as the starting point for async stacks.
     stack = allocationSite->as<SavedFrame>().getParent();
--- a/js/src/vm/AsyncIteration.h
+++ b/js/src/vm/AsyncIteration.h
@@ -111,16 +111,17 @@ class AsyncGeneratorObject : public Abst
 
     // Cached AsyncGeneratorRequest for later use.
     // undefined if there's no cache.
     Slot_CachedRequest,
 
     Slots
   };
 
+ public:
   enum State {
     // "suspendedStart" in the spec.
     // Suspended after invocation.
     State_SuspendedStart,
 
     // "suspendedYield" in the spec
     // Suspended with `yield` expression.
     State_SuspendedYield,
@@ -145,16 +146,17 @@ class AsyncGeneratorObject : public Abst
     State_Completed
   };
 
   State state() const {
     return static_cast<State>(getFixedSlot(Slot_State).toInt32());
   }
   void setState(State state_) { setFixedSlot(Slot_State, Int32Value(state_)); }
 
+ private:
   // Queue is implemented in 2 ways.  If only one request is queued ever,
   // request is stored directly to the slot.  Once 2 requests are queued, a
   // list is created and requests are appended into it, and the list is
   // stored to the slot.
 
   bool isSingleQueue() const {
     return getFixedSlot(Slot_QueueOrRequest).isNull() ||
            getFixedSlot(Slot_QueueOrRequest)
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -948,39 +948,53 @@ static void DebuggerFrame_maybeDecrement
  *
  * When Debugger::slowPathOnLeaveFrame is called for a frame that is yielding
  * or awaiting, its generator is in the "suspended" state. Letting script
  * observe this state, with the generator on stack yet also reenterable, would
  * be bad, so we mark it running while we fire events.
  */
 class MOZ_RAII AutoSetGeneratorRunning {
   int32_t resumeIndex_;
+  AsyncGeneratorObject::State asyncGenState_;
   Rooted<AbstractGeneratorObject*> genObj_;
 
  public:
   AutoSetGeneratorRunning(JSContext* cx,
                           Handle<AbstractGeneratorObject*> genObj)
-      : resumeIndex_(0), genObj_(cx, genObj) {
+      : resumeIndex_(0),
+        asyncGenState_(static_cast<AsyncGeneratorObject::State>(0)),
+        genObj_(cx, genObj) {
     if (genObj) {
       if (!genObj->isClosed() && genObj->isSuspended()) {
         // Yielding or awaiting.
         resumeIndex_ = genObj->resumeIndex();
         genObj->setRunning();
+
+        // Async generators have additionally bookkeeping which must be
+        // adjusted when switching over to the running state.
+        if (genObj->is<AsyncGeneratorObject>()) {
+          auto* asyncGenObj = &genObj->as<AsyncGeneratorObject>();
+          asyncGenState_ = asyncGenObj->state();
+          asyncGenObj->setExecuting();
+        }
       } else {
         // Returning or throwing. The generator is already closed, if
         // it was ever exposed at all.
         genObj_ = nullptr;
       }
     }
   }
 
   ~AutoSetGeneratorRunning() {
     if (genObj_) {
       MOZ_ASSERT(genObj_->isRunning());
       genObj_->setResumeIndex(resumeIndex_);
+      if (genObj_->is<AsyncGeneratorObject>()) {
+        genObj_->as<AsyncGeneratorObject>().setState(asyncGenState_);
+      }
     }
   }
 };
 
 /*
  * Handle leaving a frame with debuggers watching. |frameOk| indicates whether
  * the frame is exiting normally or abruptly. Set |cx|'s exception and/or
  * |cx->fp()|'s return value, and return a new success value.