Repository navigation
src: keep ALS store in AsyncResource::MakeCallback - #66326
nigrosimone wants to merge 3 commits into
Conversation
|
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. |
4a646e3 to
cf6df06
Compare
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 <nigro.simone@gmail.com>
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 <nigro.simone@gmail.com>
cf6df06 to
1212c0d
Compare
Codecov Report❌ Patch coverage is
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
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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.| if (!env_->can_call_into_js()) return {}; | ||
| Local<Value> callback; | ||
| if (!get_resource() | ||
| ->Get(isolate->GetCurrentContext(), symbol) |
There was a problem hiding this comment.
This could call into JS and should be scoped with a async_context_frame::Scope.
Signed-off-by: Nigro Simone <nigro.simone@gmail.com>
|
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. |
|
Would you mind confirming if the performance gain still preserve after the change? IIUC it changes the claim in the OP. |
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. The Scope goes away, so every call also saves two Environment lookups and a global handle.
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:
Refs: #43038
Refs: nodejs/performance#24
Disclosure: I used Opus 5.5 (Max) as coding assistant