Skip to content

Wrap handle callbacks with console.createTask(...) #47444

Description

@Qard

What is the problem this feature will solve?

Currently stack traces are not great with most callback-based code. Some modules exist to produce long stack traces with async_hooks, however the performance impact can be significant. V8 now has its own built-in solution, so we should investigate if that can produce better results.

What is the feature you are proposing to solve the problem?

A recent V8 update has provided the console.createTask(...) API. We can start pairing that with our existing handle callbacks to gain much better stack traces.

See: #44792

It also has the potential to be integrated with the AsyncContext spec in the future to be used as an equivalent to AsyncResource. This could mean we get context-linkage of callback-based code automatically in the future.

cc @nodejs/diagnostics

What alternatives have you considered?

No response

Activity

  1. added
    discussIssues opened for discussion and feedback.
    feature requestIssues requesting new Node.js features.
    async_hooksIssues and PRs related to the async hooks subsystem.
    errorsIssues and PRs related to JavaScript errors originating in Node.js core.
    on Apr 6, 2023
  2. Qard commented on Apr 6, 2023

    @Qard
    MemberAuthor

    (Leaving this issue mostly as a note to myself to investigate, but others can feel free to look into it on their own and/or comment here on the idea.)

  3. targos commented on Apr 6, 2023

    @targos
    Member

    I'd be happy to have a look. What would be a simple example of code that can benefit from it?

  4. legendecas commented on Apr 6, 2023

    @legendecas
    Member

    If we run the following script in the latest Chrome:

    function makeScheduler() {
      const tasks = [];
    
      return {
        schedule(f) {
          const task = console.createTask(f.name);
          tasks.push({ task, f });
        },
    
        work() {
          while (tasks.length) {
            const { task, f } = tasks.shift();
            task.run(f); // instead of f();
          }
        },
      };
    }
    
    const scheduler = makeScheduler();
    scheduler.schedule(function myTask() {
      console.log(new Error('foo'));
    });
    
    setTimeout(() => {
      scheduler.work()
    }, 1);

    The output of the console should be: (notice that the stacktrace only contains where the task is processed)

    Error: foo
        at task (script:21:15)
        at Object.work (script:13:14)
        at script:25:13
    

    However, if we set a breakpoint in the myTask function, the debugger captures the stacktrace when the task is scheduled too. The debuger call stack tab should shows the following call frames:

    Error: foo
        at task (script:21:15)
        at Object.work (script:13:14)
        at script:25:13
        myTask (async)      <---  async stack tagged
        at schedule (script:6)
        at script:20
    
  5. Qard commented on Apr 6, 2023

    @Qard
    MemberAuthor

    My basic thinking was to have a wrapper something like this:

    function makeCallbackTask(name, callback) {
      const task = console.createTask(name);
      return function callbackTask(...args) {
        return task.run(() => callback.apply(this, args));
      };
    }

    Which could be used to replace this: (https://github.com/nodejs/node/blob/main/lib/fs.js#L230-L231)

      const req = new FSReqCallback();
      req.oncomplete = callback;

    with something like this:

      const req = new FSReqCallback();
      req.oncomplete = makeCallbackTask('FSReq', callback);

    Basically anywhere that we attach those on* property callbacks onto handle objects we would do an extra step to wrap the callback up as a task.

    We would probably want something a bit more performant than making a closure every time, so could be we make a task class or something like that, but this illustrates the general idea.

  6. targos commented on Apr 8, 2023

    @targos
    Member

    I tried it with a simple script:

    const fs = require('fs');
    
    fs.access(__filename, fs.constants.F_OK, () => {
      throw new Error('test')
    });

    With Node.js 19.8.1:

    $ node try.js  
    /Users/mzasso/git/nodejs/node/try.js:4
      throw new Error('test')
      ^
    
    Error: test
        at /Users/mzasso/git/nodejs/node/try.js:4:9
        at FSReqCallback.oncomplete (node:fs:185:23)
    
    Node.js v19.8.1
    

    With your suggestion:

    $ ./node try.js
    /Users/mzasso/git/nodejs/node/try.js:4
      throw new Error('test')
      ^
    
    Error: test
        at /Users/mzasso/git/nodejs/node/try.js:4:9
        at FSReqCallback.<anonymous> (node:fs:186:23)
        at node:fs:238:36
        at FSReqCallback.callbackTask [as oncomplete] (node:fs:238:17)
    
    Node.js v20.0.0-pre
    

    Am I missing something?

  7. Qard commented on Apr 8, 2023

    @Qard
    MemberAuthor

    From what @legendecas was saying, it will only do the longer stack trace when using the debugger. Just throwing an error will not produce the longer stack trace. A bit less useful than I was hoping. It would be amazing if it could always produce the longer stack trace.

  8. targos commented on Apr 8, 2023

    @targos
    Member

    I also tried the debugger, and in that case I already got a long stacktrace without even applying the patch (on v19.8.1).

  9. joyeecheung commented on Apr 8, 2023

    @joyeecheung
    Member

    Basically anywhere that we attach those on* property callbacks onto handle objects we would do an extra step to wrap the callback up as a task.

    I think even if we implement console.createTask() ourselves (so that we do not rely on the V8 implementation, which only really does anything when the debugger is on), this should probably be opt-in instead of being wrapped by default, since the book-keeping probably comes with a non-trivial cost. In the V8 inspector console implementation it needs to capture the current stack trace and calculate the async chain, which looks non-trivial even implemented internally in V8.

  10. Qard commented on Apr 9, 2023

    @Qard
    MemberAuthor

    I also tried the debugger, and in that case I already got a long stacktrace without even applying the patch (on v19.8.1).

    Ah, could maybe use some clarification then on if there's something we're already doing that is getting us that. Could be it's just only usable for userland then.

    Basically anywhere that we attach those on* property callbacks onto handle objects we would do an extra step to wrap the callback up as a task.

    I think even if we implement console.createTask() ourselves (so that we do not rely on the V8 implementation, which only really does anything when the debugger is on), this should probably be opt-in instead of being wrapped by default, since the book-keeping probably comes with a non-trivial cost. In the V8 inspector console implementation it needs to capture the current stack trace and calculate the async chain, which looks non-trivial even implemented internally in V8.

    Agreed. At the least we should implement such an experiment behind a flag so we can measure perf impact, how successful it actually is at producing these better stack traces, etc.

    As I tried to convey in my initial messaging, I haven't looked very closely at this yet. It's as much a note to my future self to see if it's even a reasonable idea as it is a feature suggestion. It sounds like there's possibly some value there, but yet to be seen if it's worth whatever tradeoffs would be needed.

  11. legendecas commented on Apr 10, 2023

    @legendecas
    Member

    Yeah, async tasks tracked by async hooks are already reported to the inspector: #13870.

    IMO, the point here is that if we should expose console.createTask API to userland, the same as Chrome does.

    Updated: It is exposed already.

  12. Qard commented on Apr 10, 2023

    @Qard
    MemberAuthor

    It already is exposed.

  13. github-actions commented on Oct 9, 2023

    @github-actions
    Contributor

    There has been no activity on this feature request for 5 months and it is unlikely to be implemented. It will be closed 6 months after the last non-automated comment.

    For more information on how the project manages feature requests, please consult the feature request management document.

  14. added
    staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.
    on Oct 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    async_hooksIssues and PRs related to the async hooks subsystem.discussIssues opened for discussion and feedback.errorsIssues and PRs related to JavaScript errors originating in Node.js core.feature requestIssues requesting new Node.js features.staleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions