Skip to content

Commit 9165826

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 nodejs#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: nodejs#33575
1 parent 4e3f6f3 commit 9165826

6 files changed

Lines changed: 108 additions & 35 deletions

File tree

lib/internal/async_hooks.js

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ const {
5050
// each hook's after() callback.
5151
const {
5252
pushAsyncContext: pushAsyncContext_,
53-
popAsyncContext: popAsyncContext_
53+
popAsyncContext: popAsyncContext_,
54+
executionAsyncResource: executionAsyncResource_,
55+
clearAsyncIdStack,
5456
} = async_wrap;
5557
// For performance reasons, only track Promises when a hook is enabled.
5658
const { enablePromiseHook, disablePromiseHook } = async_wrap;
@@ -89,7 +91,8 @@ const { resource_symbol, owner_symbol } = internalBinding('symbols');
8991
// for a given step, that step can bail out early.
9092
const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
9193
kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId,
92-
kDefaultTriggerAsyncId, kStackLength } = async_wrap.constants;
94+
kDefaultTriggerAsyncId, kStackLength, kUsesExecutionAsyncResource
95+
} = async_wrap.constants;
9396

9497
const { async_id_symbol,
9598
trigger_async_id_symbol } = internalBinding('symbols');
@@ -111,7 +114,10 @@ function useDomainTrampoline(fn) {
111114
domain_cb = fn;
112115
}
113116

