[meta] Async Live Stacks in Debugger M1
Categories
(DevTools :: Debugger, task, P3)
Tracking
(Not tracked)
People
(Reporter: Honza, Unassigned)
References
(Blocks 1 open bug)
Details
(Keywords: dev-doc-needed, meta)
This is a tracking bug for H1 work related to asynchronous callstacks.
| Reporter | ||
Updated•6 years ago
|
| Reporter | ||
Updated•6 years ago
|
| Reporter | ||
Updated•6 years ago
|
| Reporter | ||
Updated•6 years ago
|
Comment 1•6 years ago
|
||
There was another meta-bug for this already. I'll merge in it here and close it. It has a good problem statement:
-
Suppose an async function g does
await f(). f is an async function.In a later tick, the debugger breaks in f. Only one frame is on the stack
(the frame for f), but the debugger should show:f() ------------ async boundary g() -
Or, if g in turn was called with
await g()from another async function h,
then we should show that too:f() ------------ async boundary g() h()(Maybe with another async boundary; that's a UI decision we can defer.)
-
Another test case: g does
await f().then(m);. The debugger should show:f() ------------ async boundary m() g()
What is hard here?
-
Identifying and documenting cases to test (but here we have 3 of them to start with)
-
Decrypting the promise object graph
- Exposing all the bits required via Debugger
- Implementing this in the server and making it maintainable
-
Determining how the server should present it to the client
- Need a way to express "async boundary"
- Need a way to express "this is like a call frame, only we're going to call it when we get to it (relax, this is fine)"
Updated•6 years ago
|
Updated•6 years ago
|
Updated•6 years ago
|
Updated•6 years ago
|
Updated•6 years ago
|
Comment 4•6 years ago
|
||
Here's another explanation of this from a while back, that breaks down the technical work a bit:
Including awaiters in stack traces
Suppose we have the following code:
async function big() {
await small(); // point A
}
async function small() {
return munge_reply(await fetch(url)); // point B
}
function munge_reply(reply) {
return `It's been munged: ${reply}`; // point C
}
If the debuggee is paused at point C, then the call stack should ideally look like this (youngest frame first):
munge_reply
small
big
The function small called munge_reply, and is expecting its return value; and big is awaiting the resolution of the promise of small's return value. Although the first is an ordinary function call and the latter is an await, the two relationships can be displayed similarly (although perhaps not identically; it might make sense to distinguish async calls from ordinary calls).
However, in the above situation, the actual JavaScript call stack looks like this:
munge_reply
small
The stack frame for big does not appear. The big function called small(), receiving a promise of its return value, and awaited that promise, suspending its own execution and removing its frame from the stack. The call to small itself did the same, awaiting the fetch result. When the fetch result promise was resolved, it invoked small's await expression's continuation as a callback. But a promise callback is an ECMAScript 'Job' that executes from the PromiseJobs job queue, and so has no JavaScript caller, so small has no JavaScript frames below it on the stack.
What small's stack frame does have, as an async call, is a promise that will be resolved with its return value (or rejected with the exception it throws). That promise has at least one callback attached to it: one that will resume the async call to big, passing along the promise's resolution.
More precisely, an expression await E creates a fresh promise A which, when resolved, invokes the await expression's continuation, passing a value or throwing an exception as appropriate. Then, the await resolves A to the value of E. If E is some promise B (as it would be if E is a call to an async function), then A becomes a dependent promise of B.
So the path from small's frame to big's frame is as follows:
- The
smallframe has a promise of its return value,B. - This promise has a dependent promise
A. - When resolved,
Aresumes execution inbig's frame.
To make this structure available to the devtools server, we need the following extensions to the Debugger API:
-
async.completionPromise: A
Debugger.Framefor an async call needs to be able to retrieve the promise that will be resolved when the frame returns or throws. -
async.promiseAwaiters: A
Debugger.Objectreferring to a promise needs to be able to enumerateawaitresumptions awaiting the promise's resolution. (I'm not sure how SpiderMonkey representsawaitcontinuations on promises.) -
async.resumptionFrame: An
awaitresumption on a promise needs to be able to produce theDebugger.Framereferring to theawaitcontinuation's stack frame.
This should be everything the devtools server needs to incorporate asynchronous calls into the stacks it reports to the client.
It's obvious once you think about it, but it's interesting to note that any combined call stack is segregated into two parts: all ordinary function calls appear at the young end of the stack, and all asynchronous calls appear at the old end. As ECMAScript jobs, callbacks are invoked directly from the job queue, on an empty call stack, so as you walk frames from youngest to oldest, once you have reached the promise callback, any further frames will be obtained via awaiter/awaitee links.
| Reporter | ||
Updated•5 years ago
|
Comment 5•5 years ago
|
||
Landed and riding 74.
Description
•