Bug 1448880 - Part 2: Persist Debugger.Frame objects for generators across yield/await. r=jimb.
authorJason Orendorff <jorendorff@mozilla.com>
Tue, 23 Oct 2018 23:22:00 +0000
changeset 491038 64810935a7516678a1e655b8d1034ac8700b53d7
parent 491037 2f3da01d8816a0117e39eca43cd4c52c0a381a4d
child 491039 9c03b503909a6d03150869a782d5e148870c20f5
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersjimb
bugs1448880
milestone65.0a1
Bug 1448880 - Part 2: Persist Debugger.Frame objects for generators across yield/await. r=jimb. This is the minimal patch, but it leaves two bugs: 1. When a generator or async function is resumed, stepping is reenabled in Debugger::getFrame, which isn't necessarily called. The onStep tests in this patch work because they all use an onEnterFrame hook, which causes getFrame to be called as soon as the generator is resumed. 2. .onStep and .onPop hooks on suspended Frames do not survive GC if there are no other references to the Frame or the Debugger object. The behavior is safe, but the hooks can just mysteriously stop firing when GC happens. The next three patches in this stack lay the groundwork for fixing these bugs, without changing behavior; part 6 fixes the first bug; and part 7 fixes the second. Depends on D6982 Differential Revision: https://phabricator.services.mozilla.com/D6983
js/src/doc/Debugger/Debugger.Frame.md
js/src/jit-test/tests/debug/Debugger-getNewestFrame-generators-01.js
js/src/jit-test/tests/debug/Frame-identity-05.js
js/src/jit-test/tests/debug/Frame-identity-06.js
js/src/jit-test/tests/debug/Frame-identity-07.js
js/src/jit-test/tests/debug/Frame-older-generators-01.js
js/src/jit-test/tests/debug/Frame-older-generators-02.js
js/src/jit-test/tests/debug/Frame-onPop-06.js
js/src/jit-test/tests/debug/Frame-onPop-15.js
js/src/jit-test/tests/debug/Frame-onPop-generators-05.js
js/src/jit-test/tests/debug/Frame-onPop-star-generators-03.js
js/src/jit-test/tests/debug/Frame-onStep-async-01.js
js/src/jit-test/tests/debug/Frame-onStep-async-02.js
js/src/jit-test/tests/debug/Frame-onStep-generators-04.js
js/src/jit-test/tests/debug/onEnterFrame-async-01.js
js/src/jit-test/tests/debug/onEnterFrame-async-resumption-04.js
js/src/jit-test/tests/debug/onEnterFrame-generator-01.js
js/src/jit-test/tests/debug/onEnterFrame-generator-04.js
js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-04.js
js/src/jit-test/tests/debug/onExceptionUnwind-generators-01.js
js/src/vm/Debugger-inl.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
js/src/vm/GeneratorObject.cpp
--- a/js/src/doc/Debugger/Debugger.Frame.md
+++ b/js/src/doc/Debugger/Debugger.Frame.md
@@ -88,16 +88,85 @@ 4. When the debuggee code completes, whe
    debugger.
 
 When a debugger calls an invocation function to run debuggee code, that
 code's continuation is the debugger, not the next debuggee code frame.
 Pushing a `"debugger"` frame makes this continuation explicit, and makes it
 easier to find the extent of the stack created for the invocation.
 
 
