Any way to detect an async stack trace (like Chrome devtools does)?

#!/JoePea joe at trusktr.io
Wed Jul 15 03:20:45 UTC 2020


> The way promises are chained means that some patterns like async loops would theoretically unroll to very long stack traces

Devtools in fact does that (has a button at the bottom of the stack to
keep loading more frames).

I think it would be better for it to be smart and not have duplicate
frames in the stack, so a loop would result in only one unique set of
entries in the stack trace and not repeat.

#!/JoePea

On Sun, Jul 12, 2020 at 11:53 AM Jacob Bloom <mr.jacob.bloom at gmail.com> wrote:
>
> Reading through the issue JoePea linked to, it looks like the difficulties with standardized async stack traces are twofold:
>
> 1. Even with the error stacks proposal, the implementer has total say over what qualifies as a stack frame
> 2. The way promises are chained means that some patterns like async loops would theoretically unroll to very long stack traces, and right now engines aren't required to keep track of those frames; requiring them to do so would place a burden on implementers that could quickly lead to slowdowns and memory issues
>
> What if there was a standard way to mark a Promise as an important stack frame, which the implementer is free to ignore? Maybe something like `Promise.prototype.trace()`
>
> On Fri, Jul 10, 2020 at 10:22 PM #!/JoePea <joe at trusktr.io> wrote:
>>
>> Hello Kai! That example is so meta with its own output showing the
>> numbers relative to the code including its own output! :)
>>
>> That's what I originally wanted to do, but that doesn't give us an
>> async stack trace, it only gives a sync stack trace (I presume within
>> the current event loop task).
>>
>> For example:
>>
>> ```js
>> const originalFetch = globalThis.fetch
>>
>> globalThis.fetch = function(...args) {
>>   const stack = new Error().stack
>>   console.log(stack)
>>   return originalFetch.apply(globalThis, args)
>> }
>>
>> const sleep = t => new Promise(r => setTimeout(r, t))
>>
>> async function one() {
>>   console.log('1')
>>   await sleep(10)
>>   two()
>> }
>>
>> async function two() {
>>   console.log('2')
>>   await sleep(10)
>>   three()
>> }
>>
>> async function three() {
>>   console.log('3')
>>   await sleep(10)
>>   return await fetch('https://unpkg.com/three@0.118.3')
>> }
>>
>> async function main() {
>>   await one()
>> }
>>
>> main()
>> ```
>>
>> Output:
>>
>> ```
>> 1
>> 2
>> 3
>> Error
>>     at globalThis.fetch (pen.js:5)
>>     at three (pen.js:27)
>> ```
>>
>> Live example:
>>
>> https://codepen.io/trusktr/pen/b8fd92752b4671268f516ad3804869e4?editors=1010
>>
>> I opened a request for this over here:
>> https://github.com/tc39/proposal-error-stacks/issues/34
>>
>> #!/JoePea
>>
>> On Fri, Jul 10, 2020 at 1:21 PM kai zhu <kaizhu256 at gmail.com> wrote:
>> >
>> > >  (I want to detect traces in third-party code installed locally).
>> >
>> > 1. here's 15-line javascript-hack to trace async-fetch-calls from 3rd-party-cdn-library
>> >
>> > ```html
>> > <!doctype html>
>> > <html lang="en">
>> > <body>
>> > <h1>test.html</h1>
>> > <script>
>> > (function () {
>> > /*
>> >  * 15-line-javascript-hack to trace async-fetch-calls
>> >  * enable hack by adding search-query "?modeDebugFetch=1" to web-url
>> >  */
>> >     "use strict";
>> >     if ((/\bmodeDebugFetch=1\b/).test(location.search)) {
>> >         let fetch0;
>> >         fetch0 = globalThis.fetch;
>> >         globalThis.fetch = function (...argList) {
>> >             let errStack = new Error().stack;
>> >             console.error("\n\nfetch-call-arguments:");
>> >             console.error(JSON.stringify(argList, undefined, 4));
>> >             console.error("\n\nfetch-call-trace:");
>> >             console.error(errStack);
>> >             return fetch0(...argList);
>> >         };
>> >     }
>> > }());
>> > </script>
>> > <!-- 3rd-party-cdn-library -->
>> > <script src="https://unpkg.com/http-client/umd/http-client.js"></script>
>> > <script>
>> > (async function foo() {
>> >     "use strict";
>> >     let thirdParty = window.HTTPClient;
>> >     let myFetch = thirdParty.createFetch(
>> >         thirdParty.base("https://api.stripe.com/v1"),
>> >         thirdParty.accept("application/json")
>> >     );
>> >     let response = await myFetch("/customers/5");
>> >     console.log(response.jsonData);
>> > /*
>> > dev-console-output - http://localhost:8081/test.html?modeDebugFetch=1
>> > fetch-call-arguments:
>> > [
>> >     "https://api.stripe.com/v1/customers/5",
>> >     {
>> >         "headers": {
>> >             "Accept": "application/json"
>> >         },
>> >         "responseHandlers": [
>> >             null
>> >         ]
>> >     }
>> > ]
>> > fetch-call-trace:
>> > Error
>> >     at globalThis.fetch (test.html?modeDebugFetch=1:16)
>> >     at http-client.js:194
>> >     at http-client.js:127
>> >     at http-client.js:217
>> >     at http-client.js:126
>> >     at http-client.js:154
>> >     at http-client.js:95
>> >     at foo (test.html?modeDebugFetch=1:35)
>> >     at test.html?modeDebugFetch=1:64
>> > */
>> > }());
>> > </script>
>> > </body>
>> > ```
>> >
>> >
>> > 2. here's real-world-hack added to npm-cli.js to debug its http-requests
>> >
>> > ```diff
>> > C:\Program Files\nodejs\node_modules\npm>git diff
>> > diff --git a/bin/npm-cli.js b/bin/npm-cli.js
>> > index 561dec0..98cafb8 100644
>> > --- a/bin/npm-cli.js
>> > +++ b/bin/npm-cli.js
>> > @@ -2,17 +2,6 @@
>> >  ;(function () { // wrapper in case we're in module_context mode
>> > +// hack - debug http-request
>> > +let httpRequest;
>> > +httpRequest = require("https").request.bind(require("https"));
>> > +require("https").request = function (...argList) {
>> > +    if (process.env.NPM_DEBUG) {
>> > +        console.error(
>> > +            "npm - httpRequest - " + JSON.stringify(argList.slice(0, 2), undefined, 4)
>> > +        );
>> > +    }
>> > +    return httpRequest(...argList);
>> > +};
>> >    // windows: running "npm blah" in this folder will invoke WSH, not node.
>> >
>> > C:\Program Files\nodejs\node_modules\npm>
>> > ```
>> >
>> > ```mingw-bash
>> > $ NPM_DEBUG=1 npm publish foo
>> > # console-ouput:
>> > # npm - httpRequest - [
>> > #     {
>> > #         "protocol": "https:",
>> > #         "href": "https://registry.npmjs.org/foo",
>> > #         "method": "GET",
>> > #         "headers": {
>> > #             "connection": [
>> > #                 "keep-alive"
>> > #             ],
>> > #             "user-agent": [
>> > #                 "npm/6.13.4 node/v12.16.1 win32 x64"
>> > #             ],
>> > #             ...
>> > ```
>> >
>> >
>> >
>> >
>> >
>> > On Fri, Jul 10, 2020 at 3:34 AM #!/JoePea <joe at trusktr.io> wrote:
>> >>
>> >> Thanks for the idea! That's similar to what I thought would be necessary. I was hoping to monkey patch `fetch()` to have it get the trace if any `fetch` call, but they would not be async stack traces. For async traces it would require instrumentation by injecting code similar to yours (I want to detect traces in third-party code installed locally).
>> >>
>> >> #!/JoePea
>> >>
>> >> On Wed, Jul 8, 2020, 2:13 AM kai zhu <kaizhu256 at gmail.com> wrote:
>> >>>
>> >>> here's a simple throwaway-function you can wrap around promises like fetch
>> >>> to get the caller's async-stack-trace `promiseWithErrorStack`:
>> >>>
>> >>> ```html
>> >>> <!doctype html>
>> >>> <html lang="en">
>> >>> <body>
>> >>> <h1>test.html</h1>
>> >>> <script>
>> >>> (async function foo() {
>> >>>     function promiseWithErrorStack(promise) {
>> >>>     /*
>> >>>      * this function will append current-stack to any err caught from <promise>
>> >>>      */
>> >>>         let errStack;
>> >>>         errStack = new Error().stack;
>> >>>         return new Promise(function (resolve, reject) {
>> >>>             promise.then(resolve).catch(function (err) {
>> >>>                 // append current errStack to err.stack
>> >>>                 if (err && typeof err.stack === "string") {
>> >>>                     err.stack += "\n" + errStack;
>> >>>                 }
>> >>>                 reject(err);
>> >>>             });
>> >>>         });
>> >>>     }
>> >>>     await promiseWithErrorStack(fetch("https://example.com")); // at foo (test.html:23)
>> >>>
>> >>>     /*
>> >>>     console-output:
>> >>>
>> >>>     Uncaught (in promise) TypeError: Failed to fetch
>> >>>     Error
>> >>>         at promiseWithErrorStack (test.html:12)
>> >>>         at foo (test.html:23) // async-stack-trace
>> >>>         at test.html:32
>> >>>     */
>> >>> }());
>> >>> </script>
>> >>> </body>
>> >>> ```
>> >>>
>> >>> On Tue, Jul 7, 2020 at 11:54 PM #!/JoePea <joe at trusktr.io> wrote:
>> >>>>
>> >>>> Is there some way (perhaps by patching methods on objects?) so we can
>> >>>> track async call stacks?
>> >>>>
>> >>>> When we pause code in devtools, we are able to see the async stack
>> >>>> trace of the code.
>> >>>>
>> >>>> What I'd like to do is to effectively detect the same thing as
>> >>>> devtools does at some point in any code. As a specific example, I'd
>> >>>> like to detect the async stack trace of any call to `fetch`.
>> >>>>
>> >>>> Is such a thing possible with runtime code?
>> >>>>
>> >>>> Or would it require instrumentation of the source code?
>> >>>>
>> >>>> #!/JoePea
>> >>>> _______________________________________________
>> >>>> es-discuss mailing list
>> >>>> es-discuss at mozilla.org
>> >>>> https://mail.mozilla.org/listinfo/es-discuss
>> _______________________________________________
>> es-discuss mailing list
>> es-discuss at mozilla.org
>> https://mail.mozilla.org/listinfo/es-discuss
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss


More information about the es-discuss mailing list