114-
function callbackTrampoline(asyncId, cb, ...args) {
117+
function callbackTrampoline(asyncId, resource, cb, ...args) {
118+
const index = async_hook_fields[kStackLength] - 1;
119+
execution_async_resources[index] = resource;
120+
115121
if (asyncId !== 0 && hasHooks(kBefore))
116122
emitBeforeNative(asyncId);
117123

@@ -126,6 +132,7 @@ function callbackTrampoline(asyncId, cb, ...args) {
126132
if (asyncId !== 0 && hasHooks(kAfter))
127133
emitAfterNative(asyncId);
128134

135+
execution_async_resources.pop();
129136
return result;
130137
}
131138

@@ -134,9 +141,15 @@ setCallbackTrampoline(callbackTrampoline);
134141
const topLevelResource = {};
135142

136143
function executionAsyncResource() {
144+
// Indicate to the native layer that this function is likely to be used,
145+
// in which case it will inform JS about the current async resource via
146+
// the trampoline above.
147+
async_hook_fields[kUsesExecutionAsyncResource] = 1;
148+
137149
const index = async_hook_fields[kStackLength] - 1;
138150
if (index === -1) return topLevelResource;
139-
const resource = execution_async_resources[index];
151+
const resource = execution_async_resources[index] ||
152+
executionAsyncResource_(index);
140153
return lookupPublicResource(resource);
141154
}
142155

@@ -478,16 +491,6 @@ function emitDestroyScript(asyncId) {
478491
}
479492

480493

481-
// Keep in sync with Environment::AsyncHooks::clear_async_id_stack
482-
// in src/env-inl.h.
483-
function clearAsyncIdStack() {
484-
async_id_fields[kExecutionAsyncId] = 0;
485-
async_id_fields[kTriggerAsyncId] = 0;
486-
async_hook_fields[kStackLength] = 0;
487-
execution_async_resources.splice(0, execution_async_resources.length);
488-
}
489-
490-
491494
function hasAsyncIdStack() {
492495
return hasHooks(kStackLength);
493496
}

src/api/callback.cc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,12 @@ MaybeLocal<Value> InternalMakeCallback(Environment* env,
161161
Local<Function> hook_cb = env->async_hooks_callback_trampoline();
162162
int flags = InternalCallbackScope::kNoFlags;
163163
int hook_count = 0;
164+
AsyncHooks* async_hooks = env->async_hooks();
164165
if (!hook_cb.IsEmpty()) {
165166
flags = InternalCallbackScope::kSkipAsyncHooks;
166-
AsyncHooks* async_hooks = env->async_hooks();
167167
hook_count = async_hooks->fields()[AsyncHooks::kBefore] +
168-
async_hooks->fields()[AsyncHooks::kAfter];
168+
async_hooks->fields()[AsyncHooks::kAfter] +
169+
async_hooks->fields()[AsyncHooks::kUsesExecutionAsyncResource];
169170
}
170171

171172
InternalCallbackScope scope(env, resource, asyncContext, flags);
@@ -176,11 +177,12 @@ MaybeLocal<Value> InternalMakeCallback(Environment* env,
176177
MaybeLocal<Value> ret;
177178

178179
if (hook_count != 0) {
179-
MaybeStackBuffer<Local<Value>, 16> args(2 + argc);
180+
MaybeStackBuffer<Local<Value>, 16> args(3 + argc);
180181
args[0] = v8::Number::New(env->isolate(), asyncContext.async_id);
181-
args[1] = callback;
182+
args[1] = resource;
183+
args[2] = callback;
182184
for (int i = 0; i < argc; i++) {
183-
args[i + 2] = argv[i];
185+
args[i + 3] = argv[i];
184186
}
185187
ret = hook_cb->Call(env->context(), recv, args.length(), &args[0]);
186188
} else {

src/async_wrap.cc

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,8 @@ void AsyncWrap::PushAsyncContext(const FunctionCallbackInfo<Value>& args) {
502502
// then the checks in push_async_ids() and pop_async_id() will.
503503
double async_id = args[0]->NumberValue(env->context()).FromJust();
504504
double trigger_async_id = args[1]->NumberValue(env->context()).FromJust();
505-
env->async_hooks()->push_async_context(async_id, trigger_async_id, args[2]);
505+
env->async_hooks()->push_async_context(
506+
async_id, trigger_async_id, args[2].As<Object>());
506507
}
507508

508509

@@ -513,6 +514,22 @@ void AsyncWrap::PopAsyncContext(const FunctionCallbackInfo<Value>& args) {
513514
}
514515

515516

517+
void AsyncWrap::ExecutionAsyncResource(
518+
const FunctionCallbackInfo<Value>& args) {
519+
Environment* env = Environment::GetCurrent(args);
520+
uint32_t index;
521+
if (!args[0]->Uint32Value(env->context()).To(&index)) return;
522+
args.GetReturnValue().Set(
523+
env->async_hooks()->native_execution_async_resource(index));
524+
}
525+
526+
527+
void AsyncWrap::ClearAsyncIdStack(const FunctionCallbackInfo<Value>& args) {
528+
Environment* env = Environment::GetCurrent(args);
529+
env->async_hooks()->clear_async_id_stack();
530+
}
531+
532+
516533
void AsyncWrap::AsyncReset(const FunctionCallbackInfo<Value>& args) {
517534
CHECK(args[0]->IsObject());
518535

@@ -586,6 +603,8 @@ void AsyncWrap::Initialize(Local<Object> target,
586603
env->SetMethod(target, "setCallbackTrampoline", SetCallbackTrampoline);
587604
env->SetMethod(target, "pushAsyncContext", PushAsyncContext);
588605
env->SetMethod(target, "popAsyncContext", PopAsyncContext);
606+
env->SetMethod(target, "executionAsyncResource", ExecutionAsyncResource);
607+
env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack);
589608
env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
590609
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
591610
env->SetMethod(target, "disablePromiseHook", DisablePromiseHook);
@@ -624,7 +643,7 @@ void AsyncWrap::Initialize(Local<Object> target,
624643

625644
FORCE_SET_TARGET_FIELD(target,
626645
"execution_async_resources",
627-
env->async_hooks()->execution_async_resources());
646+
env->async_hooks()->js_execution_async_resources());
628647

629648
target->Set(context,
630649
env->async_ids_stack_string(),
@@ -646,6 +665,7 @@ void AsyncWrap::Initialize(Local<Object> target,
646665
SET_HOOKS_CONSTANT(kTriggerAsyncId);
647666
SET_HOOKS_CONSTANT(kAsyncIdCounter);
648667
SET_HOOKS_CONSTANT(kDefaultTriggerAsyncId);
668+
SET_HOOKS_CONSTANT(kUsesExecutionAsyncResource);
649669
SET_HOOKS_CONSTANT(kStackLength);
650670
#undef SET_HOOKS_CONSTANT
651671
FORCE_SET_TARGET_FIELD(target, "constants", constants);

src/async_wrap.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@ class AsyncWrap : public BaseObject {
143143
static void GetAsyncId(const v8::FunctionCallbackInfo<v8::Value>& args);
144144
static void PushAsyncContext(const v8::FunctionCallbackInfo<v8::Value>& args);
145145
static void PopAsyncContext(const v8::FunctionCallbackInfo<v8::Value>& args);
146+
static void ExecutionAsyncResource(
147+
const v8::FunctionCallbackInfo<v8::Value>& args);
148+
static void ClearAsyncIdStack(
149+
const v8::FunctionCallbackInfo<v8::Value>& args);
146150
static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args);
147151
static void GetProviderType(const v8::FunctionCallbackInfo<v8::Value>& args);
148152
static void QueueDestroyAsyncId(

src/env-inl.h

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,17 @@ inline AliasedFloat64Array& AsyncHooks::async_ids_stack() {
105105
return async_ids_stack_;
106106
}
107107

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

112121
inline v8::Local<v8::String> AsyncHooks::provider_string(int idx) {
@@ -124,7 +133,7 @@ inline Environment* AsyncHooks::env() {
124133
// Remember to keep this code aligned with pushAsyncContext() in JS.
125134
inline void AsyncHooks::push_async_context(double async_id,
126135
double trigger_async_id,
127-
v8::Local<v8::Value> resource) {
136+
v8::Local<v8::Object> resource) {
128137
v8::HandleScope handle_scope(env()->isolate());
129138

130139
// Since async_hooks is experimental, do only perform the check
@@ -143,8 +152,12 @@ inline void AsyncHooks::push_async_context(double async_id,
143152
async_id_fields_[kExecutionAsyncId] = async_id;
144153
async_id_fields_[kTriggerAsyncId] = trigger_async_id;
145154

146-
auto resources = execution_async_resources();
147-
USE(resources->Set(env()->context(), offset, resource));
155+
#ifdef DEBUG
156+
for (uint32_t i = offset; i < native_execution_async_resources_.size(); i++)
157+
CHECK(native_execution_async_resources_[i].IsEmpty());
158+
#endif
159+
native_execution_async_resources_.resize(offset + 1);
160+
native_execution_async_resources_[offset].Reset(env()->isolate(), resource);
148161
}
149162

150163
// Remember to keep this code aligned with popAsyncContext() in JS.
@@ -177,17 +190,44 @@ inline bool AsyncHooks::pop_async_context(double async_id) {
177190
async_id_fields_[kTriggerAsyncId] = async_ids_stack_[2 * offset + 1];
178191
fields_[kStackLength] = offset;
179192

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

183217
return fields_[kStackLength] > 0;
184218
}
185219

186-
// Keep in sync with clearAsyncIdStack in lib/internal/async_hooks.js.
187-
inline void AsyncHooks::clear_async_id_stack() {
188-
auto isolate = env()->isolate();
220+
void AsyncHooks::clear_async_id_stack() {
221+
v8::Isolate* isolate = env()->isolate();
189222
v8::HandleScope handle_scope(isolate);
190-
execution_async_resources_.Reset(isolate, v8::Array::New(isolate));
223+
if (!js_execution_async_resources_.IsEmpty()) {
224+
USE(PersistentToLocal::Strong(js_execution_async_resources_)->Set(
225+
env()->context(),
226+
env()->length_string(),
227+
v8::Integer::NewFromUnsigned(isolate, 0)));
228+
}
229+
native_execution_async_resources_.clear();
230+
native_execution_async_resources_.shrink_to_fit();
191231

192232
async_id_fields_[kExecutionAsyncId] = 0;
193233
async_id_fields_[kTriggerAsyncId] = 0;

src/env.h

Lines changed: 7 additions & 3 deletions
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") \
@@ -661,6 +662,7 @@ class AsyncHooks : public MemoryRetainer {
661662
kTotals,
662663
kCheck,
663664
kStackLength,
665+
kUsesExecutionAsyncResource,
664666
kFieldsCount,
665667
};
666668

@@ -675,15 +677,16 @@ class AsyncHooks : public MemoryRetainer {
675677
inline AliasedUint32Array& fields();
676678
inline AliasedFloat64Array& async_id_fields();
677679
inline AliasedFloat64Array& async_ids_stack();
678-
inline v8::Local<v8::Array> execution_async_resources();
680+
inline v8::Local<v8::Array> js_execution_async_resources();
681+
inline v8::Local<v8::Object> native_execution_async_resource(size_t index);
679682

680683
inline v8::Local<v8::String> provider_string(int idx);
681684

682685
inline void no_force_checks();
683686
inline Environment* env();
684687

685688
inline void push_async_context(double async_id, double trigger_async_id,
686-
v8::Local<v8::Value> execution_async_resource_);
689+
v8::Local<v8::Object> execution_async_resource_);
687690
inline bool pop_async_context(double async_id);
688691
inline void clear_async_id_stack(); // Used in fatal exceptions.
689692

@@ -728,7 +731,8 @@ class AsyncHooks : public MemoryRetainer {
728731

729732
void grow_async_ids_stack();
730733

731-
v8::Global<v8::Array> execution_async_resources_;
734+
v8::Global<v8::Array> js_execution_async_resources_;
735+
std::vector<v8::Global<v8::Object>> native_execution_async_resources_;
732736
};
733737

734738
class ImmediateInfo : public MemoryRetainer {

0 commit comments

Comments
 (0)