Closed Bug 1523865 (dbg-async-stacks) Opened 6 years ago Closed 4 years ago

[meta] Async Live Stacks in Debugger M1

Categories

(DevTools :: Debugger, task, P3)

task

Tracking

(Not tracked)

RESOLVED FIXED

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.

Summary: [meta] Async Stacks Q1 → [meta] Async Stacks H1
Summary: [meta] Async Stacks H1 → [meta] Async Live Stacks H1
No longer depends on: 1225190

There was another meta-bug for this already. I'll merge in it here and close it. It has a good problem statement:

  1. 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()
    
  2. 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.)

  3. 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)"
Priority: -- → P2
Priority: P2 → P3
Alias: dbg-async-stacks
Depends on: 1244339
No longer depends on: 1244339
No longer depends on: 1142571
Type: enhancement → task
Summary: [meta] Async Live Stacks H1 → [meta] Async Live Stacks in Debugger
Depends on: 1586193
Depends on: 1142571
See Also: → 1589523
No longer depends on: 1586193

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 in big'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 enumerate await resumptions awaiting the promise's resolution. (I'm not sure how SpiderMonkey represents await continuations on promises.)

  • async.resumptionFrame: An await resumption on a promise needs to be able to produce the Debugger.Frame referring to the await 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.

Depends on: 1592415
Blocks: dbg-72
No longer depends on: 1142571
Depends on: 1592427
Depends on: 1592430
Depends on: 1592431
Depends on: 1592432
No longer blocks: dbg-72
Summary: [meta] Async Live Stacks in Debugger → [meta] Async Live Stacks in Debugger M1
Depends on: 1608142
Depends on: 1612501
Depends on: 1612502

Landed and riding 74.

Status: NEW → RESOLVED
Closed: 4 years ago
Keywords: dev-doc-needed
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.