Skip to content

Commit 8344ac3

Browse files
committed
src: skip the async id stack when nobody sees it
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>
1 parent f4d2742 commit 8344ac3

7 files changed

Lines changed: 144 additions & 5 deletions

File tree

‎lib/internal/async_hooks.js‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const {
5252
pushAsyncContext: pushAsyncContext_,
5353
popAsyncContext: popAsyncContext_,
5454
executionAsyncResource: executionAsyncResource_,
55+
lazyExecutionAsyncResource,
5556
clearAsyncIdStack,
5657
} = async_wrap;
5758
// Properties in active_hooks are used to keep track of the set of hooks being
@@ -91,6 +92,7 @@ const {
9192
kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
9293
kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId,
9394
kDefaultTriggerAsyncId, kStackLength, kUsesExecutionAsyncResource,
95+
kLazyScopes,
9496
} = async_wrap.constants;
9597

9698
const { async_id_symbol,
@@ -136,6 +138,12 @@ function executionAsyncResource() {
136138
async_hook_fields[kUsesExecutionAsyncResource] = 1;
137139

138140
const index = async_hook_fields[kStackLength] - 1;
141+
// A native callback that started before anybody used this function did not
142+
// push its resource on the stack.
143+
if (async_hook_fields[kLazyScopes] !== 0) {
144+
const lazy = lazyExecutionAsyncResource(index + 1);
145+
if (lazy !== undefined) return lookupPublicResource(lazy);
146+
}
139147
if (index === -1) return topLevelResource;
140148
const resource = execution_async_resources[index] ||
141149
executionAsyncResource_(index);
@@ -550,7 +558,15 @@ function pushAsyncContext(asyncId, triggerAsyncId, resource) {
550558
// This is the equivalent of the native pop_async_ids() call.
551559
function popAsyncContext(asyncId) {
552560
const stackLength = async_hook_fields[kStackLength];
553-
if (stackLength === 0) return false;
561+
if (stackLength === 0) {
562+
// A native callback that skipped the stack still has its id checked.
563+
if (async_hook_fields[kLazyScopes] !== 0 &&
564+
async_hook_fields[kCheck] > 0 &&
565+
async_id_fields[kExecutionAsyncId] !== asyncId) {
566+
return popAsyncContext_(asyncId);
567+
}
568+
return false;
569+
}
554570

555571
if (async_hook_fields[kCheck] > 0 && async_id_fields[kExecutionAsyncId] !== asyncId) {
556572
// Do the same thing as the native code (i.e. crash hard).

‎src/api/callback.cc‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,27 @@ InternalCallbackScope::InternalCallbackScope(
118118
prior_context_frame_.Reset(isolate, prior_context_frame);
119119
}
120120

121-
env->async_hooks()->push_async_context(
122-
async_context_.async_id, async_context_.trigger_async_id, object);
121+
AsyncHooks* hooks = env->async_hooks();
122+
if (hooks->fields()[AsyncHooks::kTotals] == 0 &&
123+
hooks->fields()[AsyncHooks::kUsesExecutionAsyncResource] == 0)
124+
[[likely]] {
125+
// Nobody can see the id stack: swap the ids in place, they are all that
126+
// executionAsyncId() and triggerAsyncId() read.
127+
AliasedFloat64Array& ids = hooks->async_id_fields();
128+
prior_async_id_ = ids[AsyncHooks::kExecutionAsyncId];
129+
prior_trigger_async_id_ = ids[AsyncHooks::kTriggerAsyncId];
130+
ids[AsyncHooks::kExecutionAsyncId] = async_context_.async_id;
131+
ids[AsyncHooks::kTriggerAsyncId] = async_context_.trigger_async_id;
132+
lazy_depth_ = hooks->fields()[AsyncHooks::kStackLength];
133+
lazy_resource_ = object;
134+
lazy_prev_ = hooks->lazy_top_;
135+
hooks->lazy_top_ = this;
136+
hooks->fields()[AsyncHooks::kLazyScopes] += 1;
137+
lazy_ids_ = true;
138+
} else {
139+
hooks->push_async_context(
140+
async_context_.async_id, async_context_.trigger_async_id, object);
141+
}
123142

124143
pushed_ids_ = true;
125144

@@ -130,6 +149,13 @@ InternalCallbackScope::InternalCallbackScope(
130149
}
131150
}
132151

152+
Local<Object> InternalCallbackScope::lazy_resource(Isolate* isolate) const {
153+
if (std::holds_alternative<Local<Object>*>(lazy_resource_)) {
154+
return *std::get<Local<Object>*>(lazy_resource_);
155+
}
156+
return std::get<Global<Object>*>(lazy_resource_)->Get(isolate);
157+
}
158+
133159
InternalCallbackScope::~InternalCallbackScope() {
134160
Close();
135161
env_->PopAsyncCallbackScope();
@@ -160,7 +186,20 @@ void InternalCallbackScope::Close() {
160186
}
161187

162188
if (pushed_ids_) {
163-
env_->async_hooks()->pop_async_context(async_context_.async_id);
189+
AsyncHooks* hooks = env_->async_hooks();
190+
if (lazy_ids_) {
191+
// clear_async_id_stack() may have dropped the chain already.
192+
if (hooks->lazy_top_ == this) {
193+
hooks->CheckLazyClose(async_context_.async_id, lazy_depth_);
194+
hooks->lazy_top_ = lazy_prev_;
195+
hooks->fields()[AsyncHooks::kLazyScopes] -= 1;
196+
}
197+
AliasedFloat64Array& ids = hooks->async_id_fields();
198+
ids[AsyncHooks::kExecutionAsyncId] = prior_async_id_;
199+
ids[AsyncHooks::kTriggerAsyncId] = prior_trigger_async_id_;
200+
} else {
201+
hooks->pop_async_context(async_context_.async_id);
202+
}
164203

165204
async_context_frame::set(env_, prior_context_frame_.Get(isolate));
166205
}

‎src/async_wrap.cc‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "env-inl.h"
2626
#include "node_errors.h"
2727
#include "node_external_reference.h"
28+
#include "node_internals.h"
2829
#include "tracing/traced_value.h"
2930
#include "util-inl.h"
3031

@@ -290,6 +291,17 @@ void AsyncWrap::PopAsyncContext(const FunctionCallbackInfo<Value>& args) {
290291
args.GetReturnValue().Set(env->async_hooks()->pop_async_context(async_id));
291292
}
292293

294+
// The resource of the innermost scope that skipped the id stack, if nothing
295+
// was pushed on the stack after it (depth is the stack length JS sees).
296+
static void LazyExecutionAsyncResource(
297+
const v8::FunctionCallbackInfo<v8::Value>& args) {
298+
Environment* env = Environment::GetCurrent(args);
299+
uint32_t depth = args[0].As<v8::Uint32>()->Value();
300+
InternalCallbackScope* scope = env->async_hooks()->lazy_top_;
301+
if (scope != nullptr && scope->lazy_depth() == depth) {
302+
args.GetReturnValue().Set(scope->lazy_resource(env->isolate()));
303+
}
304+
}
293305

294306
void AsyncWrap::ExecutionAsyncResource(
295307
const FunctionCallbackInfo<Value>& args) {
@@ -396,6 +408,10 @@ void AsyncWrap::CreatePerIsolateProperties(IsolateData* isolate_data,
396408
SetMethod(isolate, target, "pushAsyncContext", PushAsyncContext);
397409
SetMethod(isolate, target, "popAsyncContext", PopAsyncContext);
398410
SetMethod(isolate, target, "executionAsyncResource", ExecutionAsyncResource);
411+
SetMethod(isolate,
412+
target,
413+
"lazyExecutionAsyncResource",
414+
LazyExecutionAsyncResource);
399415
SetMethod(isolate, target, "clearAsyncIdStack", ClearAsyncIdStack);
400416
SetMethod(isolate, target, "queueDestroyAsyncId", QueueDestroyAsyncId);
401417
SetMethod(isolate, target, "setPromiseHooks", SetPromiseHooks);
@@ -470,6 +486,7 @@ void AsyncWrap::CreatePerContextProperties(Local<Object> target,
470486
SET_HOOKS_CONSTANT(kDefaultTriggerAsyncId);
471487
SET_HOOKS_CONSTANT(kUsesExecutionAsyncResource);
472488
SET_HOOKS_CONSTANT(kStackLength);
489+
SET_HOOKS_CONSTANT(kLazyScopes);
473490
#undef SET_HOOKS_CONSTANT
474491
FORCE_SET_TARGET_FIELD(target, "constants", constants);
475492

@@ -502,6 +519,7 @@ void AsyncWrap::RegisterExternalReferences(
502519
registry->Register(PushAsyncContext);
503520
registry->Register(PopAsyncContext);
504521
registry->Register(ExecutionAsyncResource);
522+
registry->Register(LazyExecutionAsyncResource);
505523
registry->Register(ClearAsyncIdStack);
506524
registry->Register(QueueDestroyAsyncId);
507525
registry->Register(SetPromiseHooks);

‎src/env.cc‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,12 +167,25 @@ void AsyncHooks::push_async_context(
167167
}
168168
}
169169

170+
void AsyncHooks::CheckLazyClose(double async_id, uint32_t depth) {
171+
if (fields_[kCheck] > 0 && (async_id_fields_[kExecutionAsyncId] != async_id ||
172+
fields_[kStackLength] != depth)) [[unlikely]] {
173+
FailWithCorruptedAsyncStack(async_id);
174+
}
175+
}
176+
170177
// Remember to keep this code aligned with popAsyncContext() in JS.
171178
bool AsyncHooks::pop_async_context(double async_id) {
172179
// In case of an exception then this may have already been reset, if the
173180
// stack was multiple MakeCallback()'s deep.
174-
if (fields_[kStackLength] == 0) [[unlikely]]
181+
if (fields_[kStackLength] == 0) [[unlikely]] {
182+
// A scope that skipped the stack still has its id checked.
183+
if (fields_[kLazyScopes] > 0 && fields_[kCheck] > 0 &&
184+
async_id_fields_[kExecutionAsyncId] != async_id) {
185+
FailWithCorruptedAsyncStack(async_id);
186+
}
175187
return false;
188+
}
176189

177190
// Ask for the async_id to be restored as a check that the stack
178191
// hasn't been corrupted.
@@ -227,6 +240,8 @@ void AsyncHooks::clear_async_id_stack() {
227240
async_id_fields_[kExecutionAsyncId] = 0;
228241
async_id_fields_[kTriggerAsyncId] = 0;
229242
fields_[kStackLength] = 0;
243+
lazy_top_ = nullptr;
244+
fields_[kLazyScopes] = 0;
230245
}
231246

232247
void AsyncHooks::InstallPromiseHooks(Local<Context> ctx) {

‎src/env.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,8 @@ extern std::shared_ptr<KVStore> system_environment;
368368

369369
struct EnvSerializeInfo;
370370

371+
class InternalCallbackScope;
372+
371373
class AsyncHooks : public MemoryRetainer {
372374
public:
373375
SET_MEMORY_INFO_NAME(AsyncHooks)
@@ -386,6 +388,7 @@ class AsyncHooks : public MemoryRetainer {
386388
kCheck,
387389
kStackLength,
388390
kUsesExecutionAsyncResource,
391+
kLazyScopes,
389392
kFieldsCount,
390393
};
391394

@@ -502,6 +505,13 @@ class AsyncHooks : public MemoryRetainer {
502505
const SerializeInfo* info_ = nullptr;
503506

504507
std::array<v8::Global<v8::Function>, 4> js_promise_hooks_;
508+
509+
public:
510+
// Innermost InternalCallbackScope that swapped the ids without the stack,
511+
// for executionAsyncResource(). Counted in fields_[kLazyScopes].
512+
InternalCallbackScope* lazy_top_ = nullptr;
513+
// Close of a scope that skipped the stack: the same check as pop.
514+
void CheckLazyClose(double async_id, uint32_t depth);
505515
};
506516

507517
class ImmediateInfo : public MemoryRetainer {

‎src/node_internals.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ class InternalCallbackScope {
270270
inline bool Failed() const { return failed_; }
271271
inline void MarkAsFailed() { failed_ = true; }
272272

273+
// For executionAsyncResource() inside a scope that skipped the id stack.
274+
inline uint32_t lazy_depth() const { return lazy_depth_; }
275+
v8::Local<v8::Object> lazy_resource(v8::Isolate* isolate) const;
276+
273277
private:
274278
Environment* env_;
275279
async_context async_context_;
@@ -279,6 +283,12 @@ class InternalCallbackScope {
279283
bool failed_ = false;
280284
bool pushed_ids_ = false;
281285
bool closed_ = false;
286+
bool lazy_ids_ = false;
287+
uint32_t lazy_depth_ = 0;
288+
double prior_async_id_ = 0;
289+
double prior_trigger_async_id_ = 0;
290+
InternalCallbackScope* lazy_prev_ = nullptr;
291+
std::variant<v8::Local<v8::Object>*, v8::Global<v8::Object>*> lazy_resource_;
282292
v8::Global<v8::Value> prior_context_frame_;
283293
std::optional<v8::Isolate::AllowJavascriptExecutionScope> allow_js_;
284294
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const assert = require('assert');
5+
const binding = require(`./build/${common.buildType}/binding`);
6+
const { executionAsyncId, executionAsyncResource } = require('async_hooks');
7+
8+
// No hook is enabled and executionAsyncResource() was never called, so the
9+
// callback scope of AsyncResource::MakeCallback() skips the async id stack.
10+
// executionAsyncResource() must still find the resource, and executionAsyncId()
11+
// must still be the id of the callback, also around a nested callback.
12+
let calls = 0;
13+
const object = {
14+
methöd: common.mustCall(function() {
15+
assert.strictEqual(executionAsyncId(), uid);
16+
assert.strictEqual(executionAsyncResource(), object);
17+
if (calls++ === 0) {
18+
assert.strictEqual(binding.callViaFunction(resource), 'baz');
19+
assert.strictEqual(executionAsyncId(), uid);
20+
assert.strictEqual(executionAsyncResource(), object);
21+
}
22+
return 'baz';
23+
}, 2),
24+
};
25+
const resource = binding.createAsyncResource(object);
26+
const uid = binding.getAsyncId(resource);
27+
const outerId = executionAsyncId();
28+
29+
assert.strictEqual(binding.callViaFunction(resource), 'baz');
30+
assert.strictEqual(executionAsyncId(), outerId);
31+
binding.destroyAsyncResource(resource);

0 commit comments

Comments
 (0)