Frame-onStep-async-02.js (3373B)
1 // With enough hackery, stepping in and out of async functions can be made to 2 // work as users expect. 3 // 4 // This test exercises the common case when we have syntactically `await 5 // $ASYNC_FN($ARGS)` so that the calls nest as if they were synchronous 6 // calls. It works, but there's a problem. 7 // 8 // onStep fires in extra places that end users would find very confusing--see 9 // the comment marked (!) below. As a result, Debugger API consumers must do 10 // some extra work to skip pausing there. This test is a proof of concept that 11 // shows what sort of effort is needed. It maintains a single `asyncStack` and 12 // skips the onStep hook if we're not running the function at top of the async 13 // stack. Real debuggers would have to maintain multiple async stacks. 14 15 // Set up debuggee. 16 var g = newGlobal({newCompartment: true}); 17 g.eval(`\ 18 async function outer() { // line 1 19 return (await inner()) + (await inner()) + "!"; // 2 20 } // 3 21 async function inner() { // 4 22 return (await leaf()) + (await leaf()); // 5 23 } // 6 24 async function leaf() { // 7 25 return (await Promise.resolve("m")); // 8 26 } // 9 27 `); 28 29 // Set up debugger. 30 let previousLine = -1; 31 let dbg = new Debugger(g); 32 let log = ""; 33 let asyncStack = []; 34 35 dbg.onEnterFrame = frame => { 36 assertEq(frame.type, "call"); 37 38 // If we're entering this frame for the first time, push it to the async 39 // stack. 40 if (!frame.seen) { 41 frame.seen = true; 42 asyncStack.push(frame); 43 log += "("; 44 } 45 46 frame.onStep = () => { 47 // When stepping, we sometimes pause at opcodes in older frames (!) 48 // where all that's happening is async function administrivia. 49 // 50 // For example, the first time `leaf()` yields, `inner()` and 51 // `outer()` are still on the stack; they haven't awaited yet because 52 // control has not returned from `leaf()` to them yet. So stepping will 53 // hop from line 8 to line 5 to line 2 as we unwind the stack, then 54 // resume on line 8. 55 // 56 // Anyway: skip that noise. 57 if (frame !== asyncStack[asyncStack.length - 1]) 58 return; 59 60 let line = frame.script.getOffsetLocation(frame.offset).lineNumber; 61 if (previousLine != line) { 62 log += line; // We stepped to a new line. 63 previousLine = line; 64 } 65 }; 66 67 frame.onPop = completion => { 68 // Popping the frame. But async function frames are popped multiple 69 // times: for the "initial suspend", at each await, and on return. The 70 // debugger offers no easy way to distinguish them (bug 1470558). 71 // For now there's an "await" property, but bug 1470558 may come up 72 // with a different solution, so don't rely on it! 73 if (!completion.await) { 74 // Returning (not awaiting or at initial suspend). 75 assertEq(asyncStack.pop(), frame); 76 log += ")"; 77 } 78 }; 79 }; 80 81 // Run. 82 let result; 83 g.outer().then(v => { result = v; }); 84 drainJobQueue(); 85 86 assertEq(result, "mmmm!"); 87 assertEq(log, "(12(45(789)5(789)56)2(45(789)5(789)56)23)");