Skip to content

Commit 7b9d29b

Browse files
addaleaxgibfahn
authored andcommitted
async_hooks: use typed array stack as fast path
- Communicate the current async stack length through a typed array field rather than a native binding method - Add a new fixed-size `async_ids_fast_stack` typed array that contains the async ID stack up to a fixed limit. This increases performance noticeably, since most of the time the async ID stack will not be more than a handful of levels deep. - Make the JS `pushAsyncIds()` and `popAsyncIds()` functions do the same thing as the native ones if the fast path is applicable. Benchmarks: $ ./node benchmark/compare.js --new ./node --old ./node-master --runs 10 --filter next-tick process | Rscript benchmark/compare.R [00:03:25|% 100| 6/6 files | 20/20 runs | 1/1 configs]: Done improvement confidence p.value process/next-tick-breadth-args.js millions=4 19.72 % *** 3.013913e-06 process/next-tick-breadth.js millions=4 27.33 % *** 5.847983e-11 process/next-tick-depth-args.js millions=12 40.08 % *** 1.237127e-13 process/next-tick-depth.js millions=12 77.27 % *** 1.413290e-11 process/next-tick-exec-args.js millions=5 13.58 % *** 1.245180e-07 process/next-tick-exec.js millions=5 16.80 % *** 2.961386e-07 PR-URL: nodejs#17780 Backport-PR-URL: nodejs#18179 Reviewed-By: James M Snell <[email protected]>
1 parent 42a8fde commit 7b9d29b

File tree

9 files changed

+165
-54
lines changed

9 files changed

+165
-54
lines changed

lib/internal/async_hooks.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,20 @@ const async_wrap = process.binding('async_wrap');
1919
* retrieving the triggerAsyncId value is passing directly to the
2020
* constructor -> value set in kDefaultTriggerAsyncId -> executionAsyncId of
2121
* the current resource.
22+
*
23+
* async_ids_fast_stack is a Float64Array that contains part of the async ID
24+
* stack. Each pushAsyncIds() call adds two doubles to it, and each
25+
* popAsyncIds() call removes two doubles from it.
26+
* It has a fixed size, so if that is exceeded, calls to the native
27+
* side are used instead in pushAsyncIds() and popAsyncIds().
2228
*/
2329
const { async_hook_fields, async_id_fields } = async_wrap;
2430
// Store the pair executionAsyncId and triggerAsyncId in a std::stack on
2531
// Environment::AsyncHooks::ids_stack_ tracks the resource responsible for the
2632
// current execution stack. This is unwound as each resource exits. In the case
2733
// of a fatal exception this stack is emptied after calling each hook's after()
2834
// callback.
29-
const { pushAsyncIds, popAsyncIds } = async_wrap;
35+
const { pushAsyncIds: pushAsyncIds_, popAsyncIds: popAsyncIds_ } = async_wrap;
3036
// For performance reasons, only track Proimses when a hook is enabled.
3137
const { enablePromiseHook, disablePromiseHook } = async_wrap;
3238
// Properties in active_hooks are used to keep track of the set of hooks being
@@ -60,8 +66,8 @@ const active_hooks = {
6066
// async execution. These are tracked so if the user didn't include callbacks
6167
// for a given step, that step can bail out early.
6268
const { kInit, kBefore, kAfter, kDestroy, kPromiseResolve,
63-
kCheck, kExecutionAsyncId, kAsyncIdCounter,
64-
kDefaultTriggerAsyncId } = async_wrap.constants;
69+
kCheck, kExecutionAsyncId, kAsyncIdCounter, kTriggerAsyncId,
70+
kDefaultTriggerAsyncId, kStackLength } = async_wrap.constants;
6571

6672
// Used in AsyncHook and AsyncResource.
6773
const init_symbol = Symbol('init');
@@ -332,6 +338,38 @@ function emitDestroyScript(asyncId) {
332338
}
333339

334340

