Skip to content

Commit 0be006e

Browse files
committed
async_hooks: improve resource stack performance
Removes some of the performance overhead that came with `executionAsyncResource()` by using the JS resource array only as a cache for the values provided by C++. The fact that we now use an entry trampoline is used to pass the resource without requiring extra C++/JS boundary crossings, and the direct accesses to the JS resource array from C++ are removed in all fast paths. This particularly improves performance when async hooks are not being used. This is a continuation of #33575 and shares some of its code with it. ./node benchmark/compare.js --new ./node --old ./node-master --runs 30 --filter messageport worker | Rscript benchmark/compare.R [00:06:14|% 100| 1/1 files | 60/60 runs | 2/2 configs]: Done confidence improvement accuracy (*) (**) (***) worker/messageport.js n=1000000 payload='object' ** 12.64 % ±7.30% ±9.72% ±12.65% worker/messageport.js n=1000000 payload='string' * 11.08 % ±9.00% ±11.98% ±15.59% ./node benchmark/compare.js --new ./node --old ./node-master --runs 20 --filter async-resource-vs-destroy async_hooks | Rscript benchmark/compare.R [00:22:35|% 100| 1/1 files | 40/40 runs | 6/6 configs]: Done confidence improvement accuracy (*) async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='async' type='async-local-storage' benchmarker='autocannon' 1.60 % ±7.35% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='async' type='async-resource' benchmarker='autocannon' 6.05 % ±6.57% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='async' type='destroy' benchmarker='autocannon' * 8.27 % ±7.50% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='callbacks' type='async-local-storage' benchmarker='autocannon' 7.42 % ±8.22% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='callbacks' type='async-resource' benchmarker='autocannon' 4.33 % ±7.84% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='callbacks' type='destroy' benchmarker='autocannon' 5.96 % ±7.15% (**) (***) async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='async' type='async-local-storage' benchmarker='autocannon' ±9.84% ±12.94% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='async' type='async-resource' benchmarker='autocannon' ±8.81% ±11.60% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='async' type='destroy' benchmarker='autocannon' ±10.07% ±13.28% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='callbacks' type='async-local-storage' benchmarker='autocannon' ±11.01% ±14.48% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='callbacks' type='async-resource' benchmarker='autocannon' ±10.50% ±13.81% async_hooks/async-resource-vs-destroy.js n=1000000 duration=5 connections=500 path='/' asyncMethod='callbacks' type='destroy' benchmarker='autocannon' ±9.58% ±12.62% Refs: #33575 PR-URL: #34319 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Gerhard Stöbich <[email protected]> Reviewed-By: Stephen Belanger <[email protected]>
1 parent 10ad873 commit 0be006e

File tree

7 files changed

+129
-44
lines changed

7 files changed

+129
-44
lines changed

lib/internal/async_hooks.js

+18-15
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ const {
4848
// each hook's after() callback.
4949
const {
5050
pushAsyncContext: pushAsyncContext_,
51-
popAsyncContext: popAsyncContext_
51+
popAsyncContext: popAsyncContext_,
52+
executionAsyncResource: executionAsyncResource_,
53+
clearAsyncIdStack,
5254
} = async_wrap;
5355
// For performance reasons, only track Promises when a hook is enabled.
5456
const { enablePromiseHook, disablePromiseHook } = async_wrap;
@@ -87,7 +89,8 @@ const { resource_symbol, owner_symbol } = internalBinding('symbols');
8789
// for a given step, that step can bail out early.
8890
const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
8991
kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId,
90-
kDefaultTriggerAsyncId, kStackLength } = async_wrap.constants;
92+
kDefaultTriggerAsyncId, kStackLength, kUsesExecutionAsyncResource
93+
} = async_wrap.constants;
9194

9295
// Used in AsyncHook and AsyncResource.
9396
const async_id_symbol = Symbol('asyncId');
@@ -108,7 +111,10 @@ function useDomainTrampoline(fn) {
108111
domain_cb = fn;
109112
}
110113

