Skip to content

src: skip the async id stack when nobody sees it - #66511

Draft
nigrosimone wants to merge 1 commit into
nodejs:mainfrom
nigrosimone:lazy-async-ids
Draft

nigrosimone wants to merge 1 commit into
nodejs:mainfrom
nigrosimone:lazy-async-ids

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

With no async hook and no executionAsyncResource() user, every InternalCallbackScope pushes and pops the async ids on the stack, only for executionAsyncId() and triggerAsyncId() to read the top. This swaps the two ids in place instead: they are all that those two and process.nextTick() read, so the readers pay nothing.

executionAsyncResource() is the only reader of the stack. The scopes that skip it form a chain in AsyncHooks, counted in a new field kLazyScopes, and JS asks C++ for the innermost one only while that count is not zero. The checks for a corrupted stack stay: test/async-hooks test-improper-order and test-emit-*-on-destroyed fail without them, because the main script runs in a scope that starts before any hook.

compare.js on napi/make_callback (the type cases of #66326), 30 runs, Linux x64, one core, with #66395 and #66500 underneath:

                                                     confidence improvement accuracy (*)   (**)  (***)
napi/make_callback n=1000000 type='AsyncResource'           ***      5.42 %  ±2.34% ±3.12% ±4.08%
napi/make_callback n=1000000 type='Call'                            -0.82 %  ±9.16% ±12.20% ±15.90%
napi/make_callback n=1000000 type='MakeCallback'                     3.71 %  ±5.60% ±7.45% ±9.70%
napi/make_callback n=10000000 type='AsyncResource'                   4.14 %  ±4.89% ±6.51% ±8.47%
napi/make_callback n=10000000 type='Call'                            3.41 %  ±7.34% ±9.76% ±12.70%
napi/make_callback n=10000000 type='MakeCallback'           ***      7.67 %  ±3.79% ±5.06% ±6.63%

MakeCallback goes from 128.7 to 119.5 ns at n=1e7. The nextTick and AsyncLocalStorage benchmarks do not move. test/async-hooks, the ALS tests (also with --no-async-context-frame) and the new addon test pass.

Draft because the new field and the chain are a design choice to agree on first, see nodejs/performance#24.

Disclosure: I used Opus 5.5 (Max) as coding assistant

With no async hook and no executionAsyncResource() user, every
InternalCallbackScope pushed and popped the async ids on the stack, only
for executionAsyncId() and triggerAsyncId() to read the top. Swap the
two ids in place instead: they are all that those two and
process.nextTick() read. executionAsyncResource() is the only reader of
the stack: the scopes that skip it form a chain in AsyncHooks, counted
in a new field, kLazyScopes, and JS asks C++ for the innermost one only
while that count is not zero. The checks for a corrupted stack stay.

Refs: nodejs/performance#24
Signed-off-by: Nigro Simone <nigro.simone@gmail.com>
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants