Skip to content

Commit 6eceba5

Browse files
committed
Web Inspector: Debugger: add a way to step over await as though it was sync code
https://bugs.webkit.org/show_bug.cgi?id=291309 Reviewed by Yusuke Suzuki. When debugging async code with `await` it's often more desirable to follow what's written in code (i.e. as if the `await` didnt exist) instead of the literal execution flow, especially since it's possible for execution to stop after the `await` if it's the end of sync code. For example, if the developer is trying to step through code similar to the following ```js /* 1 */ async function wrap(callback, state) { /* 2 */ console.log(state, "before"); /* 3 */ await callback(); /* 4 */ console.log(state, "after"); /* 5 */ } /* 6 */ /* 7 */ wrap(foo, 1); /* 8 */ debugger; /* 9 */ wrap(foo, 2); ``` execution should never pause in the flow where `state === 1`, meaning that the debugger will "skip" (i.e. resume and then pause again) between "2 before" and "1 after". Without this, the developer has to manually create breakpoints after each `await` to ensure that execution pauses after, but even then there's no guarantee that will be in the right flow if the same function is invoked multiple times (i.e. one resolves before the other). * Source/JavaScriptCore/bytecode/BytecodeList.rb: * Source/JavaScriptCore/bytecode/BytecodeUseDef.cpp: (JSC::computeUsesForBytecodeIndexImpl): * Source/JavaScriptCore/jit/JITOpcodes.cpp: (JSC::JIT::emit_op_debug): * Source/JavaScriptCore/jit/JITOperations.h: * Source/JavaScriptCore/jit/JITOperations.cpp: (JSC::operationDebug): * Source/JavaScriptCore/llint/LLIntSlowPaths.cpp: (JSC::LLInt::slow_path_debug): Modify `OpDebug` to allow for additional data to be set alongside the `DebugHookType`. Currently this is only used to pass along the `JSGenerator` instance as a way to link execution from before the `await` to after the `await`. Drive-by: Remove the unused `hasBreakpoint` member variable. * Source/JavaScriptCore/interpreter/Interpreter.h: * Source/JavaScriptCore/interpreter/Interpreter.cpp: (JSC::Interpreter::debug): (WTF::printInternal): * Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp: (JSC::dumpExpressionInfoDetails): * Source/JavaScriptCore/debugger/Debugger.h: * Source/JavaScriptCore/debugger/Debugger.cpp: (JSC::Debugger::detach): (JSC::Debugger::pauseIfNeeded): (JSC::Debugger::handlePause): (JSC::Debugger::willAwait): Added. (JSC::Debugger::didAwait): Added. (JSC::Debugger::resetAsyncPauseState): Added. Introduce new `WillAwait` and `DidAwait` debug hooks for the `Debugger` to determine what `await` to pause on. When performing a "Step over" or "Step next", if an `await` is encountered then save the `JSGenerator` instance (which is the additional data set alongside the `DebugHookType`). Once the `await` is done, if the `JSGenerator` matches what was saved then pause at the next opportunity. * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.h: * Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp: (JSC::BytecodeGenerator::emitDebugHook): (JSC::BytecodeGenerator::emitWillLeaveCallFrameDebugHook): (JSC::BytecodeGenerator::emitAwait): (JSC::BytecodeGenerator::emitIteratorGenericNext): (JSC::BytecodeGenerator::emitIteratorGenericClose): (JSC::BytecodeGenerator::emitDelegateYield): * Source/JavaScriptCore/bytecompiler/NodesCodegen.cpp: (JSC::ReturnNode::emitBytecode): (JSC::emitProgramNodeBytecode): (JSC::EvalNode::emitBytecode): (JSC::FunctionNode::emitBytecode): (JSC::AwaitExprNode::emitBytecode): Drive-by: remove unnecessary overrides to make debug hook logic simpler. * Source/WebKitLegacy/mac/WebView/WebScriptDebugger.h: * Source/WebKitLegacy/mac/WebView/WebScriptDebugger.mm: (WebScriptDebugger::handlePause): Drive-by: no need to pass the `ReasonForPause` since it's already exposed via `Debugger::reasonForPause`. * LayoutTests/inspector/debugger/resources/stepping-async.js: Added. * LayoutTests/inspector/debugger/stepping/stepInto-await.html: * LayoutTests/inspector/debugger/stepping/stepInto-await-expected.txt: * LayoutTests/inspector/debugger/stepping/stepNext-await.html: * LayoutTests/inspector/debugger/stepping/stepNext-await-expected.txt: * LayoutTests/inspector/debugger/stepping/stepOut-await.html: * LayoutTests/inspector/debugger/stepping/stepOut-await-expected.txt: * LayoutTests/inspector/debugger/stepping/stepOver-await.html: * LayoutTests/inspector/debugger/stepping/stepOver-await-expected.txt: Canonical link: https://commits.webkit.org/293628@main
1 parent 0bab83a commit 6eceba5

25 files changed