+## <span id='suspended'>Suspended</span> Frames
+
+Some frames can be *suspended*.
+
+When a generator `yield`s a value, or when an async function `await`s a
+value, the current frame is suspended and removed from the stack, and
+other JS code has a chance to run. Later (if the `await`ed promise
+becomes resolved, for example), SpiderMonkey will *resume* the frame. It
+will be put back onto the stack, and execution will continue where it
+left off. Only generator and async function call frames can be suspended
+and resumed.
+
+Currently, a frame's `live` property is `false` while it's suspended
+([bug 1448880](https://bugzilla.mozilla.org/show_bug.cgi?id=1448880)).
+
+SpiderMonkey uses the same `Debugger.Frame` object each time a generator
+or async function call is put back onto the stack. This means that the
+`onStep` handler can be used to step over `yield` and `await`.
+
+The `frame.onPop` handler is called each time a frame is suspended, and
+the `Debugger.onEnterFrame` handler is called each time a frame is
+resumed. (This means these events can fire multiple times for the same
+`Frame` object, which is odd, but accurately conveys what's happening.)
+
+
+## Stepping Into Generators: The "Initial Yield"
+
+When a debuggee generator is called, something weird happens. The
+`.onEnterFrame` hook fires, as though we're stepping into the generator.
+But the code inside the generator doesn't run. Instead it immediately
+returns. Then we sometimes get *another* `.onEnterFrame` event for the
+same generator. What's going on?
+
+To explain this, we first have to describe how generator calls work,
+according to the ECMAScript language specification. Note that except for
+step 3, it's exactly like a regular function call.
+
+1.  An "execution context" (what we call a `Frame`) is pushed to the stack.
+2.  An environment is created (for arguments and local variables).
+    Argument-default-value-expressions, if any, are evaluated.
+3.  A generator object is created, initially suspended at the start of the generator body.
+4.  The stack frame is popped, and the generator object is returned to the caller.
+
+The JavaScript engine actually carries out these steps, in this order.
+So when a debuggee generator is called, here's what you'll observe:
+
+1.  The `debugger.onEnterFrame` hook fires.
+2.  The debugger can step through the argument-default-value code, if any.
+3.  The body of the generator does not run yet. Instead, a generator object
+    is created and suspended (which does not fire any debugger events).
+4.  The `frame.onPop` hook fires, with a completion value of
+    `{return:` *(the new generator object)* `}`.
+
+In SpiderMonkey, this process of suspending and returning a new
+generator object is called the "initial yield".
+
+If the caller then uses the generator's `.next()` method, which may or
+may not happen right away depending on the debuggee code, the suspended
+generator will be resumed, firing `.onEnterFrame` again.
+
+**Stepping into async functions** — SpiderMonkey also performs an
+initial yield for async functions. This is not so easy to defend by
+citing the spec; we're just exposing internal SpiderMonkey
+implementation details. When the initial yield happens, you'll observe
+an extra `.onPop` returning a generator object that SpiderMonkey will
+use internally, followed immediately by a second `.onEnterFrame` event
+for the same frame.
+
+
 ## Accessor Properties of the Debugger.Frame Prototype Object
 
 A `Debugger.Frame` instance inherits the following accessor properties from
 its prototype:
 
 `type`
 :   A string describing what sort of frame this is:
 
@@ -259,16 +328,21 @@ the compartment to which the handler met
     handlers set, their handlers are run in an unspecified order. The
     resumption value each handler returns establishes the completion value
     reported to the next handler.
 
     This handler is not called on `"debugger"` frames. It is also not called
     when unwinding a frame due to an over-recursion or out-of-memory
     exception.
 
+    The `onPop` handler is typically called only once for a given frame,
+    after which the frame becomes inactive. However, in the case of
+    [generators and async functions](suspended), `onPop` fires each time
+    the frame is suspended.
+
 
 ## Function Properties of the Debugger.Frame Prototype Object
 
 The functions described below may only be called with a `this` value
 referring to a `Debugger.Frame` instance; they may not be used as
 methods of other kinds of objects.
 
 <code id="eval">eval(<i>code</i>, [<i>options</i>])</code>
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Debugger-getNewestFrame-generators-01.js
@@ -0,0 +1,49 @@
+// Generator/async frames can be created and revived by calling Debugger.getNewestFrame().
+//
+// Modified copy of Frame-older-generators-01.js.
+
+let g = newGlobal();
+g.eval(`
+    function* gen() {
+        f();
+        yield 1;
+        f();
+    }
+    function* genDefaults(x=f()) {
+        f();
+    }
+    async function af() {
+        f();
+        await 1;
+        f();
+    }
+    async function afDefaults(x=f()) {
+        await 1;
+        f();
+    }
+`);
+
+function test(expected, code) {
+    let dbg = Debugger(g);
+    let hits = 0;
+    let genFrame = null;
+    g.f = () => {
+        hits++;
+        let frame = dbg.getNewestFrame();
+        if (genFrame === null) {
+            genFrame = frame;
+        } else {
+            assertEq(frame, genFrame);
+        }
+        assertEq(genFrame.callee.name, expected);
+    }
+
+    g.eval(code);
+    assertEq(hits, 2);
+    dbg.removeDebuggee(g);
+}
+
+test("gen", "for (var x of gen()) {}");
+test("genDefaults", "for (var x of genDefaults()) {}");
+test("af", "af(); drainJobQueue();");
+test("afDefaults", "afDefaults(); drainJobQueue();")
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-identity-05.js
@@ -0,0 +1,20 @@
+// Suspended generators keep their associated Debugger.Frames gc-alive.
+
+var g = newGlobal();
+g.eval("function* f() { debugger; yield 1; debugger; }");
+var dbg = new Debugger(g);
+var hits = 0;
+dbg.onDebuggerStatement = function (frame) {
+    if (hits === 0)
+        frame.seen = true;
+    else
+        assertEq(frame.seen, true);
+    gc();
+    hits++;
+};
+var it = g.f();
+gc();
+assertEq(it.next().value, 1);
+gc();
+assertEq(it.next().done, true);
+assertEq(hits, 2);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-identity-06.js
@@ -0,0 +1,45 @@
+// Debugger.Frames for async functions are not GC'd while they're suspended.
+// The awaited promise keeps the generator alive, via its reaction lists.
+
+var g = newGlobal();
+g.eval(`
+    // Create a few promises.
+    var promises = [], resolvers = [];
+    for (let i = 0; i < 3; i++)
+        promises.push(new Promise(r => { resolvers.push(r); }));
+
+    async function f() {
+        debugger;
+        for (let p of promises) {
+            await p;
+            debugger;
+        }
+    }
+`);
+var dbg = new Debugger(g);
+var hits = 0;
+dbg.onDebuggerStatement = function (frame) {
+    if (hits === 0)
+        frame.seen = true;
+    else
+        assertEq(frame.seen, true);
+    hits++;
+};
+
+let done = false;
+g.f().then(_ => { done = true; });
+gc();
+drainJobQueue();
+gc();
+
+// Resolve the promises one by one.
+for (let [i, resolve] of g.resolvers.entries()) {
+    assertEq(hits, 1 + i);
+    assertEq(done, false);
+    resolve("x");
+    gc();
+    drainJobQueue();
+    gc();
+}
+assertEq(hits, 1 + g.resolvers.length);
+assertEq(done, true);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-identity-07.js
@@ -0,0 +1,52 @@
+// Distinct generator calls result in distinct Debugger.Frames.
+
+let g = newGlobal();
+g.eval(`
+    function* count(n) {
+        if (n > 0) {
+            for (let x of count(n - 1))
+                yield x;
+            yield n;
+        }
+    }
+`);
+
+let log = "";
+let dbg = Debugger(g);
+let nextId = 0;
+function mark(frame) {
+    if (frame.id === undefined)
+        frame.id = nextId++;
+}
+dbg.onEnterFrame = frame => {
+    mark(frame);
+    log += frame.id + "[";
+    frame.onPop = completion => {
+        mark(frame);
+        log += "]" + frame.id;
+    };
+};
+
+
+let j = 0;
+for (let k of g.count(5)) {
+    assertEq(k, ++j);
+    log += " ";
+}
+
+assertEq(log,
+         // Calling a generator function just returns a generator object
+         // without running the body at all; hence "0[]0". However, this call
+         // does evaluate default argument values, if any, so we do report an
+         // onEnterFrame / onPop for it.
+         "0[]0" +
+         // Demanding the first value from the top generator forces
+         // SpiderMonkey to create all five generator objects (the empty "n[]n"
+         // pairs) and then demand a value from them (the longer "n[...]n"
+         // substrings).
+         "0[1[]11[2[]22[3[]33[4[]44[5[]55[]5]4]3]2]1]0 " +
+         "0[1[2[3[4[]4]3]2]1]0 " +
+         "0[1[2[3[]3]2]1]0 " +
+         "0[1[2[]2]1]0 " +
+         "0[1[]1]0 " +
+         "0[]0");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-older-generators-01.js
@@ -0,0 +1,53 @@
+// Generator/async frames can be created by following .older.
+//
+// The goal here is to get some test coverage creating generator Frame objects
+// at some time other than when firing onEnterFrame. Here they're created after
+// the initial yield.
+
+let g = newGlobal();
+g.eval(`
+    function f() {
+        debugger;
+    }
+    function* gen() {
+        f();
+        yield 1;
+        f();
+    }
+    function* genDefaults(x=f()) {
+        f();
+    }
+    async function af() {
+        f();
+        await 1;
+        f();
+    }
+    async function afDefaults(x=f()) {
+        await 1;
+        f();
+    }
+`);
+
+function test(expected, code) {
+    let dbg = Debugger(g);
+    let hits = 0;
+    let genFrame = null;
+    dbg.onDebuggerStatement = frame => {
+        hits++;
+        assertEq(frame.callee.name, "f");
+        if (genFrame === null) {
+            genFrame = frame.older;
+        } else {
+            assertEq(frame.older, genFrame);
+        }
+        assertEq(genFrame.callee.name, expected);
+    };
+    g.eval(code);
+    assertEq(hits, 2);
+    dbg.removeDebuggee(g);
+}
+
+test("gen", "for (var x of gen()) {}");
+test("genDefaults", "for (var x of genDefaults()) {}");
+test("af", "af(); drainJobQueue();");
+test("afDefaults", "afDefaults(); drainJobQueue();")
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-older-generators-02.js
@@ -0,0 +1,50 @@
+// Like Frame-older-generators-01.js, but attach the debugger on the fly.
+//
+// (That is, check that it works even if the debugger never received
+// onNewGenerator for the generator, because it wasn't attached at the time.)
+
+let g = newGlobal();
+g.eval(`
+    function f() {
+        attach();
+        debugger;
+    }
+    function* gen() {
+        f();
+        yield 1;
+        f();
+    }
+    async function af() {
+        f();
+        await 1;
+        f();
+    }
+`);
+
+function test(expected, code) {
+    let dbg;
+    let hits = 0;
+    let genFrame = null;
+
+    g.attach = () => {
+        if (dbg === undefined) {
+            dbg = Debugger(g);
+            dbg.onDebuggerStatement = frame => {
+                hits++;
+                assertEq(frame.callee.name, "f");
+                if (genFrame === null) {
+                    genFrame = frame.older;
+                } else {
+                    assertEq(frame.older, genFrame);
+                }
+                assertEq(genFrame.callee.name, expected);
+            };
+        }
+    };
+    g.eval(code);
+    assertEq(hits, 2);
+    dbg.removeDebuggee(g);
+}
+
+test("gen", "for (var x of gen()) {}");
+test("af", "af(); drainJobQueue();");
--- a/js/src/jit-test/tests/debug/Frame-onPop-06.js
+++ b/js/src/jit-test/tests/debug/Frame-onPop-06.js
@@ -1,9 +1,10 @@
 // dbg.getNewestFrame in an onPop handler returns the frame being popped.
+
 var g = newGlobal();
 g.eval("function f() { debugger; }");
 g.eval("function g() { f(); }");
 g.eval("function h() { g(); }");
 g.eval("function i() { h(); }");
 
 var dbg = new Debugger(g);
 var log;
--- a/js/src/jit-test/tests/debug/Frame-onPop-15.js
+++ b/js/src/jit-test/tests/debug/Frame-onPop-15.js
@@ -1,32 +1,31 @@
-// Each resumption of a generator gets a fresh frame, whose onPop handler
-// fires the next time the generator yields.
-// This is not the behavior the spec requests, but it's what we do for the
-// moment, and it's good to check that at least we don't crash.
+// Each resumption of a generator gets the same Frame; its onPop handler
+// fires each time the generator yields.
+
 var g = newGlobal();
 var dbg = new Debugger(g);
 var log;
 
-var debuggerFrames = [];
-var poppedFrames = [];
+var seenFrame = null;
 dbg.onDebuggerStatement = function handleDebugger(frame) {
     log += 'd';
     assertEq(frame.type, "call");
 
-    assertEq(debuggerFrames.indexOf(frame), -1);
-    assertEq(poppedFrames.indexOf(frame), -1);
-    debuggerFrames.push(frame);
+    if (seenFrame === null) {
+        seenFrame = frame;
+    } else {
+        assertEq(seenFrame, frame);
+    }
 
-    if (frame.eval('i').return % 3 == 0) {
+    let i = frame.eval('i').return;
+    if (i % 3 == 0) {
         frame.onPop = function handlePop(c) {
-            log += ')' + c.return.unsafeDereference().value;
-            assertEq(debuggerFrames.indexOf(this) != -1, true);
-            assertEq(poppedFrames.indexOf(this), -1);
-            poppedFrames.push(this);
+            assertEq(this, seenFrame);
+            log += ')' + i;
         };
     }
 };
 
 g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }");
 log ='';
 assertEq(g.eval("var t = 0; for (j of g()) t += j; t;"), 45);
-assertEq(log, "d)0ddd)3ddd)6ddd)9");
+assertEq(log, "d)0d)0d)0d)3d)3d)3d)6d)6d)6d)9");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onPop-generators-05.js
@@ -0,0 +1,29 @@
+// In .onPop for the "initial yield" of a generator, while the generator frame
+// is on the stack, the generator object's .next() method throws.
+
+let g = newGlobal();
+g.eval(`
+    function* f() {
+        return "ok";
+    }
+`);
+
+let hits = 0;
+let dbg = new Debugger;
+let gw = dbg.addDebuggee(g);
+dbg.onEnterFrame = frame => {
+    dbg.onEnterFrame = undefined;  // Trigger only once.
+    frame.onPop = completion => {
+        // Initial yield.
+        let genObj = completion.return;
+        assertEq(genObj.class, "Generator");
+        let result = frame.evalWithBindings("genObj.next()", {genObj});
+        assertEq(result.throw.class, "Error");
+        assertEq(result.throw.getProperty("message").return,
+                 "already executing generator");
+        hits++;
+    };
+};
+
+g.f();
+assertEq(hits, 1);
deleted file mode 100644
--- a/js/src/jit-test/tests/debug/Frame-onPop-star-generators-03.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// Each resumption of an ES6 generator gets a fresh frame, whose onPop
-// handler fires the next time the generator yields.  This is not the
-// behavior the spec requests, but it's what we do for the moment, and
-// it's good to check that at least we don't crash.
-
-load(libdir + 'iteration.js');
-
-var g = newGlobal();
-var dbg = new Debugger(g);
-var log;
-
-var debuggerFrames = [];
-var poppedFrames = [];
-dbg.onDebuggerStatement = function handleDebugger(frame) {
-    log += 'd';
-    assertEq(frame.type, "call");
-
-    assertEq(debuggerFrames.indexOf(frame), -1);
-    assertEq(poppedFrames.indexOf(frame), -1);
-    debuggerFrames.push(frame);
-
-    if (frame.eval('i').return % 3 == 0) {
-        frame.onPop = function handlePop(c) {
-            log += ')' + c.return.value;
-            assertEq(debuggerFrames.indexOf(this) != -1, true);
-            assertEq(poppedFrames.indexOf(this), -1);
-            poppedFrames.push(this);
-        };
-    }
-};
-
-g.eval("function* g() { for (var i = 0; i < 10; i++) { debugger; yield i; } }");
-log ='';
-g.eval("var t = 0, iter = g();");
-for (var j = 0; j < 10; j++)
-    g.eval("t += iter.next().value;");
-assertIteratorResult(g.eval("iter.next()"), undefined, true);
-assertEq(g.eval("t"), 45);
-
-// FIXME: Should equal this, but see bug 917809.
-// assertEq(log, "d)0ddd)3ddd)6ddd)9");
-assertEq(log, "d)undefinedddd)undefinedddd)undefinedddd)undefined");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-async-01.js
@@ -0,0 +1,34 @@
+// Stepping works across `await` in async functions.
+
+// Set up debuggee.
+var g = newGlobal();
+g.log = "";
+g.eval(`                              // line 1
+async function aloop() {              // 2
+    for (let i = 0; i < 3; i++) {     // 3
+        await i;                      // 4
+        log += " ";                   // 5
+    }                                 // 6
+    log += "^";                       // 7
+}
+`);
+
+// Set up debugger.
+let previousLine = -1;
+let dbg = new Debugger(g);
+dbg.onEnterFrame = frame => {
+    frame.onStep = function () {
+        assertEq(this, frame);
+        let line = frame.script.getOffsetLocation(frame.offset).lineNumber;
+        if (previousLine != line) {
+            g.log += line; // We stepped to a new line.
+            previousLine = line;
+        }
+    };
+};
+
+// Run.
+g.aloop();
+drainJobQueue();
+
+assertEq(g.log, "2345 345 345 37^");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-async-02.js
@@ -0,0 +1,85 @@
+// With enough hackery, stepping in and out of async functions can be made to
+// work as users expect.
+//
+// This test exercises the common case when we have syntactically `await
+// $ASYNC_FN($ARGS)` so that the calls nest as if they were synchronous
+// calls. It works, but there's a problem.
+//
+// onStep fires in extra places that end users would find very confusing--see
+// the comment marked (!) below. As a result, Debugger API consumers must do
+// some extra work to skip pausing there. This test is a proof of concept that
+// shows what sort of effort is needed. It maintains a single `asyncStack` and
+// skips the onStep hook if we're not running the function at top of the async
+// stack. Real debuggers would have to maintain multiple async stacks.
+
+// Set up debuggee.
+var g = newGlobal();
+g.eval(`\
+async function outer() {                                // line 1
+    return (await inner()) + (await inner()) + "!";     // 2
+}                                                       // 3
+async function inner() {                                // 4
+    return (await leaf()) + (await leaf());             // 5
+}                                                       // 6
+async function leaf() {                                 // 7
+    return (await Promise.resolve("m"));                // 8
+}                                                       // 9
+`);
+
+// Set up debugger.
+let previousLine = -1;
+let dbg = new Debugger(g);
+let log = "";
+let asyncStack = [];
+
+dbg.onEnterFrame = frame => {
+    assertEq(frame.type, "call");
+
+    // If we're entering this frame for the first time, push it to the async
+    // stack.
+    if (!frame.seen) {
+        frame.seen = true;
+        asyncStack.push(frame);
+        log += "(";
+    }
+
+    frame.onStep = () => {
+        // When stepping, we sometimes pause at opcodes in older frames (!)
+        // where all that's happening is async function administrivia.
+        //
+        // For example, the first time `leaf()` yields, `inner()` and
+        // `outer()` are still on the stack; they haven't awaited yet because
+        // control has not returned from `leaf()` to them yet. So stepping will
+        // hop from line 8 to line 5 to line 2 as we unwind the stack, then
+        // resume on line 8.
+        //
+        // Anyway: skip that noise.
+        if (frame !== asyncStack[asyncStack.length - 1])
+            return;
+
+        let line = frame.script.getOffsetLocation(frame.offset).lineNumber;
+        if (previousLine != line) {
+            log += line; // We stepped to a new line.
+            previousLine = line;
+        }
+    };
+
+    frame.onPop = completion => {
+        // Popping the frame. But async function frames are popped multiple
+        // times: for the "initial suspend", at each await, and on return. The
+        // debugger offers no easy way to distinguish them (bug 1470558).
+        if (typeof completion.return === "string") {
+            // Returning (not awaiting or at initial suspend).
+            assertEq(asyncStack.pop(), frame);
+            log += ")";
+        }
+    };
+};
+
+// Run.
+let result;
+g.outer().then(v => { result = v; });
+drainJobQueue();
+
+assertEq(result, "mmmm!");
+assertEq(log, "(12(45(789)5(789)56)2(45(789)5(789)56)23)");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-onStep-generators-04.js
@@ -0,0 +1,35 @@
+// Stepping works across `yield` in generators.
+
+// Set up debuggee.
+var g = newGlobal();
+g.log = "";
+g.eval(`
+function* range(stop) {               // line 2
+    for (let i = 0; i < stop; i++) {  // 3
+        yield i;                      // 4
+        log += " ";                   // 5
+    }                                 // 6
+    log += "^";                       // 7
+}
+`);
+
+// Set up debugger.
+let previousLine = -1;
+let dbg = new Debugger(g);
+dbg.onEnterFrame = frame => {
+    frame.onStep = function () {
+        assertEq(this, frame);
+        let line = frame.script.getOffsetLocation(frame.offset).lineNumber;
+        if (previousLine != line) {
+            g.log += line; // We stepped to a new line.
+            previousLine = line;
+        }
+    };
+};
+
+// Run.
+for (let value of g.range(3)) {
+    g.log += "*";
+}
+
+assertEq(g.log, "234*5 34*5 34*5 37^");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-async-01.js
@@ -0,0 +1,33 @@
+// async functions fire onEnterFrame each time they resume, like generators
+
+let g = newGlobal();
+g.eval(`
+    async function timeout(n) {
+        for (let i = 0; i < n; i++) {
+            await Promise.resolve(i);
+        }
+    }
+    async function job() {
+        let racer = timeout(5);
+        await timeout(3);
+        await racer;
+    }
+`);
+
+let dbg = Debugger(g);
+let log = "";
+let nicknames = ["job", "t5", "t3"];
+dbg.onEnterFrame = frame => {
+    if (!("nickname" in frame))
+        frame.nickname = nicknames.shift() || "FAIL";
+    log += "(" + frame.nickname;
+    frame.onPop = completion => { log += ")"; };
+};
+
+g.job();
+drainJobQueue();
+assertEq(log,
+         "(job)(job(t5)(t5)(t3)(t3))" +
+         "(t5)(t3)".repeat(3) + "(job)" +
+         "(t5)(t5)(job)");
+
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-async-resumption-04.js
@@ -0,0 +1,36 @@
+// Returning {throw:} from onEnterFrame when resuming inside a try block in an
+// async function causes control to jump to the catch block.
+
+let g = newGlobal();
+g.eval(`
+    async function af() {
+        try {
+            return await Promise.resolve("fail");
+        } catch (exc) {
+            assertEq(exc, "fit");
+            return "ok";
+        }
+    }
+`)
+
+let dbg = new Debugger(g);
+dbg.onEnterFrame = frame => {
+    if (!("hits" in frame)) {
+        frame.hits = 1;
+    } else if (++frame.hits == 3) {
+        // First two hits happen when g.af() is called;
+        // third hit is resuming at the `await` inside the try block.
+        return {throw: "fit"};
+    }
+};
+
+let p = g.af();
+let hits = 0;
+p.then(value => {
+    result = value;
+    hits++;
+});
+drainJobQueue();
+assertEq(hits, 1);
+assertEq(result, "ok");
+
--- a/js/src/jit-test/tests/debug/onEnterFrame-generator-01.js
+++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-01.js
@@ -19,19 +19,21 @@ let savedOffsets = new Set;
 
 function check(frame) {
     assertEq(frame.type, "call");
     assertEq(frame.constructing, false);
     assertEq(frame.callee, gw.makeDebuggeeValue(g.gen));
 
     // `arguments` elements don't work in resumed generator frames,
     // because generators don't keep the arguments around.
-    // The first onEnterFrame and onPop events can see them.
-    assertEq(frame.arguments.length, hits < 2 ? args.length : 0);
-    for (var i = 0; i < frame.arguments.length; i++) {
+    // However, some of this is initialized when the frame.arguments object is
+    // created, so if they are created during the first onEnterFrame or onPop
+    // event, the properties exist, and those events can also see the values.
+    assertEq(frame.arguments.length, args.length);
+    for (var i = 0; i < args.length; i++) {
         assertEq(frame.arguments.hasOwnProperty(i), true);
 
         if (hits < 2)
             assertEq(frame.arguments[i], gw.makeDebuggeeValue(args[i]), `arguments[${i}]`);
         else
             assertEq(frame.arguments[i], undefined);
     }
 
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-04.js
@@ -0,0 +1,44 @@
+// When a generator frame is resumed, the onEnterFrame fires again.
+// The same Frame object is passed.
+
+let g = newGlobal();
+g.eval(`
+    function* easyMode() {}
+
+    function* f() { yield* "XYZ"; }
+    function* hardMode() {
+        for (let c1 of "AB")
+            for (let c2 of f())
+                yield c1 + c2;
+    }
+`);
+
+function test(mode, expected) {
+    let dbg = new Debugger(g);
+    let nextid = 1;
+    dbg.onEnterFrame = frame => {
+        if (frame.type == "call") {
+            if (!("id" in frame))
+                frame.id = nextid++;
+            g.log += frame.id + "(";
+
+            frame.onPop = rv => {
+                g.log += ")";
+            };
+        }
+    };
+
+    g.log = "";
+    g.eval(`
+        for (let x of ${mode}())
+            log += x;
+    `);
+    assertEq(g.log, expected);
+    dbg.enabled = false;
+}
+
+// We fire onEnterFrame for the initial activation when a generator is first
+// called, even though the initial yield happens before any body code. This is
+// weird but at least it's consistent.
+test("easyMode", "1()1()");
+test("hardMode", "1()1(2()2())AX1(2())AY1(2())AZ1(2()3()3())BX1(3())BY1(3())BZ1(3())");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onEnterFrame-generator-resumption-04.js
@@ -0,0 +1,34 @@
+// Returning {throw:} from onEnterFrame when resuming inside a try block in a
+// generator causes control to jump to the finally block.
+
+let g = newGlobal();
+g.eval(`
+    function* gen() {
+        try {
+            yield 0;
+            return "fail";
+        } finally {
+            return "ok"; // preempts exception unwinding
+        }
+    }
+`)
+
+let dbg = new Debugger(g);
+dbg.onEnterFrame = frame => {
+    if (!("hits" in frame)) {
+        frame.hits = 1;
+    } else if (++frame.hits == 3) {
+        // First hit is when calling gen();
+        // second hit is resuming at the implicit initial yield;
+        // third hit is resuming inside the try block.
+        return {throw: "fit"};
+    }
+};
+
+let it = g.gen();
+let result = it.next();
+assertEq(result.done, false);
+assertEq(result.value, 0);
+result = it.next();
+assertEq(result.done, true);
+assertEq(result.value, "ok");
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/onExceptionUnwind-generators-01.js
@@ -0,0 +1,39 @@
+// Generator/async frames can be created and revived by the onExceptionUnwind hook.
+//
+// Modified copy of Frame-older-generators-01.js.
+
+let g = newGlobal();
+g.eval(`
+    function* gen() {
+        try { throw new Error("bad"); } catch { }
+        yield 1;
+        try { throw new Error("bad"); } catch { }
+    }
+    async function af() {
+        try { throw new Error("bad"); } catch { }
+        await 1;
+        try { throw new Error("bad"); } catch { }
+    }
+`);
+
+function test(expected, code) {
+    let dbg = Debugger(g);
+    let hits = 0;
+    let genFrame = null;
+    dbg.onExceptionUnwind = frame => {
+        hits++;
+        if (genFrame === null) {
+            genFrame = frame;
+        } else {
+            assertEq(frame, genFrame);
+        }
+        assertEq(genFrame.callee.name, expected);
+    }
+
+    g.eval(code);
+    assertEq(hits, 2);
+    dbg.removeDebuggee(g);
+}
+
+test("gen", "for (var x of gen()) {}");
+test("af", "af(); drainJobQueue();");
--- a/js/src/vm/Debugger-inl.h
+++ b/js/src/vm/Debugger-inl.h
@@ -23,16 +23,26 @@ js::Debugger::onLeaveFrame(JSContext* cx
     MOZ_ASSERT_IF(evalTraps, frame.isDebuggee());
     if (frame.isDebuggee()) {
         ok = slowPathOnLeaveFrame(cx, frame, pc, ok);
     }
     MOZ_ASSERT(!inFrameMaps(frame));
     return ok;
 }
 
+/* static */ inline bool
+js::Debugger::onNewGenerator(JSContext* cx, AbstractFramePtr frame,
+                             Handle<GeneratorObject*> genObj)
+{
+    if (frame.isDebuggee()) {
+        return slowPathOnNewGenerator(cx, frame, genObj);
+    }
+    return true;
+}
+
 /* static */ inline js::Debugger*
 js::Debugger::fromJSObject(const JSObject* obj)
 {
     MOZ_ASSERT(obj->getClass() == &class_);
     return (Debugger*) obj->as<NativeObject>().getPrivate();
 }
 
 /* static */ inline bool
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -697,16 +697,17 @@ Debugger::Debugger(JSContext* cx, Native
     collectCoverageInfo(false),
     observedGCs(cx->zone()),
     allocationsLog(cx),
     trackingAllocationSites(false),
     allocationSamplingProbability(1.0),
     maxAllocationsLogLength(DEFAULT_MAX_LOG_LENGTH),
     allocationsLogOverflowed(false),
     frames(cx->zone()),
+    generatorFrames(cx),
     scripts(cx),
     lazyScripts(cx),
     sources(cx),
     objects(cx),
     environments(cx),
     wasmInstanceScripts(cx),
     wasmInstanceSources(cx),
 #ifdef NIGHTLY_BUILD
@@ -796,39 +797,118 @@ Debugger::getFrame(JSContext* cx, const 
     MOZ_ASSERT_IF(referent.hasScript(), !referent.script()->selfHosted());
 
     if (referent.hasScript() && !referent.script()->ensureHasAnalyzedArgsUsage(cx)) {
         return false;
     }
 
     FrameMap::AddPtr p = frames.lookupForAdd(referent);
     if (!p) {
-        // Create and populate the Debugger.Frame object.
-        RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
-        RootedNativeObject debugger(cx, object);
-
-        RootedDebuggerFrame frame(cx, DebuggerFrame::create(cx, proto, iter, debugger));
+        RootedDebuggerFrame frame(cx);
+
+        // If this is a generator frame, there may be an existing
+        // Debugger.Frame object that isn't in `frames` because the generator
+        // was suspended, popping the stack frame, and later resumed.
+        Rooted<GeneratorObject*> genObj(cx);
+        GeneratorWeakMap::AddPtr gp;
+        if (referent.isFunctionFrame() && (referent.callee()->isGenerator() ||
+                                           referent.callee()->isAsync()))
+        {
+            {
+                AutoRealm ar(cx, referent.callee());
+                genObj = GetGeneratorObjectForFrame(cx, referent);
+            }
+            if (genObj) {
+                gp = generatorFrames.lookupForAdd(genObj);
+                if (gp) {
+                    frame = &gp->value()->as<DebuggerFrame>();
+
+                    // We have found an existing Debugger.Frame object. But
+                    // since it was previously popped (see comment above), it
+                    // is not currently "live". We must revive it.
+                    MOZ_ASSERT(!frame->isLive());
+                    FrameIter::Data* data = iter.copyData();
+                    if (!data) {
+                        return false;
+                    }
+                    frame->setPrivate(data);
+
+                    if (!ensureExecutionObservabilityOfFrame(cx, referent)) {
+                        return false;
+                    }
+
+                    RootedValue onStep(cx, frame->getReservedSlot(JSSLOT_DEBUGFRAME_ONSTEP_HANDLER));
+                    if (!onStep.isUndefined()) {
+                        AutoRealm ar(cx, genObj);
+                        if (!referent.script()->incrementStepModeCount(cx)) {
+                            return false;
+                        }
+                    }
+                }
+            }
+
+            // If no GeneratorObject exists yet, we create a Debugger.Frame
+            // below anyway, and Debugger::onNewGenerator() will associate it
+            // with the GeneratorObject later when we hit JSOP_GENERATOR.
+        }
+
         if (!frame) {
-            return false;
-        }
-
-        if (!ensureExecutionObservabilityOfFrame(cx, referent)) {
-            return false;
+            // Create and populate the Debugger.Frame object.
+            RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_FRAME_PROTO).toObject());
+            RootedNativeObject debugger(cx, object);
+
+            frame = DebuggerFrame::create(cx, proto, iter, debugger);
+            if (!frame) {
+                return false;
+            }
+
+            if (!ensureExecutionObservabilityOfFrame(cx, referent)) {
+                return false;
+            }
+
+            if (genObj) {
+                DebuggerFrame* frameObj = frame;
+                if (!generatorFrames.relookupOrAdd(gp, genObj, frameObj)) {
+                    ReportOutOfMemory(cx);
+                    return false;
+                }
+            }
         }
 
         if (!frames.add(p, referent, frame)) {
+            NukeDebuggerWrapper(frame);
+            if (genObj) {
+                generatorFrames.remove(genObj);
+            }
             ReportOutOfMemory(cx);
             return false;
         }
     }
 
     result.set(&p->value()->as<DebuggerFrame>());
     return true;
 }
 
+bool
+Debugger::addGeneratorFrame(JSContext* cx,
+                            Handle<GeneratorObject*> genObj,
+                            HandleDebuggerFrame frameObj)
+{
+    GeneratorWeakMap::AddPtr p = generatorFrames.lookupForAdd(genObj);
+    if (p) {
+        MOZ_ASSERT(p->value() == frameObj);
+    } else {
+        if (!generatorFrames.relookupOrAdd(p, genObj, frameObj)) {
+            ReportOutOfMemory(cx);
+            return false;
+        }
+    }
+    return true;
+}
+
 /* static */ bool
 Debugger::hasLiveHook(GlobalObject* global, Hook which)
 {
     if (GlobalObject::DebuggerVector* debuggers = global->getDebuggers()) {
         for (auto p = debuggers->begin(); p != debuggers->end(); p++) {
             Debugger* dbg = *p;
             if (dbg->enabled && dbg->getHook(which)) {
                 return true;
@@ -945,23 +1025,24 @@ class MOZ_RAII AutoSetGeneratorRunning
     Rooted<GeneratorObject*> genObj_;
 
   public:
     AutoSetGeneratorRunning(JSContext* cx, Handle<GeneratorObject*> genObj)
       : yieldAwaitIndex_(0),
         genObj_(cx, genObj)
     {
         if (genObj) {
-            if (!genObj->isBeforeInitialYield() && !genObj->isClosed() && genObj->isSuspended()) {
+            if (!genObj->isClosed() && genObj->isSuspended()) {
+                // Yielding or awaiting.
                 yieldAwaitIndex_ =
                     genObj->getFixedSlot(GeneratorObject::YIELD_AND_AWAIT_INDEX_SLOT).toInt32();
                 genObj->setRunning();
             } else {
-                // We're returning or throwing, not yielding or awaiting. The
-                // generator is already closed, if it was ever exposed at all.
+                // Returning or throwing. The generator is already closed, if
+                // it was ever exposed at all.
                 genObj_ = nullptr;
             }
         }
     }
 
     ~AutoSetGeneratorRunning() {
         if (genObj_) {
             MOZ_ASSERT(genObj_->isRunning());
@@ -1072,16 +1153,45 @@ Debugger::slowPathOnLeaveFrame(JSContext
         MOZ_ASSERT(!cx->isExceptionPending());
         return false;
 
       default:
         MOZ_CRASH("bad final onLeaveFrame resume mode");
     }
 }
 
+/* static */ bool
+Debugger::slowPathOnNewGenerator(JSContext* cx, AbstractFramePtr frame,
+                                 Handle<GeneratorObject*> genObj)
+{
+    // This is called from JSOP_GENERATOR, after default parameter expressions
+    // are evaluated and well after onEnterFrame, so Debugger.Frame objects for
+    // `frame` may already have been exposed to debugger code. The
+    // GeneratorObject for this generator call, though, has just been
+    // created. It must be associated with any existing Debugger.Frames.
+    bool ok = true;
+    forEachDebuggerFrame(frame, [&] (DebuggerFrame* frameObjPtr) {
+        if (!ok) {
+            return;
+        }
+
+        RootedDebuggerFrame frameObj(cx, frameObjPtr);
+        Debugger* dbg = Debugger::fromChildJSObject(frameObj);
+        if (!dbg->addGeneratorFrame(cx, genObj, frameObj)) {
+            ReportOutOfMemory(cx);
+
+            // This leaves `genObj` and `frameObj` unassociated. It's OK
+            // because we won't pause again with this generator on the stack:
+            // the caller will immediately discard `genObj` and unwind `frame`.
+            ok = false;
+        }
+    });
+    return ok;
+}
+
 /* static */ ResumeMode
 Debugger::slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame)
 {
     RootedValue rval(cx);
     ResumeMode resumeMode = dispatchHook(
         cx,
         [](Debugger* dbg) -> bool { return dbg->getHook(OnDebuggerStatement); },
         [&](Debugger* dbg) -> ResumeMode {
@@ -3152,16 +3262,17 @@ Debugger::removeAllocationsTrackingForAl
 
 
 
 /*** Debugger JSObjects **************************************************************************/
 
 void
 Debugger::traceCrossCompartmentEdges(JSTracer* trc)
 {
+    generatorFrames.traceCrossCompartmentEdges<DebuggerFrame_trace>(trc);
     objects.traceCrossCompartmentEdges<DebuggerObject_trace>(trc);
     environments.traceCrossCompartmentEdges<DebuggerEnv_trace>(trc);
     scripts.traceCrossCompartmentEdges<DebuggerScript_trace>(trc);
     lazyScripts.traceCrossCompartmentEdges<DebuggerScript_trace>(trc);
     sources.traceCrossCompartmentEdges<DebuggerSource_trace>(trc);
     wasmInstanceScripts.traceCrossCompartmentEdges<DebuggerScript_trace>(trc);
     wasmInstanceSources.traceCrossCompartmentEdges<DebuggerSource_trace>(trc);
 }
@@ -3338,46 +3449,33 @@ Debugger::trace(JSTracer* trc)
 {
     TraceEdge(trc, &object, "Debugger Object");
 
     TraceNullableEdge(trc, &uncaughtExceptionHook, "hooks");
 
     // Mark Debugger.Frame objects. These are all reachable from JS, because the
     // corresponding JS frames are still on the stack.
     //
-    // (Once we support generator frames properly, we will need
-    // weakly-referenced Debugger.Frame objects as well, for suspended generator
-    // frames.)
+    // (We have weakly-referenced Debugger.Frame objects as well, for suspended
+    // generator frames; these are traced via generatorFrames just below.)
     for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
         HeapPtr<DebuggerFrame*>& frameobj = r.front().value();
         TraceEdge(trc, &frameobj, "live Debugger.Frame");
         MOZ_ASSERT(frameobj->getPrivate(frameobj->numFixedSlotsMaybeForwarded()));
     }
 
     allocationsLog.trace(trc);
 
-    // Trace the weak map from JSScript instances to Debugger.Script objects.
+    generatorFrames.trace(trc);
     scripts.trace(trc);
-
-    // Trace the weak map from LazyScript instances to Debugger.Script objects.
     lazyScripts.trace(trc);
-
-    // Trace the referent -> Debugger.Source weak map
     sources.trace(trc);
-
-    // Trace the referent -> Debugger.Object weak map.
     objects.trace(trc);
-
-    // Trace the referent -> Debugger.Environment weak map.
     environments.trace(trc);
-
-    // Trace the WasmInstanceObject -> synthesized Debugger.Script weak map.
     wasmInstanceScripts.trace(trc);
-
-    // Trace the WasmInstanceObject -> synthesized Debugger.Source weak map.
     wasmInstanceSources.trace(trc);
 }
 
 /* static */ void
 Debugger::sweepAll(FreeOp* fop)
 {
     JSRuntime* rt = fop->runtime();
 
@@ -3434,16 +3532,17 @@ Debugger::findZoneEdges(Zone* zone, js::
                 }
             }
         } else {
             // For debugger cross compartment wrappers, add edges in the
             // opposite direction to those already added by
             // Compartment::findOutgoingEdges and above. This ensure that
             // debuggers and their debuggees are finalized in the same group.
             if (dbg->debuggeeZones.has(zone) ||
+                dbg->generatorFrames.hasKeyInZone(zone) ||
                 dbg->scripts.hasKeyInZone(zone) ||
                 dbg->lazyScripts.hasKeyInZone(zone) ||
                 dbg->sources.hasKeyInZone(zone) ||
                 dbg->objects.hasKeyInZone(zone) ||
                 dbg->environments.hasKeyInZone(zone) ||
                 dbg->wasmInstanceScripts.hasKeyInZone(zone) ||
                 dbg->wasmInstanceSources.hasKeyInZone(zone))
             {
@@ -4348,16 +4447,31 @@ Debugger::removeDebuggeeGlobal(FreeOp* f
         DebuggerFrame* frameobj = e.front().value();
         if (frame.global() == global) {
             frameobj->freeFrameIterData(fop);
             DebuggerFrame_maybeDecrementFrameScriptStepModeCount(fop, frame, frameobj);
             e.removeFront();
         }
     }
 
+    // Clear this global's generators from generatorFrames as well.
+    //
+    // This method can be called either from script (dbg.removeDebuggee) or
+    // from an awkward time during GC sweeping. In the latter case, skip this
+    // loop to avoid touching dead objects. It's correct because, when we're
+    // called from GC, all `global`'s generators are guaranteed to be dying:
+    // live generators would keep the global alive and we wouldn't be here. GC
+    // will sweep dead keys from the weakmap.
+    if (!global->zone()->isGCSweeping()) {
+        generatorFrames.removeIf([global](JSObject* key) {
+            GeneratorObject& genObj = key->as<GeneratorObject>();
+            return genObj.isClosed() || &genObj.callee().global() == global;
+        });
+    }
+
     auto *globalDebuggersVector = global->getDebuggers();
     auto *zoneDebuggersVector = global->zone()->getDebuggers();
 
     // The relation must be removed from up to three places:
     // globalDebuggersVector and debuggees for sure, and possibly the
     // compartment's debuggee set.
     //
     // The debuggee zone set is recomputed on demand. This avoids refcounting
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -18,16 +18,17 @@
 #include "ds/TraceableFifo.h"
 #include "gc/Barrier.h"
 #include "gc/WeakMap.h"
 #include "js/Debug.h"
 #include "js/GCVariant.h"
 #include "js/HashTable.h"
 #include "js/Utility.h"
 #include "js/Wrapper.h"
+#include "vm/GeneratorObject.h"
 #include "vm/GlobalObject.h"
 #include "vm/JSContext.h"
 #include "vm/Realm.h"
 #include "vm/SavedStacks.h"
 #include "vm/Stack.h"
 #include "wasm/WasmJS.h"
 
 namespace js {
@@ -115,19 +116,21 @@ CheckDebuggeeThing(JSObject* obj, bool i
  * and compartments may be unnecessarily grouped, however it results in a
  * simpler and faster implementation.
  *
  * If InvisibleKeysOk is true, then the map can have keys in invisible-to-
  * debugger compartments. If it is false, we assert that such entries are never
  * created.
  *
  * Also note that keys in these weakmaps can be in any compartment, debuggee or
- * not, because they cannot be deleted when a compartment is no longer a
+ * not, because they are not deleted when a compartment is no longer a
  * debuggee: the values need to maintain object identity across add/remove/add
- * transitions.
+ * transitions. (Frames are an exception to the rule. Existing Debugger.Frame
+ * objects are killed when debugging is disabled for their compartment, and if
+ * it's re-enabled later, new Frame objects are created.)
  */
 template <class UnbarrieredKey, bool InvisibleKeysOk=false>
 class DebuggerWeakMap : private WeakMap<HeapPtr<UnbarrieredKey>, HeapPtr<JSObject*>>
 {
   private:
     typedef HeapPtr<UnbarrieredKey> Key;
     typedef HeapPtr<JSObject*> Value;
 
@@ -156,16 +159,17 @@ class DebuggerWeakMap : private WeakMap<
     typedef typename Base::AddPtr AddPtr;
     typedef typename Base::Range Range;
     typedef typename Base::Enum Enum;
     typedef typename Base::Lookup Lookup;
 
     /* Expose WeakMap public interface */
 
     using Base::lookupForAdd;
+    using Base::remove;
     using Base::all;
     using Base::trace;
 
     template<typename KeyInput, typename ValueInput>
     bool relookupOrAdd(AddPtr& p, const KeyInput& k, const ValueInput& v) {
         MOZ_ASSERT(v->compartment() == this->compartment);
 #ifdef DEBUG
         CheckDebuggeeThing(k, InvisibleKeysOk);
@@ -176,22 +180,39 @@ class DebuggerWeakMap : private WeakMap<
         }
         bool ok = Base::relookupOrAdd(p, k, v);
         if (!ok) {
             decZoneCount(k->zone());
         }
         return ok;
     }
 
+    template <typename KeyInput>
+    bool has(const KeyInput& k) const {
+        return !!this->lookup(k);
+    }
+
     void remove(const Lookup& l) {
         MOZ_ASSERT(Base::has(l));
         Base::remove(l);
         decZoneCount(l->zone());
     }
 
+    // Remove entries whose keys satisfy the given predicate.
+    template <typename Predicate>
+    void removeIf(Predicate test) {
+        for (Enum e(*static_cast<Base*>(this)); !e.empty(); e.popFront()) {
+            JSObject* key = e.front().key();
+            if (test(key)) {
+                decZoneCount(key->zoneFromAnyThread());
+                e.removeFront();
+            }
+        }
+    }
+
   public:
     template <void (traceValueEdges)(JSTracer*, JSObject*)>
     void traceCrossCompartmentEdges(JSTracer* tracer) {
         for (Enum e(*static_cast<Base*>(this)); !e.empty(); e.popFront()) {
             traceValueEdges(tracer, e.front().value());
             Key key = e.front().key();
             TraceEdge(tracer, &key, "Debugger WeakMap key");
             if (key != e.front().key()) {
@@ -528,16 +549,39 @@ class Debugger : private mozilla::Linked
      * has to be different.
      */
     typedef HashMap<AbstractFramePtr,
                     HeapPtr<DebuggerFrame*>,
                     DefaultHasher<AbstractFramePtr>,
                     ZoneAllocPolicy> FrameMap;
     FrameMap frames;
 
+    /*
+     * Map from generator objects to their Debugger.Frame instances.
+     *
+     * When a Debugger.Frame is created for a generator frame, it is added to
+     * this map and remains there for the lifetime of the generator, whether
+     * that frame is on the stack at the moment or not.  This is in addition to
+     * the entry in `frames` that exists as long as the generator frame is on
+     * the stack.
+     *
+     * We need to keep the Debugger.Frame object alive to deliver it to the
+     * onEnterFrame handler on resume, and to retain onStep and onPop hooks.
+     *
+     * An entry is present in this table when:
+     * -   both the debuggee generator object and the Debugger.Frame object exist
+     * -   the Debugger.Frame's owner is still an enabled debugger of
+     *     the debuggee compartment
+     * regardless of whether the frame is currently suspended. (This list is
+     * meant to explain why we update the table in the particular places where
+     * we do so.)
+     */
+    typedef DebuggerWeakMap<JSObject*> GeneratorWeakMap;
+    GeneratorWeakMap generatorFrames;
+
     /* An ephemeral map from JSScript* to Debugger.Script instances. */
     typedef DebuggerWeakMap<JSScript*> ScriptWeakMap;
     ScriptWeakMap scripts;
 
     using LazyScriptWeakMap = DebuggerWeakMap<LazyScript*>;
     LazyScriptWeakMap lazyScripts;
 
     using LazyScriptVector = JS::GCVector<LazyScript*>;
@@ -801,16 +845,18 @@ class Debugger : private mozilla::Linked
 
     JSObject* getHook(Hook hook) const;
     bool hasAnyLiveHooks(JSRuntime* rt) const;
 
     static MOZ_MUST_USE bool slowPathCheckNoExecute(JSContext* cx, HandleScript script);
     static ResumeMode slowPathOnEnterFrame(JSContext* cx, AbstractFramePtr frame);
     static MOZ_MUST_USE bool slowPathOnLeaveFrame(JSContext* cx, AbstractFramePtr frame,
                                                   jsbytecode* pc, bool ok);
+    static MOZ_MUST_USE bool slowPathOnNewGenerator(JSContext* cx, AbstractFramePtr frame,
+                                                    Handle<GeneratorObject*> genObj);
     static ResumeMode slowPathOnDebuggerStatement(JSContext* cx, AbstractFramePtr frame);
     static ResumeMode slowPathOnExceptionUnwind(JSContext* cx, AbstractFramePtr frame);
     static void slowPathOnNewScript(JSContext* cx, HandleScript script);
     static void slowPathOnNewWasmInstance(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance);
     static void slowPathOnNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
     static MOZ_MUST_USE bool slowPathOnLogAllocationSite(JSContext* cx, HandleObject obj,
                                                          HandleSavedFrame frame,
                                                          mozilla::TimeStamp when,
@@ -961,16 +1007,25 @@ class Debugger : private mozilla::Linked
      * first call has any effect. (Permitting double calls simplifies some
      * cases where an onPop handler's resumption value changes a return to a
      * throw, or vice versa: we can redirect to a complete copy of the
      * alternative path, containing its own call to onLeaveFrame.)
      */
     static inline MOZ_MUST_USE bool onLeaveFrame(JSContext* cx, AbstractFramePtr frame,
                                                  jsbytecode* pc, bool ok);
 
+    /*
+     * Announce to the debugger that a generator object has been created,
+     * via JSOP_GENERATOR.
+     *
+     * This does not fire user hooks, but it's needed for debugger bookkeeping.
+     */
+    static inline MOZ_MUST_USE bool onNewGenerator(JSContext* cx, AbstractFramePtr frame,
+                                                   Handle<GeneratorObject*> genObj);
+
     static inline void onNewScript(JSContext* cx, HandleScript script);
     static inline void onNewWasmInstance(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance);
     static inline void onNewGlobalObject(JSContext* cx, Handle<GlobalObject*> global);
     static inline MOZ_MUST_USE bool onLogAllocationSite(JSContext* cx, JSObject* obj,
                                                         HandleSavedFrame frame, mozilla::TimeStamp when);
     static ResumeMode onTrap(JSContext* cx, MutableHandleValue vp);
     static ResumeMode onSingleStep(JSContext* cx, MutableHandleValue vp);
     static MOZ_MUST_USE bool handleBaselineOsr(JSContext* cx, InterpreterFrame* from,
@@ -1145,16 +1200,33 @@ class Debugger : private mozilla::Linked
     /*
      * Return the Debugger.Source object for |wasmInstance| (the entire module),
      * synthesizing a new one if needed. The context |cx| must be in the
      * debugger compartment; |wasmInstance| must be a WasmInstanceObject in the
      * debuggee realm.
      */
     JSObject* wrapWasmSource(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance);
 
+    /*
+     * Add a link between the given generator object and a Debugger.Frame
+     * object.  This link is used to make sure the same Debugger.Frame stays
+     * associated with a given generator object (or async function activation),
+     * even while it is suspended and removed from the stack.
+     *
+     * The context `cx` and `frameObj` must be in the debugger realm, and
+     * `genObj` must be in a debuggee realm.
+     *
+     * `frameObj` must be this `Debugger`'s debug wrapper for the generator or
+     * async function call associated with `genObj`. This activation may
+     * or may not actually be on the stack right now.
+     */
+    MOZ_MUST_USE bool addGeneratorFrame(JSContext* cx,
+                                        Handle<GeneratorObject*> genObj,
+                                        HandleDebuggerFrame frameObj);
+
   private:
     Debugger(const Debugger&) = delete;
     Debugger & operator=(const Debugger&) = delete;
 };
 
 enum class DebuggerEnvironmentType {
     Declarative,
     With,
--- a/js/src/vm/GeneratorObject.cpp
+++ b/js/src/vm/GeneratorObject.cpp
@@ -4,16 +4,17 @@
  * 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/GeneratorObject.h"
 
 #include "vm/JSObject.h"
 
 #include "vm/ArrayObject-inl.h"
+#include "vm/Debugger-inl.h"
 #include "vm/JSAtom-inl.h"
 #include "vm/JSScript-inl.h"
 #include "vm/NativeObject-inl.h"
 #include "vm/Stack-inl.h"
 
 using namespace js;
 
 JSObject*
@@ -46,16 +47,20 @@ GeneratorObject::create(JSContext* cx, A
 
     genObj->setCallee(*frame.callee());
     genObj->setEnvironmentChain(*frame.environmentChain());
     if (frame.script()->needsArgsObj()) {
         genObj->setArgsObj(frame.argsObj());
     }
     genObj->clearExpressionStack();
 
+    if (!Debugger::onNewGenerator(cx, frame, genObj)) {
+        return nullptr;
+    }
+
     return genObj;
 }
 
 bool
 GeneratorObject::suspend(JSContext* cx, HandleObject obj, AbstractFramePtr frame, jsbytecode* pc,
                          Value* vp, unsigned nvalues)
 {
     MOZ_ASSERT(*pc == JSOP_INITIALYIELD || *pc == JSOP_YIELD || *pc == JSOP_AWAIT);