Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34.2k
stream: improve performance of finished()#59873
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
stream: improve performance of finished() #59873
Uh oh!
There was an error while loading. Please reload this page.
Conversation
nodejs-github-bot commented Sep 12, 2025
Review requested:
|
codecovbot commented Sep 13, 2025 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@## main #59873 +/- ## ========================================== + Coverage 88.56% 88.57% +0.01% ========================================== Files 704 704 Lines 208322 208330 +8 Branches 40027 40040 +13 ========================================== + Hits 184501 184533 +32 + Misses 15863 15818 -45 - Partials 7958 7979 +21
🚀 New features to boost your workflow:
|
H4ad 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.
Hey, thanks for the PR, I added a few comments about some issues I found running locally.
Also, would you mind split this change in two commits, the first one being the benchmark and the second one the optimization?
You can also improve the commit message a little bit by adding what you really changed to improve performance, eg: stream: preserve AsyncLocalStorage on finished only when needed
About the benchmark name, maybe changing to finished should be more relevant than end-of-stream since we are testing the finished function.
Preview of the results:
h4ad:node-copy-4/ (improve-stream-finished-perf✗) $ node-benchmark-compare streams.csv [10:49:03] confidence improvement accuracy (*) (**) (***) streams/compose.js n=1000 *** 42.25 % ±3.90% ±5.24% ±6.91% streams/end-of-stream.js streamType='readable' n=100000 *** 76.67 % ±1.41% ±1.89% ±2.48% streams/end-of-stream.js streamType='writable' n=100000 *** 105.00 % ±2.01% ±2.69% ±3.56% streams/pipe-object-mode.js n=5000000 -0.30 % ±0.81% ±1.08% ±1.42% streams/pipe.js n=5000000 0.14 % ±1.77% ±2.36% ±3.08% Be aware that when doing many comparisons the risk of a false-positive result increases. In this case, there are 5 comparisons, you can thus expect the following amount of false-positive results: 0.25 false positives, when considering a 5% risk acceptance (*, **, ***), 0.05 false positives, when considering a 1% risk acceptance (**, ***), 0.01 false positives, when considering a 0.1% risk acceptance (***) Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
mcollina 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.
While I love the PR, but I fear that this breaks continuation when using async_hooks.
We could argue that with ALS not using async_hooks we could remove them (or add them via a compile time flag) but that's outside of the scope of this PR.
56bc9ed to 2d4a3b5Compareavcribl commented Sep 13, 2025
Thanks @mcollina! I removed the async_hooks part. |
krsjenswbp 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.
thanks
Flarna commented Sep 15, 2025
There is command line option I think we might need special handing for this variation. |
mcollina commented Sep 15, 2025
@avcribl you didn't address my comment. |
avcribl commented Sep 15, 2025
2d4a3b5 to e7d45d6Compareavcribl commented Sep 16, 2025
@mcollina I've pushed a new commit that adds a check to determine whether async_context_frame is enabled. I tested both scenarios: In both cases, the unit tests ran successfully and passed: Also, I ran the benchmark test with and without the flag and both have similar improved performance: Please let me know if it addresses your comment. |
mcollina commented Sep 16, 2025
I don't see any new tests in this PR, just a benchmark. @Qard or @Flarna could chime in as well, but checking if AsyncContextFrame is enabled is not enough in case plain async_hooks are used. There are still a lot of modules out there that are not using AsyncLocalStorage for context tracking. My understanding is that using |
Flarna commented Sep 16, 2025
It's quite hard to tell. I would recommend to write tests for all these scenarios to be improved here and also verify that non improvable scenarios still work. FWIW I'm not a big fan of tinkering with async hooks/async local store internals at several places in code. |
| if((AsyncContextFrame.enabled&&AsyncContextFrame.current())|| | ||
| (!AsyncContextFrame.enabled&&getHookArrays()[0].length>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.
This could be simplified to:
| if((AsyncContextFrame.enabled&&AsyncContextFrame.current())|| | |
| (!AsyncContextFrame.enabled&&getHookArrays()[0].length>0)){ | |
| if(AsyncContextFrame.current()||enabledHooksExist()){ |
The AsyncContextFrame.current() call is a no-op when inactive, you don't really need the enabled check as that will just add more instructions. Also, as @Flarna pointed out, enabledHooksExist() is the clearer way to check if there are active async_hooks listeners.
avcriblSep 17, 2025 • edited
Loading Uh oh!
There was an error while loading. Please reload this page.
edited
Uh oh!
There was an error while loading. Please reload this page.
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.
Thanks! I removed AsyncContextFrame.enabled
Qard commented Sep 16, 2025
One thing worth noting is that this would be a breaking change as currently the AsyncLocalStorage.bind(...) will always bind, even if there are no hooks or stores in use, but async_hooks could start listening at any time. In the current model you could start listening after the bind call there but before the callback is called and you would see the events. After this change you would not. |
e7d45d6 to 51e0905Compareavcribl commented Sep 17, 2025
Thanks all for the feedback! |
mcollina 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.
lgtm
| finished(readable,common.mustCall(()=>{ | ||
| strictEqual(internalAsyncHooks.getHookArrays()[0].length>0, | ||
| true,'Should have active user async hook'); | ||
| })); |
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.
Note that this does not verify continuation is preserved
| finished(readable,common.mustCall(()=>{ | ||
| strictEqual(internalAsyncHooks.getHookArrays()[0].length>0, | ||
| true,'Should have active user async hook'); | ||
| })); |
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.
Note that this does not verify continuation is preserved
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 doesn't verify the code path either. It is a copy of the condition and someone might change one of the locations.
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.
Thanks! I expanded on the test!
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.
@mcollina I addressed your comment, thanks!
ff9809b to fd7f7a4Comparefd7f7a4 to d51ca0dCompareavcribl commented Oct 15, 2025
I rebased my PR. Can I please get approval for kicking off the workflows? Thanks! |
nodejs-github-bot commented Oct 16, 2025
avcribl commented Oct 20, 2025
Can the pipeline be triggered again? Still, some flaky tests fail, thank you! |
nodejs-github-bot commented Oct 20, 2025
avcribl commented Oct 22, 2025
Can I get another run please? Thanks! |
nodejs-github-bot commented Oct 22, 2025
avcribl commented Oct 22, 2025
CI is green now! |
avcribl commented Oct 27, 2025
The CI build is green, do I need to do anything more here? Thanks! |
avcribl commented Oct 27, 2025
Thanks @H4ad ! I think the label |
4fe325d into nodejs:mainUh oh!
There was an error while loading. Please reload this page.
nodejs-github-bot commented Oct 27, 2025
Landed in 4fe325d |
Correct the implementaton of enabledHooksExist to return true if there are enabled hooks. Adapt callsites which used getHooksArrays() as workaround. PR-URL: #61054Fixes: #61019 Refs: #59873 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Gürgün Dayıoğlu <[email protected]>
The changes optimize the
end-of-stream.jsmodule to only preserve AsyncLocalStorage context when it's actually needed, improving performance by avoiding ALS overhead in the common case where no async context tracking is active.The performance test results with this patch:
The same perf test results with the current nodejs:
You can compare and see the noticeable performance improvement with this patch.