341+
// This is the equivalent of the native push_async_ids() call.
342+
function pushAsyncIds(asyncId, triggerAsyncId) {
343+
const offset = async_hook_fields[kStackLength];
344+
if (offset * 2 >= async_wrap.async_ids_stack.length)
345+
return pushAsyncIds_(asyncId, triggerAsyncId);
346+
async_wrap.async_ids_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
347+
async_wrap.async_ids_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
348+
async_hook_fields[kStackLength]++;
349+
async_id_fields[kExecutionAsyncId] = asyncId;
350+
async_id_fields[kTriggerAsyncId] = triggerAsyncId;
351+
}
352+
353+
354+
// This is the equivalent of the native pop_async_ids() call.
355+
function popAsyncIds(asyncId) {
356+
if (async_hook_fields[kStackLength] === 0) return false;
357+
const stackLength = async_hook_fields[kStackLength];
358+
359+
if (async_hook_fields[kCheck] > 0 &&
360+
async_id_fields[kExecutionAsyncId] !== asyncId) {
361+
// Do the same thing as the native code (i.e. crash hard).
362+
return popAsyncIds_(asyncId);
363+
}
364+
365+
const offset = stackLength - 1;
366+
async_id_fields[kExecutionAsyncId] = async_wrap.async_ids_stack[2 * offset];
367+
async_id_fields[kTriggerAsyncId] = async_wrap.async_ids_stack[2 * offset + 1];
368+
async_hook_fields[kStackLength] = offset;
369+
return offset > 0;
370+
}
371+
372+
335373
module.exports = {
336374
// Private API
337375
getHookArrays,

lib/internal/bootstrap_node.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@
357357
// Arrays containing hook flags and ids for async_hook calls.
358358
const { async_hook_fields, async_id_fields } = async_wrap;
359359
// Internal functions needed to manipulate the stack.
360-
const { clearAsyncIdStack, asyncIdStackSize } = async_wrap;
360+
const { clearAsyncIdStack } = async_wrap;
361361
const { kAfter, kExecutionAsyncId,
362-
kDefaultTriggerAsyncId } = async_wrap.constants;
362+
kDefaultTriggerAsyncId, kStackLength } = async_wrap.constants;
363363

364364
process._fatalException = function(er) {
365365
var caught;
@@ -395,7 +395,7 @@
395395
do {
396396
NativeModule.require('internal/async_hooks').emitAfter(
397397
async_id_fields[kExecutionAsyncId]);
398-
} while (asyncIdStackSize() > 0);
398+
} while (async_hook_fields[kStackLength] > 0);
399399
// Or completely empty the id stack.
400400
} else {
401401
clearAsyncIdStack();

src/aliased_buffer.h

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ class AliasedBuffer {
9595
js_array_.Reset();
9696
}
9797

98+
AliasedBuffer& operator=(AliasedBuffer&& that) {
99+
this->~AliasedBuffer();
100+
isolate_ = that.isolate_;
101+
count_ = that.count_;
102+
byte_offset_ = that.byte_offset_;
103+
buffer_ = that.buffer_;
104+
free_buffer_ = that.free_buffer_;
105+
106+
js_array_.Reset(isolate_, that.js_array_.Get(isolate_));
107+
108+
that.buffer_ = nullptr;
109+
that.js_array_.Reset();
110+
return *this;
111+
}
112+
98113
/**
99114
* Helper class that is returned from operator[] to support assignment into
100115
* a specified location.
@@ -111,11 +126,17 @@ class AliasedBuffer {
111126
index_(that.index_) {
112127
}
113128

114-
inline Reference& operator=(const NativeT &val) {
129+
template <typename T>
130+
inline Reference& operator=(const T& val) {
115131
aliased_buffer_->SetValue(index_, val);
116132
return *this;
117133
}
118134

135+
// This is not caught by the template operator= above.
136+
inline Reference& operator=(const Reference& val) {
137+
return *this = static_cast<NativeT>(val);
138+
}
139+
119140
operator NativeT() const {
120141
return aliased_buffer_->GetValue(index_);
121142
}
@@ -186,8 +207,12 @@ class AliasedBuffer {
186207
return GetValue(index);
187208
}
188209

210+
size_t Length() const {
211+
return count_;
212+
}
213+
189214
private:
190-
v8::Isolate* const isolate_;
215+
v8::Isolate* isolate_;
191216
size_t count_;
192217
size_t byte_offset_;
193218
NativeT* buffer_;

src/async_wrap.cc

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -489,13 +489,6 @@ void AsyncWrap::PopAsyncIds(const FunctionCallbackInfo<Value>& args) {
489489
}
490490

491491

492-
void AsyncWrap::AsyncIdStackSize(const FunctionCallbackInfo<Value>& args) {
493-
Environment* env = Environment::GetCurrent(args);
494-
args.GetReturnValue().Set(
495-
static_cast<double>(env->async_hooks()->stack_size()));
496-
}
497-
498-
499492
void AsyncWrap::ClearAsyncIdStack(const FunctionCallbackInfo<Value>& args) {
500493
Environment* env = Environment::GetCurrent(args);
501494
env->async_hooks()->clear_async_id_stack();
@@ -534,7 +527,6 @@ void AsyncWrap::Initialize(Local<Object> target,
534527
env->SetMethod(target, "setupHooks", SetupHooks);
535528
env->SetMethod(target, "pushAsyncIds", PushAsyncIds);
536529
env->SetMethod(target, "popAsyncIds", PopAsyncIds);
537-
env->SetMethod(target, "asyncIdStackSize", AsyncIdStackSize);
538530
env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack);
539531
env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
540532
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
@@ -572,6 +564,10 @@ void AsyncWrap::Initialize(Local<Object> target,
572564
"async_id_fields",
573565
env->async_hooks()->async_id_fields().GetJSArray());
574566

567+
target->Set(context,
568+
env->async_ids_stack_string(),
569+
env->async_hooks()->async_ids_stack().GetJSArray()).FromJust();
570+
575571
Local<Object> constants = Object::New(isolate);
576572
#define SET_HOOKS_CONSTANT(name) \
577573
FORCE_SET_TARGET_FIELD( \
@@ -588,6 +584,7 @@ void AsyncWrap::Initialize(Local<Object> target,
588584
SET_HOOKS_CONSTANT(kTriggerAsyncId);
589585
SET_HOOKS_CONSTANT(kAsyncIdCounter);
590586
SET_HOOKS_CONSTANT(kDefaultTriggerAsyncId);
587+
SET_HOOKS_CONSTANT(kStackLength);
591588
#undef SET_HOOKS_CONSTANT
592589
FORCE_SET_TARGET_FIELD(target, "constants", constants);
593590

@@ -617,6 +614,7 @@ void AsyncWrap::Initialize(Local<Object> target,
617614
env->set_async_hooks_after_function(Local<Function>());
618615
env->set_async_hooks_destroy_function(Local<Function>());
619616
env->set_async_hooks_promise_resolve_function(Local<Function>());
617+
env->set_async_hooks_binding(target);
620618
}
621619

622620

src/async_wrap.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ class AsyncWrap : public BaseObject {
123123
static void GetAsyncId(const v8::FunctionCallbackInfo<v8::Value>& args);
124124
static void PushAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args);
125125
static void PopAsyncIds(const v8::FunctionCallbackInfo<v8::Value>& args);
126-
static void AsyncIdStackSize(const v8::FunctionCallbackInfo<v8::Value>& args);
127126
static void ClearAsyncIdStack(
128127
const v8::FunctionCallbackInfo<v8::Value>& args);
129128
static void AsyncReset(const v8::FunctionCallbackInfo<v8::Value>& args);

src/env-inl.h

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ inline uint32_t* IsolateData::zero_fill_field() const {
8181
return zero_fill_field_;
8282
}
8383

84-
inline Environment::AsyncHooks::AsyncHooks(v8::Isolate* isolate)
85-
: isolate_(isolate),
86-
fields_(isolate, kFieldsCount),
87-
async_id_fields_(isolate, kUidFieldsCount) {
88-
v8::HandleScope handle_scope(isolate_);
84+
inline Environment::AsyncHooks::AsyncHooks()
85+
: async_ids_stack_(env()->isolate(), 16 * 2),
86+
fields_(env()->isolate(), kFieldsCount),
87+
async_id_fields_(env()->isolate(), kUidFieldsCount) {
88+
v8::HandleScope handle_scope(env()->isolate());
8989

9090
// kDefaultTriggerAsyncId should be -1, this indicates that there is no
9191
// specified default value and it should fallback to the executionAsyncId.
@@ -102,9 +102,9 @@ inline Environment::AsyncHooks::AsyncHooks(v8::Isolate* isolate)
102102
// strings can be retrieved quickly.
103103
#define V(Provider) \
104104
providers_[AsyncWrap::PROVIDER_ ## Provider].Set( \
105-
isolate_, \
105+
env()->isolate(), \
106106
v8::String::NewFromOneByte( \
107-
isolate_, \
107+
env()->isolate(), \
108108
reinterpret_cast<const uint8_t*>(#Provider), \
109109
v8::NewStringType::kInternalized, \
110110
sizeof(#Provider) - 1).ToLocalChecked());
@@ -122,15 +122,25 @@ Environment::AsyncHooks::async_id_fields() {
122122
return async_id_fields_;
123123
}
124124

125+
inline AliasedBuffer<double, v8::Float64Array>&
126+
Environment::AsyncHooks::async_ids_stack() {
127+
return async_ids_stack_;
128+
}
129+
125130
inline v8::Local<v8::String> Environment::AsyncHooks::provider_string(int idx) {
126-
return providers_[idx].Get(isolate_);
131+
return providers_[idx].Get(env()->isolate());
127132
}
128133

129134
inline void Environment::AsyncHooks::force_checks() {
130135
// fields_ does not have the += operator defined
131136
fields_[kCheck] = fields_[kCheck] + 1;
132137
}
133138

139+
inline Environment* Environment::AsyncHooks::env() {
140+
return Environment::ForAsyncHooks(this);
141+
}
142+
143+
// Remember to keep this code aligned with pushAsyncIds() in JS.
134144
inline void Environment::AsyncHooks::push_async_ids(double async_id,
135145
double trigger_async_id) {
136146
// Since async_hooks is experimental, do only perform the check
@@ -140,16 +150,21 @@ inline void Environment::AsyncHooks::push_async_ids(double async_id,
140150
CHECK_GE(trigger_async_id, -1);
141151
}
142152

143-
async_ids_stack_.push({ async_id_fields_[kExecutionAsyncId],
144-
async_id_fields_[kTriggerAsyncId] });
153+
uint32_t offset = fields_[kStackLength];
154+
if (offset * 2 >= async_ids_stack_.Length())
155+
grow_async_ids_stack();
156+
async_ids_stack_[2 * offset] = async_id_fields_[kExecutionAsyncId];
157+
async_ids_stack_[2 * offset + 1] = async_id_fields_[kTriggerAsyncId];
158+
fields_[kStackLength] = fields_[kStackLength] + 1;
145159
async_id_fields_[kExecutionAsyncId] = async_id;
146160
async_id_fields_[kTriggerAsyncId] = trigger_async_id;
147161
}
148162

163+
// Remember to keep this code aligned with popAsyncIds() in JS.
149164
inline bool Environment::AsyncHooks::pop_async_id(double async_id) {
150165
// In case of an exception then this may have already been reset, if the
151166
// stack was multiple MakeCallback()'s deep.
152-
if (async_ids_stack_.empty()) return false;
167+
if (fields_[kStackLength] == 0) return false;
153168

154169
// Ask for the async_id to be restored as a check that the stack
155170
// hasn't been corrupted.
@@ -161,32 +176,27 @@ inline bool Environment::AsyncHooks::pop_async_id(double async_id) {
161176
"actual: %.f, expected: %.f)\n",
162177
async_id_fields_.GetValue(kExecutionAsyncId),
163178
async_id);
164-
Environment* env = Environment::GetCurrent(isolate_);
165179
DumpBacktrace(stderr);
166180
fflush(stderr);
167-
if (!env->abort_on_uncaught_exception())
181+
if (!env()->abort_on_uncaught_exception())
168182
exit(1);
169183
fprintf(stderr, "\n");
170184
fflush(stderr);
171185
ABORT_NO_BACKTRACE();
172186
}
173187

174-
auto async_ids = async_ids_stack_.top();
175-
async_ids_stack_.pop();
176-
async_id_fields_[kExecutionAsyncId] = async_ids.async_id;
177-
async_id_fields_[kTriggerAsyncId] = async_ids.trigger_async_id;
178-
return !async_ids_stack_.empty();
179-
}
188+
uint32_t offset = fields_[kStackLength] - 1;
189+
async_id_fields_[kExecutionAsyncId] = async_ids_stack_[2 * offset];
190+
async_id_fields_[kTriggerAsyncId] = async_ids_stack_[2 * offset + 1];
191+
fields_[kStackLength] = offset;
180192

181-
inline size_t Environment::AsyncHooks::stack_size() {
182-
return async_ids_stack_.size();
193+
return fields_[kStackLength] > 0;
183194
}
184195

185196
inline void Environment::AsyncHooks::clear_async_id_stack() {
186-
while (!async_ids_stack_.empty())
187-
async_ids_stack_.pop();
188197
async_id_fields_[kExecutionAsyncId] = 0;
189198
async_id_fields_[kTriggerAsyncId] = 0;
199+
fields_[kStackLength] = 0;
190200
}
191201

192202
inline Environment::AsyncHooks::DefaultTriggerAsyncIdScope
@@ -210,6 +220,11 @@ inline Environment::AsyncHooks::DefaultTriggerAsyncIdScope
210220
}
211221

212222

223+
Environment* Environment::ForAsyncHooks(AsyncHooks* hooks) {
224+
return ContainerOf(&Environment::async_hooks_, hooks);
225+
}
226+
227+
213228
inline Environment::AsyncCallbackScope::AsyncCallbackScope(Environment* env)
214229
: env_(env) {
215230
env_->makecallback_cntr_++;
@@ -298,7 +313,6 @@ inline Environment::Environment(IsolateData* isolate_data,
298313
v8::Local<v8::Context> context)
299314
: isolate_(context->GetIsolate()),
300315
isolate_data_(isolate_data),
301-
async_hooks_(context->GetIsolate()),
302316
timer_base_(uv_now(isolate_data->event_loop())),
303317
using_domains_(false),
304318
printed_error_(false),

src/env.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,4 +269,21 @@ void Environment::ActivateImmediateCheck() {
269269
uv_idle_start(&immediate_idle_handle_, [](uv_idle_t*){ });
270270
}
271271

272+
273+
void Environment::AsyncHooks::grow_async_ids_stack() {
274+
const uint32_t old_capacity = async_ids_stack_.Length() / 2;
275+
const uint32_t new_capacity = old_capacity * 1.5;
276+
AliasedBuffer<double, v8::Float64Array> new_buffer(
277+
env()->isolate(), new_capacity * 2);
278+
279+
for (uint32_t i = 0; i < old_capacity * 2; ++i)
280+
new_buffer[i] = async_ids_stack_[i];
281+
async_ids_stack_ = std::move(new_buffer);
282+
283+
env()->async_hooks_binding()->Set(
284+
env()->context(),
285+
env()->async_ids_stack_string(),
286+
async_ids_stack_.GetJSArray()).FromJust();
287+
}
288+
272289
} // namespace node

0 commit comments

Comments
 (0)