Skip to content

src: keep ALS store in AsyncResource::MakeCallback - #66326

Open
nigrosimone wants to merge 3 commits into
nodejs:mainfrom
nigrosimone:async-resource-frame
Open

nigrosimone wants to merge 3 commits into
nodejs:mainfrom
nigrosimone:async-resource-frame

Conversation

@nigrosimone

@nigrosimone nigrosimone commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

AsyncResource saves the async context frame when it is created, and MakeCallback() enters it with async_context_frame::Scope. Then node::MakeCallback() opens the callback scope with an undefined frame, so the callback never runs in the saved one. Since AsyncContextFrame is the default (v24), AsyncLocalStorage loses its store in the callbacks of node::AsyncResource. With --no-async-context-frame the store is there.

Now MakeCallback() passes the saved frame to InternalMakeCallback(), as the Node-API AsyncContext already does. On the function overload the Scope goes away, so every call also saves two Environment lookups and a global handle. The overloads by method name keep it around the Get(), as a getter can call into JS.

The new test fails without the change: the store is undefined in the callback.

The second commit adds type=AsyncResource and type=Call to the make_callback benchmark added in #66316. Call is a plain v8::Function::Call, what the call costs without Node. benchmark/compare.js, 30 runs, Linux x64:

                                                    confidence improvement accuracy (*)   (**)  (***)
napi/make_callback n=1000000 type='AsyncResource'           ***     36.22 %  ±5.06% ±6.74% ±8.79%
napi/make_callback n=1000000 type='Call'                             0.80 %  ±4.70% ±6.25% ±8.14%
napi/make_callback n=1000000 type='MakeCallback'                     2.77 %  ±6.38% ±8.49% ±11.07%
napi/make_callback n=10000000 type='AsyncResource'          ***     29.34 %  ±4.31% ±5.75% ±7.49%
napi/make_callback n=10000000 type='Call'                            4.00 %  ±5.10% ±6.79% ±8.85%
napi/make_callback n=10000000 type='MakeCallback'                    2.17 %  ±3.37% ±4.49% ±5.85%

Refs: #43038
Refs: nodejs/performance#24

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

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. labels Sep 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to Node.js, and thank you for your first contribution!

Before review, please take a moment to read:

Please make sure every commit is signed off. For a first pull request, GitHub Actions require collaborator approval and Jenkins CI must be started by a collaborator or triager, so an initial wait is normal.

AsyncResource saves the async context frame when it is created, and
MakeCallback() enters it with async_context_frame::Scope. Then
node::MakeCallback() opens the callback scope with an undefined frame,
so the callback never runs in the saved one. Since AsyncContextFrame is
the default, AsyncLocalStorage loses its store in these callbacks.

Pass the saved frame to InternalMakeCallback(), as the Node-API
AsyncContext already does. This also removes the Scope from every call,
with its two Environment lookups and its global handle.

Refs: nodejs#66316
Refs: nodejs#43038
Refs: nodejs/performance#24
Signed-off-by: Nigro Simone <[email protected]>
type=AsyncResource calls node::AsyncResource::MakeCallback() and
type=Call a plain v8::Function::Call, from the same libuv timer as
type=MakeCallback. Call is what the call costs without Node.

Signed-off-by: Nigro Simone <[email protected]>
@nigrosimone
nigrosimone marked this pull request as ready for review September 29, 2026 07:06
@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.18182% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.45%. Comparing base (6866129) to head (78d5c35).
⚠️ Report is 283 commits behind head on main.

Files with missing lines Patch % Lines
src/api/async_resource.cc 68.18% 2 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #66326      +/-   ##
==========================================
+ Coverage   90.36%   90.45%   +0.09%     
==========================================
  Files         792      791       -1     
  Lines      275498   276571    +1073     
  Branches    52798    53114     +316     
