-
-
Notifications
You must be signed in to change notification settings - Fork 34.2k
fs: add stacktrace to fs/promises #49849
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
fs: add stacktrace to fs/promises #49849
Conversation
| fsBinding.fstat = common.mustCall( | ||
| () => (/* stat fields */ [0, 1, 2, 3, 4, 5, 6, 7, 0 /* size */]) | ||
| async () => (/* stat fields */ [0, 1, 2, 3, 4, 5, 6, 7, 0 /* size */]) | ||
| ); |
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.
This now needs to be an async function otherwise binding.fstat(/* */).catch will fail.
aduh95
left a comment
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.
Could you try to use PromisePrototypeThen from primordials so it keeps working even if the Promise prototype have been mutated in userland?
anonrig
left a comment
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.
Instead of going down this path, we should throw the errors on C++ side. Similar approach is taken with Sync functions at the moment.
|
Sure. I'll take another look on the c++ land. |
We already reject directly from the C++ land (note that this is not throwing errors, this is promise rejection) and that's actually why the async stack trace is getting lost, because by the time we reject from the C++ land (note that this is async, so this is likely coming from some libuv callback execution in an event loop iteration), there is no JS frame on the stack at all. We need to reject and then await for V8 to resume back, and only until V8 resumes execution back to the JS land would there be enough information about the call site for us to re-capture a full stack trace. I don't think there is a way to resume in C++ land, so we have to do this in JS. |
joyeecheung
left a comment
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.
Overall LGTM % some suggestions
| name: 'Error', | ||
| message: /^ENOENT: no such file or directory, access/ | ||
| message: /^ENOENT: no such file or directory, access/, | ||
| stack: /at async Function\.rejects/ |
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.
Can you add a test with a closure too?
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.
I'm not sure if I understand correctly but I added a test here.
https://github.com/nodejs/node/pull/49849/files#diff-9f28bba0b9948a16a3905ddc942f5c0a49cfd2e34214dc7ce331c7e33fb1c13fR98-R106
To make this work, I had to add await before return for zero-cost async stack traces (http://bit.ly/v8-zero-cost-async-stack-traces).
But the linter doesn't allow to do return await by the no-return-await rule.
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.
Does the 4-year-old document you link still apply on modern V8 versions? AFAIK the lint rule is there because of assumptions, we should challenge those assumptions and reassess the linter rule if necessary.
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.
Does the 4-year-old document you link still apply on modern V8 versions?
I'm not sure but if I remove await the test I added won't pass at least. FWIW the document is linked from https://v8.dev/docs/stack-trace-api#async-stack-traces.
AFAIK the lint rule is there because of assumptions, we should challenge those assumptions and reassess the linter rule if necessary.
The documentation says the rule is deprecated.
This rule was deprecated in ESLint v8.46.0 with no replacement. The original intent of this rule no longer applies due to the fact JavaScript now handles native Promises differently. It can now be slower to remove await rather than keeping it. More technical information can be found in this V8 blog entry.
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.
If you open a PR to remove that rule, it will probably get accepted.
|
Also to answer questions from the OP:
I don't think skipping async stack generation is done on purpose, it's likely an oversight. This is also done on the error path so the success path is unaffected. In this case I think having an actually meaningful stack trace with a bit of overhead is much more important than generating a useless stack trace quickly.
For now I think resuming back to JS land and capture stack trace there is probably the best we can do. Note that it's not a matter of where you capture the stack trace, it's when you capture the stack trace. When the system call encounter an error, libuv invokes our callback in an event loop iteration, and we create and error and then reject. But at this point in time, there is no JS stack frame on the stack (this is why we have no JS frames for async stack trace but we have them for the sync ones - by the time we create JS errors in a synchronous method, no matter in JS or C++, the call site information is still there on the stack, V8 can do some lightweight capture for that and defer the expensive stringification until
I think 2 is much simpler than 1, also 1 can cost quite a bit of additional memory and it'll go to waste if the system call does not fail (which is the more likely case), whereas V8 already maintains the async execution context more performantly under the hood for async-await, so we might as well just use that. |
|
The
notable-change
Please suggest a text for the release notes if you'd like to include a more detailed summary, then proceed to update the PR description with the text or a link to the notable change suggested text comment. |
| } | ||
|
|
||
| function handleErrorFromBinding(error) { | ||
| ErrorCaptureStackTrace(error, handleErrorFromBinding); |
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.
is there a perf difference when doing
| ErrorCaptureStackTrace(error, handleErrorFromBinding); | |
| Error.stackTraceLimit && ErrorCaptureStackTrace(error, handleErrorFromBinding); |
So only call ErrorCaptureStackTrace if Error.strackTraceLimit is not false (= 0)
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.
the code of benchmark
'use strict';
const common = require('../common');
const fs = require('fs/promises');
const bench = common.createBenchmark(main, {
n: [1e5],
stacktraceLimit: [0, 10]
});
async function main({ n, stacktraceLimit }) {
const fn = fs.access;
Error.stackTraceLimit = stacktraceLimit;
bench.start();
for (let i = 0; i < n; i++) {
try {
await fn('no existance');
} catch {}
}
bench.end(n);
}The result of the benchmark above on my machine is
// without Error.stackTraceLimit
fs/foo.js stacktraceLimit=0 n=100000: 25,569.003187542396
fs/foo.js stacktraceLimit=10 n=100000: 24,136.711560423188
// with Error.stackTraceLimit
fs/foo.js stacktraceLimit=0 n=100000: 25,789.21066371575
fs/foo.js stacktraceLimit=10 n=100000: 23,625.22690199484
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.
Thank you
Thank you for the answers! |
Sync functions in fs throwed an error with a stacktrace which is helpful for debugging. But functions in fs/promises throwed an error without a stacktrace. This commit adds stacktraces by calling Error.captureStacktrace and re-throwing the error. Refs: nodejs#34817
Co-authored-by: Joyee Cheung <[email protected]>
e531277 to
b7d6550
Compare
|
I rebased on main branch to include #50118 so that the lint passes. |
Failed to start CI- Validating Jenkins credentials ✘ Jenkins credentials invalidhttps://github.com/nodejs/node/actions/runs/6497069197 |
|
@UlisesGascon Sure! Opened it: #51127 |
@joyeecheung Sorry to hijack this but would you mind elaborating a bit on option 1? We have a custom wrapper for our C++ library using napi and we have this same issue of empty stack traces when rejecting a promise from C++. I wanted to evaluate whether option 1 would be a better match for us than option 2. But I can't get it to work. What I tried is:
However the rejection still has no stack trace. Is this different to what you mean by option 1 above. Thanks for the help! |
By "async context" I did not mean napi_async_context, but a conceptual "some structure we need to maintain ourselves to keep track every time we do a C++ -> JS callback, and it should save the user caller function information in there, somehow" |
I see, so the infrastructure to implement option 1 does not readily exist, much less so in napi. Fair enough, then we need to see how to implement option 2. Thanks for the input! |
Sync functions in fs throwed an error with a stacktrace which is helpful for debugging. But functions in fs/promises throwed an error without a stacktrace. This commit adds stacktraces by calling Error.captureStacktrace and re-throwing the error.
Refs: #34817
Fixes: #50160
I have some questions.
Error.captureStackTraceon C++ land but didn't find the way.