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 <nigro.simone@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66326
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
Nigro Simone authored and GitHub committed 2026-10-14 01:57:03 +00:00
1 parent 04fb145b6a
commit a0ce8f152b
4 files changed
+107 -22

No files matched your search

+25 -2
View File
@@ -12,12 +12,19 @@ using v8::Local;
using v8::Object;
using v8::Value;
// Same order as the types in index.js.
enum Type { kMakeCallback, kAsyncResource, kCall };
struct State {
uv_timer_t timer;
Isolate* isolate;
int64_t n;
Type type;
Global<Function> fn;
Global<Function> done;
node::AsyncResource* resource = nullptr;
~State() { delete resource; }
};
static void OnTimer(uv_timer_t* handle) {
@@ -30,7 +37,17 @@ static void OnTimer(uv_timer_t* handle) {
Local<Object> recv = context->Global();
for (int64_t i = 0; i < state->n; i++) {
HandleScope inner_scope(isolate);
(void)node::MakeCallback(isolate, recv, fn, 0, nullptr, {0, 0});
switch (state->type) {
case kMakeCallback:
(void)node::MakeCallback(isolate, recv, fn, 0, nullptr, {0, 0});
break;
case kAsyncResource:
(void)state->resource->MakeCallback(fn, 0, nullptr);
break;
case kCall:
(void)fn->Call(context, recv, 0, nullptr);
break;
}
}
Local<Function> done = state->done.Get(isolate);
(void)node::MakeCallback(isolate, recv, done, 0, nullptr, {0, 0});
@@ -38,7 +55,7 @@ static void OnTimer(uv_timer_t* handle) {
[](uv_handle_t* h) { delete static_cast<State*>(h->data); });
}
// run(n, fn, done): calls fn n times from a timer, then calls done.
// run(n, fn, done, type): calls fn n times from a timer, then calls done.
static void Run(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
State* state = new State;
@@ -46,6 +63,12 @@ static void Run(const FunctionCallbackInfo<Value>& args) {
state->n = args[0]->IntegerValue(isolate->GetCurrentContext()).FromJust();
state->fn.Reset(isolate, args[1].As<Function>());
state->done.Reset(isolate, args[2].As<Function>());
state->type = static_cast<Type>(
args[3]->Int32Value(isolate->GetCurrentContext()).FromJust());
if (state->type == kAsyncResource) {
state->resource =
new node::AsyncResource(isolate, Object::New(isolate), "Benchmark");
}
state->timer.data = state;
uv_timer_init(node::GetCurrentEventLoop(isolate), &state->timer);
uv_timer_start(&state->timer, OnTimer, 0, 0);
+8 -4
View File
@@ -2,8 +2,9 @@
const common = require('../../common.js');
// The addon calls into JS with node::MakeCallback from a libuv timer, so
// every call opens a top-level callback scope, like an I/O callback does.
// The addon calls into JS from a libuv timer, so every call opens a top-level
// callback scope, like an I/O callback does. type=Call is a plain
// v8::Function::Call, what the call costs without Node.
let binding;
try {
@@ -13,11 +14,14 @@ try {
process.exit(0);
}
const types = ['MakeCallback', 'AsyncResource', 'Call'];
const bench = common.createBenchmark(main, {
type: types,
n: [1e6, 1e7],
});
function main({ n }) {
function main({ type, n }) {
bench.start();
binding.run(n, () => {}, () => bench.end(n));
binding.run(n, () => {}, () => bench.end(n), types.indexOf(type));
}