-
Notifications
You must be signed in to change notification settings - Fork 48.8k
[Flight] Send the awaited Promise to the client as additional debug information #33592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
The WeakRef keeps it from retaining itself in a cycle. However, we create a chain of dependencies between Promise instances so that the child retains the parent so that in practice the WeakRefs are kept alive as long as we need them which makes it safe to extract debugInfo from them later.
This lets the client inspect the value or reason it rejected.
This is better context than the internal Promise of the third party implementation. To do this we need to find the first await in user space and not just the first await that has any call frames in user space. To do this we need to find the first frame outside of the async hooks instrumentation. We do that by hard coding how many extra frames Node currently adds (fairly stable). The problem with this approach is that it doesn't work if someone adds a third party wrapper around .then() like we do in our ReactPromises atm. It's fine if you await it but not if you call it directly.
When printing common classes it gets noisy to show internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Somehow my question got lost when I submitted my first review.)
// If the thing we're waiting on is another Await we still track that sequence | ||
// so that we can later pick the best stack trace in user space. | ||
node = ({ | ||
tag: UNRESOLVED_AWAIT_NODE, | ||
owner: resolveOwner(), | ||
debugInfo: new WeakRef((resource: Promise<any>)), | ||
stack: parseStackTrace(new Error(), 1), | ||
stack: parseStackTrace(new Error(), 5), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't the 5
become incorrect when Node.js internals change between versions? Should we ignore node:internal/async_hooks
frames instead (plus skip the first frame)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed I had that in an earlier version but decided against it.
Yes it could change. Last time it was changed was 6 years ago and before that it didn't change since it was added.
However, it could also just as well change file names. We're knowingly dealing with internals here regardless.
They could also just remove the API all together since it's deprecated. Hopefully our usage can inform the shape of a better replacement API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Stacked on #33588, #33589 and #33590.
This lets us automatically show the resolved value in the UI.
We can also show rejected I/O that may or may not have been handled with the error message.
To get this working we need to keep the Promise around for longer so that we can access it once we want to emit an async sequence. I do this by storing the WeakRefs but to ensure that the Promise doesn't get garbage collected, I keep a WeakMap of Promise to the Promise that it depended on. This lets the VM still clean up any Promise chains that have leaves that are cleaned up. So this makes Promises live until the last Promise downstream is done. At that point we can go back up the chain to read the values out of them.
Additionally, to get the best possible value we don't want to get a Promise that's used by internals of a third-party function. We want the value that the first party gets to observe. To do this I had to change the logic for which "await" to use, to be the one that is the first await that happened in user space. It's not enough that the await has any first party at all on the stack - it has to be the very first frame. This is a little sketchy because it relies on the
.then()
call orawait
call not having any third party wrappers. But it gives the best object since it hides all the internals. For example when you callfetch()
we now log that actualResponse
object.