Lines changed: 2120 additions & 69 deletions
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
async function a() { return "a"; }
2+
async function b() { return "b"; }
3+
async function c() { return "c"; }
4+
5+
async function testStatements() {
6+
debugger;
7+
let x = await 1;
8+
let y = await 2;
9+
TestPage.dispatchEventToFrontend("done");
10+
}
11+
12+
async function testFunctions() {
13+
debugger;
14+
let before = await 1;
15+
await a();
16+
let after = await 2;
17+
TestPage.dispatchEventToFrontend("done");
18+
}
19+
20+
async function testEval() {
21+
debugger;
22+
let before = await 1;
23+
await eval("1 + 1");
24+
let after = await 2;
25+
TestPage.dispatchEventToFrontend("done");
26+
}
27+
28+
async function testAnonymousFunction() {
29+
await (async function() {
30+
debugger;
31+
let inner = await 1;
32+
})();
33+
let outer = await 2;
34+
TestPage.dispatchEventToFrontend("done");
35+
}
36+
37+
async function testCommas() {
38+
debugger;
39+
let x = await 1,
40+
y = await 2,
41+
z = await 3;
42+
await a(), await b(), await c();
43+
await true && (await a(), await b(), await c()) && await true;
44+
TestPage.dispatchEventToFrontend("done");
45+
}
46+
47+
async function testChainedExpressions() {
48+
debugger;
49+
await a() && await b() && await c();
50+
TestPage.dispatchEventToFrontend("done");
51+
}
52+
53+
async function testDeclarations() {
54+
debugger;
55+
let x = await a(),
56+
y = await b(),
57+
z = await c();
58+
TestPage.dispatchEventToFrontend("done");
59+
}
60+
61+
async function testInnerFunction() {
62+
async function alpha() {
63+
await beta();
64+
}
65+
async function beta() {
66+
debugger;
67+
}
68+
await alpha();
69+
TestPage.dispatchEventToFrontend("done");
70+
}
71+
72+
async function testFor() {
73+
debugger;
74+
for await (let item of [a(), b()]) {
75+
c();
76+
}
77+
TestPage.dispatchEventToFrontend("done");
78+
}
79+
80+
async function testRepeatedInvocation() {
81+
async function wrap(state) {
82+
if (state === 2)
83+
debugger;
84+
if (state === 1)
85+
await a(); // should not pause on this line
86+
await b();
87+
if (state === 1)
88+
await c(); // should not pause on this line
89+
if (state === 2)
90+
TestPage.dispatchEventToFrontend("done");
91+
}
92+
93+
wrap(1);
94+
wrap(2);
95+
}
96+
97+
TestPage.registerInitializer(() => {
98+
InspectorTest.SteppingAsync = {};
99+
100+
InspectorTest.SteppingAsync.run = function(name) {
101+
let suite = InspectorTest.createAsyncSuite(name);
102+
103+
function addTestCase({name, expression}) {
104+
suite.addTestCase({
105+
name,
106+
test(resolve, reject) {
107+
let done = false;
108+
let paused = false;
109+
110+
let pausedListener = WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Paused, (event) => {
111+
InspectorTest.log(`PAUSED (${WI.debuggerManager.dataForTarget(WI.debuggerManager.activeCallFrame.target).pauseReason})`);
112+
paused = true;
113+
});
114+
115+
let resumeListener = WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Resumed, (event) => {
116+
InspectorTest.log("RESUMED");
117+
paused = false;
118+
119+
if (done) {
120+
WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.Paused, pausedListener);
121+
WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.Resumed, resumeListener);
122+
resolve();
123+
}
124+
});
125+
126+
InspectorTest.singleFireEventListener("done", (event) => {
127+
done = true;
128+
129+
if (!paused) {
130+
WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.Paused, pausedListener);
131+
WI.debuggerManager.removeEventListener(WI.DebuggerManager.Event.Resumed, resumeListener);
132+
resolve();
133+
}
134+
});
135+
136+
InspectorTest.evaluateInPage(expression).catch(reject);
137+
},
138+
});
139+
}
140+
141+
addTestCase({
142+
name: name + ".statements",
143+
expression: "setTimeout(testStatements)",
144+
});
145+
146+
addTestCase({
147+
name: name + ".functions",
148+
expression: "setTimeout(testFunctions)",
149+
});
150+
151+
addTestCase({
152+
name: name + ".eval",
153+
expression: "setTimeout(testEval)",
154+
});
155+
156+
addTestCase({
157+
name: name + ".anonymousFunction",
158+
expression: "setTimeout(testAnonymousFunction)",
159+
});
160+
161+
addTestCase({
162+
name: name + ".commas",
163+
expression: "setTimeout(testCommas)",
164+
});
165+
166+
addTestCase({
167+
name: name + ".chainedExpressions",
168+
expression: "setTimeout(testChainedExpressions)",
169+
});
170+
171+
addTestCase({
172+
name: name + ".declarations",
173+
expression: "setTimeout(testDeclarations)",
174+
});
175+
176+
addTestCase({
177+
name: name + ".innerFunction",
178+
expression: "setTimeout(testInnerFunction)",
179+
});
180+
181+
addTestCase({
182+
name: name + ".testFor",
183+
expression: "setTimeout(testFor)",
184+
});
185+
186+
addTestCase({
187+
name: name + ".testRepeatedInvocation",
188+
expression: "setTimeout(testRepeatedInvocation)",
189+
});
190+
191+
loadLinesFromSourceCode(findScript(/\/inspector\/debugger\/resources\/stepping-async\.js$/)).then(() => {
192+
suite.runTestCasesAndFinish();
193+
});
194+
};
195+
});

0 commit comments

Comments
 (0)