[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•5 years ago
|
Comment 4•5 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
small
frame has a promise of its return value,B
. - This promise has a dependent promise
A
. - When resolved,
A
resumes 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.Frame
for 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.Object
referring to a promise needs to be able to enumerateawait
resumptions awaiting the promise's resolution. (I'm not sure how SpiderMonkey representsawait
continuations on promises.) -
async.resumptionFrame: An
await
resumption on a promise needs to be able to produce theDebugger.Frame
referring to theawait
continuation'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
•