Skip to content

Commit a0ce8f1

Browse files
authored
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: #66316 Refs: #43038 Refs: nodejs/performance#24 Signed-off-by: Nigro Simone <nigro.simone@gmail.com> PR-URL: #66326 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 04fb145 commit a0ce8f1

4 files changed

Lines changed: 107 additions & 22 deletions

File tree

‎benchmark/napi/make_callback/binding.cc‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ using v8::Local;
1212
using v8::Object;
1313
using v8::Value;
1414

15+
// Same order as the types in index.js.
16+
enum Type { kMakeCallback, kAsyncResource, kCall };
17+
1518
struct State {
1619
uv_timer_t timer;
1720
Isolate* isolate;
1821
int64_t n;
22+
Type type;
1923
Global<Function> fn;
2024
Global<Function> done;
25+
node::AsyncResource* resource = nullptr;
26+
27+
~State() { delete resource; }
2128
};
2229

2330
static void OnTimer(uv_timer_t* handle) {
@@ -30,22 +37,38 @@ static void OnTimer(uv_timer_t* handle) {
3037
Local<Object> recv = context->Global();
3138
for (int64_t i = 0; i < state->n; i++) {
3239
HandleScope inner_scope(isolate);
33-
(void)node::MakeCallback(isolate, recv, fn, 0, nullptr, {0, 0});
40+
switch (state->type) {
41+
case kMakeCallback:
42+
(void)node::MakeCallback(isolate, recv, fn, 0, nullptr, {0, 0});
43+
break;
44+
case kAsyncResource:
45+
(void)state->resource->MakeCallback(fn, 0, nullptr);
46+
break;
47+
case kCall:
48+
(void)fn->Call(context, recv, 0, nullptr);
49+
break;
50+
}
3451
}
3552
Local<Function> done = state->done.Get(isolate);
3653
(void)node::MakeCallback(isolate, recv, done, 0, nullptr, {0, 0});
3754
uv_close(reinterpret_cast<uv_handle_t*>(&state->timer),
3855
[](uv_handle_t* h) { delete static_cast<State*>(h->data); });
3956
}
4057

41-
// run(n, fn, done): calls fn n times from a timer, then calls done.
58+
// run(n, fn, done, type): calls fn n times from a timer, then calls done.
4259
static void Run(const FunctionCallbackInfo<Value>& args) {
4360
Isolate* isolate = args.GetIsolate();
4461
State* state = new State;
4562
state->isolate = isolate;
4663
state->n = args[0]->IntegerValue(isolate->GetCurrentContext()).FromJust();
4764
state->fn.Reset(isolate, args[1].As<Function>());
4865
state->done.Reset(isolate, args[2].As<Function>());
66+
state->type = static_cast<Type>(
67+
args[3]->Int32Value(isolate->GetCurrentContext()).FromJust());
68+
if (state->type == kAsyncResource) {
69+
state->resource =
70+
new node::AsyncResource(isolate, Object::New(isolate), "Benchmark");
71+
}
4972
state->timer.data = state;
5073
uv_timer_init(node::GetCurrentEventLoop(isolate), &state->timer);
5174
uv_timer_start(&state->timer, OnTimer, 0, 0);

‎benchmark/napi/make_callback/index.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
const common = require('../../common.js');
44

5-
// The addon calls into JS with node::MakeCallback from a libuv timer, so
6-
// every call opens a top-level callback scope, like an I/O callback does.
5+
// The addon calls into JS from a libuv timer, so every call opens a top-level
6+
// callback scope, like an I/O callback does. type=Call is a plain
7+
// v8::Function::Call, what the call costs without Node.
78

89
let binding;
910
try {
@@ -13,11 +14,14 @@ try {
1314
process.exit(0);
1415
}
1516

17+
const types = ['MakeCallback', 'AsyncResource', 'Call'];
18+
1619
const bench = common.createBenchmark(main, {
20+
type: types,
1721
n: [1e6, 1e7],
1822
});
1923

20-
function main({ n }) {
24+
function main({ type, n }) {
2125
bench.start();
22-
binding.run(n, () => {}, () => bench.end(n));
26+
binding.run(n, () => {}, () => bench.end(n), types.indexOf(type));
2327
}

‎src/api/async_resource.cc‎

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "async_context_frame.h"
22
#include "env-inl.h"
33
#include "node.h"
4+
#include "node_internals.h"
45

56
namespace node {
67

@@ -10,6 +11,7 @@ using v8::Local;
1011
using v8::MaybeLocal;
1112
using v8::Object;
1213
using v8::String;
14+
using v8::Undefined;
1315
using v8::Value;
1416

1517
AsyncResource::AsyncResource(Isolate* isolate,
@@ -39,33 +41,45 @@ MaybeLocal<Value> AsyncResource::MakeCallback(Local<Function> callback,
3941
int argc,
4042
Local<Value>* argv) {
4143
auto isolate = env_->isolate();
42-
async_context_frame::Scope async_context_frame_scope(
43-
isolate, context_frame_.Get(isolate));
44-
45-
return node::MakeCallback(
46-
isolate, get_resource(), callback, argc, argv, async_context_);
44+
// As in Node-API: node::MakeCallback() would run it with no frame.
45+
return InternalMakeCallback(isolate,
46+
get_resource(),
47+
callback,
48+
argc,
49+
argv,
50+
async_context_,
51+
context_frame_.Get(isolate));
4752
}
4853

4954
MaybeLocal<Value> AsyncResource::MakeCallback(const char* method,
5055
int argc,
5156
Local<Value>* argv) {
52-
auto isolate = env_->isolate();
53-
async_context_frame::Scope async_context_frame_scope(
54-
isolate, context_frame_.Get(isolate));
55-
56-
return node::MakeCallback(
57-
isolate, get_resource(), method, argc, argv, async_context_);
57+
Local<String> method_string;
58+
if (!String::NewFromUtf8(env_->isolate(), method).ToLocal(&method_string)) {
59+
return {};
60+
}
61+
return MakeCallback(method_string, argc, argv);
5862
}
5963

6064
MaybeLocal<Value> AsyncResource::MakeCallback(Local<String> symbol,
6165
int argc,
6266
Local<Value>* argv) {
6367
auto isolate = env_->isolate();
64-
async_context_frame::Scope async_context_frame_scope(
65-
isolate, context_frame_.Get(isolate));
66-
67-
return node::MakeCallback(
68-
isolate, get_resource(), symbol, argc, argv, async_context_);
68+
// Check can_call_into_js() first because calling Get() might do so.
69+
if (!env_->can_call_into_js()) return {};
70+
Local<Value> callback;
71+
{
72+
// A getter runs in the saved frame too, so what it enters stays there.
73+
async_context_frame::Scope async_context_frame_scope(
74+
isolate, context_frame_.Get(isolate));
75+
if (!get_resource()
76+
->Get(isolate->GetCurrentContext(), symbol)
77+
.ToLocal(&callback)) {
78+
return {};
79+
}
80+
}
81+
if (!callback->IsFunction()) return Undefined(isolate);
82+
return MakeCallback(callback.As<Function>(), argc, argv);
6983
}
7084

7185
Local<Object> AsyncResource::get_resource() {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const assert = require('assert');
5+
const { AsyncLocalStorage } = require('async_hooks');
6+
const binding = require(`./build/${common.buildType}/binding`);
7+
8+
// AsyncResource::MakeCallback() must run the callback in the async context
9+
// frame that was active when the resource was created.
10+
11+
const als = new AsyncLocalStorage();
12+
const object = {
13+
'methöd': common.mustCall(() => {
14+
assert.strictEqual(als.getStore(), 'store');
15+
}, 3),
16+
};
17+
const resource = als.run('store', () => binding.createAsyncResource(object));
18+
19+
binding.callViaFunction(resource);
20+
binding.callViaString(resource);
21+
binding.callViaUtf8Name(resource);
22+
binding.destroyAsyncResource(resource);
23+
24+
// A getter of the method runs in that frame too, and what it enters stays
25+
// there.
26+
{
27+
let getterStore;
28+
const object = Object.defineProperty({}, 'methöd', {
29+
get: common.mustCall(() => {
30+
getterStore = als.getStore();
31+
als.enterWith('getter');
32+
return common.mustCall();
33+
}),
34+
});
35+
const resource =
36+
als.run('resource', () => binding.createAsyncResource(object));
37+
let callerStore;
38+
als.run('caller', () => {
39+
binding.callViaString(resource);
40+
callerStore = als.getStore();
41+
});
42+
binding.destroyAsyncResource(resource);
43+
assert.deepStrictEqual([getterStore, callerStore], ['resource', 'caller']);
44+
}

0 commit comments

Comments
 (0)