111-
function callbackTrampoline(asyncId, cb, ...args) {
114+
function callbackTrampoline(asyncId, resource, cb, ...args) {
115+
const index = async_hook_fields[kStackLength] - 1;
116+
execution_async_resources[index] = resource;
117+
112118
if (asyncId !== 0 && hasHooks(kBefore))
113119
emitBeforeNative(asyncId);
114120

@@ -123,6 +129,7 @@ function callbackTrampoline(asyncId, cb, ...args) {
123129
if (asyncId !== 0 && hasHooks(kAfter))
124130
emitAfterNative(asyncId);
125131

132+
execution_async_resources.pop();
126133
return result;
127134
}
128135

@@ -131,9 +138,15 @@ setCallbackTrampoline(callbackTrampoline);
131138
const topLevelResource = {};
132139

133140
function executionAsyncResource() {
141+
// Indicate to the native layer that this function is likely to be used,
142+
// in which case it will inform JS about the current async resource via
143+
// the trampoline above.
144+
async_hook_fields[kUsesExecutionAsyncResource] = 1;
145+
134146
const index = async_hook_fields[kStackLength] - 1;
135147
if (index === -1) return topLevelResource;
136-
const resource = execution_async_resources[index];
148+
const resource = execution_async_resources[index] ||
149+
executionAsyncResource_(index);
137150
return lookupPublicResource(resource);
138151
}
139152

@@ -413,16 +426,6 @@ function emitDestroyScript(asyncId) {
413426
}
414427

415428

416-
// Keep in sync with Environment::AsyncHooks::clear_async_id_stack
417-
// in src/env-inl.h.
418-
function clearAsyncIdStack() {
419-
async_id_fields[kExecutionAsyncId] = 0;
420-
async_id_fields[kTriggerAsyncId] = 0;
421-
async_hook_fields[kStackLength] = 0;
422-
execution_async_resources.splice(0, execution_async_resources.length);
423-
}
424-
425-
426429
function hasAsyncIdStack() {
427430
return hasHooks(kStackLength);
428431
}
@@ -432,7 +435,7 @@ function hasAsyncIdStack() {
432435
function pushAsyncContext(asyncId, triggerAsyncId, resource) {
433436
const offset = async_hook_fields[kStackLength];
434437
if (offset * 2 >= async_wrap.async_ids_stack.length)
435-
return pushAsyncContext_(asyncId, triggerAsyncId, resource);
438+
return pushAsyncContext_(asyncId, triggerAsyncId);
436439
async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
437440
async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
438441
execution_async_resources[offset] = resource;

lib/internal/process/execution.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,10 @@ function createOnGlobalUncaughtException() {
186186
do {
187187
emitAfter(executionAsyncId());
188188
} while (hasAsyncIdStack());
189-
// Or completely empty the id stack.
190-
} else {
191-
clearAsyncIdStack();
192189
}
190+
// And completely empty the id stack, including anything that may be
191+
// cached on the native side.
192+
clearAsyncIdStack();
193193

194194
return true;
195195
};

src/api/callback.cc

+13-8
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,16 @@ MaybeLocal<Value> InternalMakeCallback(Environment* env,
160160

161161
Local<Function> hook_cb = env->async_hooks_callback_trampoline();
162162
int flags = InternalCallbackScope::kNoFlags;
163-
int hook_count = 0;
163+
bool use_async_hooks_trampoline = false;
164+
AsyncHooks* async_hooks = env->async_hooks();
164165
if (!hook_cb.IsEmpty()) {
166+
// Use the callback trampoline if there are any before or after hooks, or
167+
// we can expect some kind of usage of async_hooks.executionAsyncResource().
165168
flags = InternalCallbackScope::kSkipAsyncHooks;
166-
AsyncHooks* async_hooks = env->async_hooks();
167-
hook_count = async_hooks->fields()[AsyncHooks::kBefore] +
168-
async_hooks->fields()[AsyncHooks::kAfter];
169+
use_async_hooks_trampoline =
170+
async_hooks->fields()[AsyncHooks::kBefore] +
171+
async_hooks->fields()[AsyncHooks::kAfter] +
172+
async_hooks->fields()[AsyncHooks::kUsesExecutionAsyncResource] > 0;
169173
}
170174

171175
InternalCallbackScope scope(env, resource, asyncContext, flags);
@@ -175,12 +179,13 @@ MaybeLocal<Value> InternalMakeCallback(Environment* env,
175179

176180
MaybeLocal<Value> ret;
177181

178-
if (hook_count != 0) {
179-
MaybeStackBuffer<Local<Value>, 16> args(2 + argc);
182+
if (use_async_hooks_trampoline) {
183+
MaybeStackBuffer<Local<Value>, 16> args(3 + argc);
180184
args[0] = v8::Number::New(env->isolate(), asyncContext.async_id);
181-
args[1] = callback;
185+
args[1] = resource;
186+
args[2] = callback;
182187
for (int i = 0; i < argc; i++) {
183-
args[i + 2] = argv[i];
188+
args[i + 3] = argv[i];
184189
}
185190
ret = hook_cb->Call(env->context(), recv, args.length(), &args[0]);
186191
} else {

src/async_wrap.cc

+21-2
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ void AsyncWrap::PushAsyncContext(const FunctionCallbackInfo<Value>& args) {
419419
// then the checks in push_async_ids() and pop_async_id() will.
420420
double async_id = args[0]->NumberValue(env->context()).FromJust();
421421
double trigger_async_id = args[1]->NumberValue(env->context()).FromJust();
422-
env->async_hooks()->push_async_context(async_id, trigger_async_id, args[2]);
422+
env->async_hooks()->push_async_context(async_id, trigger_async_id, {});
423423
}
424424

425425

@@ -430,6 +430,22 @@ void AsyncWrap::PopAsyncContext(const FunctionCallbackInfo<Value>& args) {
430430
}
431431

432432

433+
void AsyncWrap::ExecutionAsyncResource(
434+
const FunctionCallbackInfo<Value>& args) {
435+
Environment* env = Environment::GetCurrent(args);
436+
uint32_t index;
437+
if (!args[0]->Uint32Value(env->context()).To(&index)) return;
438+
args.GetReturnValue().Set(
439+
env->async_hooks()->native_execution_async_resource(index));
440+
}
441+
442+
443+
void AsyncWrap::ClearAsyncIdStack(const FunctionCallbackInfo<Value>& args) {
444+
Environment* env = Environment::GetCurrent(args);
445+
env->async_hooks()->clear_async_id_stack();
446+
}
447+
448+
433449
void AsyncWrap::AsyncReset(const FunctionCallbackInfo<Value>& args) {
434450
CHECK(args[0]->IsObject());
435451

@@ -502,6 +518,8 @@ void AsyncWrap::Initialize(Local<Object> target,
502518
env->SetMethod(target, "setCallbackTrampoline", SetCallbackTrampoline);
503519
env->SetMethod(target, "pushAsyncContext", PushAsyncContext);
504520
env->SetMethod(target, "popAsyncContext", PopAsyncContext);
521+
env->SetMethod(target, "executionAsyncResource", ExecutionAsyncResource);
522+
env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack);
505523
env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
506524
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
507525
env->SetMethod(target, "disablePromiseHook", DisablePromiseHook);
@@ -540,7 +558,7 @@ void AsyncWrap::Initialize(Local<Object> target,
540558

541559
FORCE_SET_TARGET_FIELD(target,
542560
"execution_async_resources",
543-
env->async_hooks()->execution_async_resources());
561+
env->async_hooks()->js_execution_async_resources());
544562

545563
target->Set(context,
546564
env->async_ids_stack_string(),
@@ -562,6 +580,7 @@ void AsyncWrap::Initialize(Local<Object> target,
562580
SET_HOOKS_CONSTANT(kTriggerAsyncId);
563581
SET_HOOKS_CONSTANT(kAsyncIdCounter);
564582
SET_HOOKS_CONSTANT(kDefaultTriggerAsyncId);
583+
SET_HOOKS_CONSTANT(kUsesExecutionAsyncResource);
565584
SET_HOOKS_CONSTANT(kStackLength);
566585
#undef SET_HOOKS_CONSTANT
567586
FORCE_SET_TARGET_FIELD(target, "constants", constants);

src/async_wrap.h

+4
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class AsyncWrap : public BaseObject {
136136
static void GetAsyncId(const v8::FunctionCallbackInfo<v8::Value>& args);
137137
static void PushAsyncContext(const v8::FunctionCallbackInfo<v8::Value>& args);
138138
static void PopAsyncContext(const v8::FunctionCallbackInfo<v8::Value>& args);
139+
static void ExecutionAsyncResource(
140+
const v8::FunctionCallbackInfo<v8::Value>& args);
141+
static void ClearAsyncIdStack(
142+
const v8::FunctionCallbackInfo<v8::Value>& args);
139143
static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args);
140144
static void GetProviderType(const v8::FunctionCallbackInfo<v8::Value>& args);
141145
static void QueueDestroyAsyncId(

src/env-inl.h

+59-13
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,17 @@ inline AliasedFloat64Array& AsyncHooks::async_ids_stack() {
104104
return async_ids_stack_;
105105
}
106106

107-
inline v8::Local<v8::Array> AsyncHooks::execution_async_resources() {
108-
return PersistentToLocal::Strong(execution_async_resources_);
107+
v8::Local<v8::Array> AsyncHooks::js_execution_async_resources() {
108+
if (UNLIKELY(js_execution_async_resources_.IsEmpty())) {
109+
js_execution_async_resources_.Reset(
110+
env()->isolate(), v8::Array::New(env()->isolate()));
111+
}
112+
return PersistentToLocal::Strong(js_execution_async_resources_);
113+
}
114+
115+
v8::Local<v8::Object> AsyncHooks::native_execution_async_resource(size_t i) {
116+
if (i >= native_execution_async_resources_.size()) return {};
117+
return PersistentToLocal::Strong(native_execution_async_resources_[i]);
109118
}
110119

111120
inline v8::Local<v8::String> AsyncHooks::provider_string(int idx) {
@@ -123,9 +132,7 @@ inline Environment* AsyncHooks::env() {
123132
// Remember to keep this code aligned with pushAsyncContext() in JS.
124133
inline void AsyncHooks::push_async_context(double async_id,
125134
double trigger_async_id,
126-
v8::Local<v8::Value> resource) {
127-
v8::HandleScope handle_scope(env()->isolate());
128-
135+
v8::Local<v8::Object> resource) {
129136
// Since async_hooks is experimental, do only perform the check
130137
// when async_hooks is enabled.
131138
if (fields_[kCheck] > 0) {
@@ -142,8 +149,19 @@ inline void AsyncHooks::push_async_context(double async_id,
142149
async_id_fields_[kExecutionAsyncId] = async_id;
143150
async_id_fields_[kTriggerAsyncId] = trigger_async_id;
144151

145-
auto resources = execution_async_resources();
146-
USE(resources->Set(env()->context(), offset, resource));
152+
#ifdef DEBUG
153+
for (uint32_t i = offset; i < native_execution_async_resources_.size(); i++)
154+
CHECK(native_execution_async_resources_[i].IsEmpty());
155+
#endif
156+
157+
// When this call comes from JS (as a way of increasing the stack size),
158+
// `resource` will be empty, because JS caches these values anyway, and
159+
// we should avoid creating strong global references that might keep
160+
// these JS resource objects alive longer than necessary.
161+
if (!resource.IsEmpty()) {
162+
native_execution_async_resources_.resize(offset + 1);
163+
native_execution_async_resources_[offset].Reset(env()->isolate(), resource);
164+
}
147165
}
148166

149167
// Remember to keep this code aligned with popAsyncContext() in JS.
@@ -176,17 +194,45 @@ inline bool AsyncHooks::pop_async_context(double async_id) {
176194
async_id_fields_[kTriggerAsyncId] = async_ids_stack_[2 * offset + 1];
177195
fields_[kStackLength] = offset;
178196

179-
auto resources = execution_async_resources();
180-
USE(resources->Delete(env()->context(), offset));
197+
if (LIKELY(offset < native_execution_async_resources_.size() &&
198+
!native_execution_async_resources_[offset].IsEmpty())) {
199+
#ifdef DEBUG
200+
for (uint32_t i = offset + 1;
201+
i < native_execution_async_resources_.size();
202+
i++) {
203+
CHECK(native_execution_async_resources_[i].IsEmpty());
204+
}
205+
#endif
206+
native_execution_async_resources_.resize(offset);
207+
if (native_execution_async_resources_.size() <
208+
native_execution_async_resources_.capacity() / 2 &&
209+
native_execution_async_resources_.size() > 16) {
210+
native_execution_async_resources_.shrink_to_fit();
211+
}
212+
}
213+
214+
if (UNLIKELY(js_execution_async_resources()->Length() > offset)) {
215+
v8::HandleScope handle_scope(env()->isolate());
216+
USE(js_execution_async_resources()->Set(
217+
env()->context(),
218+
env()->length_string(),
219+
v8::Integer::NewFromUnsigned(env()->isolate(), offset)));
220+
}
181221

182222
return fields_[kStackLength] > 0;
183223
}
184224

185-
// Keep in sync with clearAsyncIdStack in lib/internal/async_hooks.js.
186-
inline void AsyncHooks::clear_async_id_stack() {
187-
auto isolate = env()->isolate();
225+
void AsyncHooks::clear_async_id_stack() {
226+
v8::Isolate* isolate = env()->isolate();
188227
v8::HandleScope handle_scope(isolate);
189-
execution_async_resources_.Reset(isolate, v8::Array::New(isolate));
228+
if (!js_execution_async_resources_.IsEmpty()) {
229+
USE(PersistentToLocal::Strong(js_execution_async_resources_)->Set(
230+
env()->context(),
231+
env()->length_string(),
232+
v8::Integer::NewFromUnsigned(isolate, 0)));
233+
}
234+
native_execution_async_resources_.clear();
235+
native_execution_async_resources_.shrink_to_fit();
190236

191237
async_id_fields_[kExecutionAsyncId] = 0;
192238
async_id_fields_[kTriggerAsyncId] = 0;

src/env.h

+11-3
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ constexpr size_t kFsStatsBufferLength =
275275
V(issuercert_string, "issuerCertificate") \
276276
V(kill_signal_string, "killSignal") \
277277
V(kind_string, "kind") \
278+
V(length_string, "length") \
278279
V(library_string, "library") \
279280
V(mac_string, "mac") \
280281
V(max_buffer_string, "maxBuffer") \
@@ -642,6 +643,7 @@ class AsyncHooks : public MemoryRetainer {
642643
kTotals,
643644
kCheck,
644645
kStackLength,
646+
kUsesExecutionAsyncResource,
645647
kFieldsCount,
646648
};
647649

@@ -656,15 +658,20 @@ class AsyncHooks : public MemoryRetainer {
656658
inline AliasedUint32Array& fields();
657659
inline AliasedFloat64Array& async_id_fields();
658660
inline AliasedFloat64Array& async_ids_stack();
659-
inline v8::Local<v8::Array> execution_async_resources();
661+
inline v8::Local<v8::Array> js_execution_async_resources();
662+
// Returns the native executionAsyncResource value at stack index `index`.
663+
// Resources provided on the JS side are not stored on the native stack,
664+
// in which case an empty `Local<>` is returned.
665+
// The `js_execution_async_resources` array contains the value in that case.
666+
inline v8::Local<v8::Object> native_execution_async_resource(size_t index);
660667

661668
inline v8::Local<v8::String> provider_string(int idx);
662669

663670
inline void no_force_checks();
664671
inline Environment* env();
665672

666673
inline void push_async_context(double async_id, double trigger_async_id,
667-
v8::Local<v8::Value> execution_async_resource_);
674+
v8::Local<v8::Object> execution_async_resource_);
668675
inline bool pop_async_context(double async_id);
669676
inline void clear_async_id_stack(); // Used in fatal exceptions.
670677

@@ -709,7 +716,8 @@ class AsyncHooks : public MemoryRetainer {
709716

710717
void grow_async_ids_stack();
711718

712-
v8::Global<v8::Array> execution_async_resources_;
719+
v8::Global<v8::Array> js_execution_async_resources_;
720+
std::vector<v8::Global<v8::Object>> native_execution_async_resources_;
713721
};
714722

715723
class ImmediateInfo : public MemoryRetainer {

0 commit comments

Comments
 (0)