==========================================
+ Hits       248947   250172    +1225     
+ Misses      16976    16799     -177     
- Partials     9575     9600      +25     
Files with missing lines Coverage Δ
src/api/async_resource.cc 83.01% <68.18%> (-12.54%) ⬇️

... and 115 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@anonrig anonrig added author ready PRs with CI started, the required approvals, and no outstanding review comments. request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. labels Oct 4, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 4, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@legendecas legendecas left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A test case like this fails with the PR:

'use strict';

const common = require('../../common');
const assert = require('assert');
const { AsyncLocalStorage } = require('async_hooks');
const binding = require(`./build/${common.buildType}/binding`);

const als = new AsyncLocalStorage();
let getterStore;

const object = Object.defineProperty({}, 'methöd', {
  get() {
    getterStore = als.getStore();
    als.enterWith('getter');           // <= this leaks out the MakeCallback
    return () => {};
  },
});

const resource =
  als.run('resource', () => binding.createAsyncResource(object));

let actual;
als.run('caller', () => {
  binding.callViaString(resource);
  actual = [getterStore, als.getStore()];
});

binding.destroyAsyncResource(resource);

assert.deepStrictEqual(actual, ['resource', 'caller']);
// With this PR, this results in ['resource', 'getter'] instead.

Comment thread src/api/async_resource.cc Outdated
if (!env_->can_call_into_js()) return {};
Local<Value> callback;
if (!get_resource()
->Get(isolate->GetCurrentContext(), symbol)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could call into JS and should be scoped with a async_context_frame::Scope.

@nigrosimone

Copy link
Copy Markdown
Contributor Author

Thanks, you are right. Fixed in 78d5c35: the Get() runs in the saved frame again, and your case is in test-async-local-storage.js, it fails without the fix.

@nigrosimone
nigrosimone requested a review from legendecas October 8, 2026 14:30
@legendecas

Copy link
Copy Markdown
Member

Would you mind confirming if the performance gain still preserve after the change? IIUC it changes the claim in the OP.

@nigrosimone

Copy link
Copy Markdown
Contributor Author

Yes, it holds. The benchmark calls AsyncResource::MakeCallback(fn, ...), the function overload, and 78d5c35 only touches the overloads by method name: there the Get() now runs inside a Scope, as the whole call did before this PR. I ran it again on the new head, with a local type for the call by name too (30 runs, the machine is noisier today):

                                                    confidence improvement accuracy (*)
napi/make_callback n=1000000 type='AsyncResource'          ***     25.77 %     ±12.14%
napi/make_callback n=10000000 type='AsyncResource'         ***     28.75 %     ±10.66%
napi/make_callback n=1000000 type='AsyncResourceName'              -2.48 %     ±11.67%
napi/make_callback n=10000000 type='AsyncResourceName'              8.27 %     ±10.71%
napi/make_callback n=1000000 type='MakeCallback'                    1.15 %     ±11.85%
napi/make_callback n=10000000 type='MakeCallback'                  -3.48 %     ±10.38%

So by function it is still ~27% faster, and by name it costs the same as before. I updated the description: the two Environment lookups and the global handle are saved only on the function overload.

@legendecas legendecas added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 9, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 9, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panva panva added the resume-ci Add this label to resume the latest eligible Jenkins CI run on a PR with an approving review. label Oct 10, 2026
@github-actions github-actions Bot removed the resume-ci Add this label to resume the latest eligible Jenkins CI run on a PR with an approving review. label Oct 10, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nigrosimone

Copy link
Copy Markdown
Contributor Author

CI is green, could someone add commit-queue? Same for #66344.

@nigrosimone

Copy link
Copy Markdown
Contributor Author

@legendecas thanks for the review. CI is green (78290), could you add commit-queue?

@legendecas legendecas added the commit-queue PRs queued for automated landing through the Commit Queue. label Oct 14, 2026
@nodejs-github-bot nodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Oct 14, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Commit Queue failed

This pull request has multiple commits, but no landing policy was selected.

Add commit-queue-squash PRs the Commit Queue should land as one squashed commit. to land it as one commit, or commit-queue-rebase PRs the Commit Queue should land as multiple self-contained commits. to land the commits separately.

The pull request was removed from the Commit Queue and labeled commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. . After resolving the failure, remove that label and add commit-queue PRs queued for automated landing through the Commit Queue. to retry.

Full Commit Queue output
�[36m⠋�[39m Loading data for nodejs/node/pull/66326
�[36m⠋�[39m Loading data for nodejs/node/pull/66326
�[36m⠋�[39m Getting collaborator contacts from README of nodejs/node
�[36m⠋�[39m Getting PR from nodejs/node/pull/66326
�[36m⠋�[39m Getting reviews from nodejs/node/pull/66326
�[36m⠋�[39m Getting comments from nodejs/node/pull/66326
�[36m⠋�[39m Getting commits from nodejs/node/pull/66326
✔  Done loading data for nodejs/node/pull/66326
----------------------------------- PR info ------------------------------------
Title      src: keep ALS store in AsyncResource::MakeCallback (#66326)
Author     Nigro Simone <[email protected]> (@nigrosimone)
Branch     nigrosimone:async-resource-frame -> nodejs:main
Labels     c++, author ready, needs-ci, commit-queue
Commits    3
 - src: keep ALS store in AsyncResource::MakeCallback
 - benchmark: add AsyncResource and Call cases
 - src: run the method getter in the saved frame
Committers 1
 - Nigro Simone <[email protected]>
PR-URL: https://github.com/nodejs/node/pull/66326
Refs: https://github.com/nodejs/node/issues/43038
Refs: https://github.com/nodejs/performance/issues/24
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/66326
Refs: https://github.com/nodejs/node/issues/43038
Refs: https://github.com/nodejs/performance/issues/24
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
--------------------------------------------------------------------------------
   ℹ  This PR was created on Sat, 26 Sep 2026 18:07:19 GMT
   ✔  Approvals: 2
   ✔  - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/66326#pullrequestreview-5406818831
   ✔  - Chengzhong Wu (@legendecas) (TSC): https://github.com/nodejs/node/pull/66326#pullrequestreview-5470104488
   ✔  Last GitHub CI successful
   ℹ  Last Full PR CI on 2026-10-10T08:49:16Z: https://ci.nodejs.org/job/node-test-pull-request/78290/
�[36m⠙�[39m Querying data for job/node-test-pull-request/78290/
�[36m⠙�[39m Querying data for job/node-test-pull-request/78290/
�[36m⠙�[39m Querying API for job/node-test-pull-request/78290/
   ✔  Last Jenkins CI successful
--------------------------------------------------------------------------------
�[36m⠹�[39m Querying API for job/node-test-pull-request/78290/
   ✔  No git cherry-pick in progress
   ✔  No git am in progress
   ✔  No git rebase in progress
--------------------------------------------------------------------------------
�[36m⠹�[39m Querying API for job/node-test-pull-request/78290/
�[36m⠹�[39m Bringing origin/main up to date...
From https://github.com/nodejs/node
 * branch                  main       -> FETCH_HEAD
✔  origin/main is now up-to-date
�[36m⠸�[39m Downloading patch for 66326
�[36m⠸�[39m Downloading patch for 66326
From https://github.com/nodejs/node
 * branch                  refs/pull/66326/merge -> FETCH_HEAD
✔  Fetched commits as 4599b6a0f27f..78d5c35b1d5c
--------------------------------------------------------------------------------
[main 5bb2a76599] src: keep ALS store in AsyncResource::MakeCallback
 Author: Nigro Simone <[email protected]>
 Date: Sat Sep 26 19:51:02 2026 +0200
 2 files changed, 47 insertions(+), 16 deletions(-)
 create mode 100644 test/addons/async-resource/test-async-local-storage.js
[main ebb2fb4b91] benchmark: add AsyncResource and Call cases
 Author: Nigro Simone <[email protected]>
 Date: Sat Sep 26 20:04:28 2026 +0200
 2 files changed, 33 insertions(+), 6 deletions(-)
[main afe0db9e44] src: run the method getter in the saved frame
 Author: Nigro Simone <[email protected]>
 Date: Thu Oct 8 16:15:14 2026 +0200
 2 files changed, 31 insertions(+), 4 deletions(-)
   ✔  Patches applied
There are 3 commits in the PR. Attempting autorebase.
(node:423) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/6)
Executing: git node land --amend --yes
   ⚠  Found Refs: https://github.com/nodejs/node/issues/43038, skipping..
   ⚠  Found Refs: https://github.com/nodejs/performance/issues/24, skipping..
--------------------------------- New Message ----------------------------------
src: keep ALS store in AsyncResource::MakeCallback

AsyncResource saves the async context frame when it is created, and
MakeCallback() enters it with async_context_frame::Scope. Then
node::MakeCallback() opens the callback scope with an undefined frame,
so the callback never runs in the saved one. Since AsyncContextFrame is
the default, AsyncLocalStorage loses its store in these callbacks.

Pass the saved frame to InternalMakeCallback(), as the Node-API
AsyncContext already does. This also removes the Scope from every call,
with its two Environment lookups and its global handle.

Refs: https://github.com/nodejs/node/pull/66316
Refs: https://github.com/nodejs/node/issues/43038
Refs: https://github.com/nodejs/performance/issues/24
Signed-off-by: Nigro Simone <[email protected]>
PR-URL: https://github.com/nodejs/node/pull/66326
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
--------------------------------------------------------------------------------
[detached HEAD 8aa784b56f] src: keep ALS store in AsyncResource::MakeCallback
 Author: Nigro Simone <[email protected]>
 Date: Sat Sep 26 19:51:02 2026 +0200
 2 files changed, 47 insertions(+), 16 deletions(-)
 create mode 100644 test/addons/async-resource/test-async-local-storage.js
Rebasing (3/6)
Rebasing (4/6)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
benchmark: add AsyncResource and Call cases

type=AsyncResource calls node::AsyncResource::MakeCallback() and
type=Call a plain v8::Function::Call, from the same libuv timer as
type=MakeCallback. Call is what the call costs without Node.

Signed-off-by: Nigro Simone <[email protected]>
PR-URL: https://github.com/nodejs/node/pull/66326
Refs: https://github.com/nodejs/node/issues/43038
Refs: https://github.com/nodejs/performance/issues/24
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
--------------------------------------------------------------------------------
[detached HEAD bcffd748f6] benchmark: add AsyncResource and Call cases
 Author: Nigro Simone <[email protected]>
 Date: Sat Sep 26 20:04:28 2026 +0200
 2 files changed, 33 insertions(+), 6 deletions(-)
Rebasing (5/6)
Rebasing (6/6)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
src: run the method getter in the saved frame

Signed-off-by: Nigro Simone <[email protected]>
PR-URL: https://github.com/nodejs/node/pull/66326
Refs: https://github.com/nodejs/node/issues/43038
Refs: https://github.com/nodejs/performance/issues/24
Reviewed-By: Yagiz Nizipli <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
--------------------------------------------------------------------------------
[detached HEAD 4c33151bbf] src: run the method getter in the saved frame
 Author: Nigro Simone <[email protected]>
 Date: Thu Oct 8 16:15:14 2026 +0200
 2 files changed, 31 insertions(+), 4 deletions(-)
Successfully rebased and updated refs/heads/main.
--------------------------------------------------------------------------------
   ℹ  Add `commit-queue-squash` label to land the PR as one commit, or `commit-queue-rebase` to land as separate commits.

View workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs with CI started, the required approvals, and no outstanding review comments. c++ Issues and PRs that require attention from people who are familiar with C++. commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants