Skip to content

Commit 6bed72a

Browse files
committed
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
1 parent d84f16e commit 6bed72a

File tree

8 files changed

+123
-30
lines changed

8 files changed

+123
-30
lines changed

lib/internal/async_hooks.js

Lines changed: 44 additions & 4 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
*/
23-
const { async_hook_fields, async_id_fields } = async_wrap;
29+
const { async_hook_fields, async_id_fields, async_ids_fast_stack } = 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,9 @@ 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,
71+
kFastStackCapacity } = async_wrap.constants;
6572

6673
// Used in AsyncHook and AsyncResource.
6774
const init_symbol = Symbol('init');
@@ -332,6 +339,39 @@ function emitDestroyScript(asyncId) {
332339
}
333340

334341

342+
// This is the equivalent of the native push_async_ids() call.
343+
function pushAsyncIds(asyncId, triggerAsyncId) {
344+
const stackLength = async_hook_fields[kStackLength];
345+
if (stackLength >= kFastStackCapacity)
346+
return pushAsyncIds_(asyncId, triggerAsyncId);
347+
const offset = stackLength;
348+
async_ids_fast_stack[offset * 2] = async_id_fields[kExecutionAsyncId];
349+
async_ids_fast_stack[offset * 2 + 1] = async_id_fields[kTriggerAsyncId];
350+
async_hook_fields[kStackLength]++;
351+
async_id_fields[kExecutionAsyncId] = asyncId;
352+
async_id_fields[kTriggerAsyncId] = triggerAsyncId;
353+
}
354+
355+
356+
// This is the equivalent of the native pop_async_ids() call.
357+
function popAsyncIds(asyncId) {
358+
if (async_hook_fields[kStackLength] === 0) return false;
359+
const stackLength = async_hook_fields[kStackLength];
360+
361+
if (stackLength > kFastStackCapacity ||
362+
(async_hook_fields[kCheck] > 0 &&
363+
async_id_fields[kExecutionAsyncId] !== asyncId)) {
364+
return popAsyncIds_(asyncId);
365+
}
366+
367+
const offset = stackLength - 1;
368+
async_id_fields[kExecutionAsyncId] = async_ids_fast_stack[2 * offset];
369+
async_id_fields[kTriggerAsyncId] = async_ids_fast_stack[2 * offset + 1];
370+
async_hook_fields[kStackLength] = offset;
371+
return offset > 0;
372+
}
373+
374+
335375
module.exports = {
336376
// Private API
337377
getHookArrays,

lib/internal/bootstrap_node.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,9 @@
367367
// Arrays containing hook flags and ids for async_hook calls.
368368
const { async_hook_fields, async_id_fields } = async_wrap;
369369
// Internal functions needed to manipulate the stack.
370-
const { clearAsyncIdStack, asyncIdStackSize } = async_wrap;
370+
const { clearAsyncIdStack } = async_wrap;
371371
const { kAfter, kExecutionAsyncId,
372-
kDefaultTriggerAsyncId } = async_wrap.constants;
372+
kDefaultTriggerAsyncId, kStackLength } = async_wrap.constants;
373373

374374
process._fatalException = function(er) {
375375
var caught;
@@ -407,7 +407,7 @@
407407
do {
408408
NativeModule.require('internal/async_hooks').emitAfter(
409409
async_id_fields[kExecutionAsyncId]);
410-
} while (asyncIdStackSize() > 0);
410+
} while (async_hook_fields[kStackLength] > 0);
411411
// Or completely empty the id stack.
412412
} else {
413413
clearAsyncIdStack();

src/aliased_buffer.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,17 @@ class AliasedBuffer {
111111
index_(that.index_) {
112112
}
113113

114-
inline Reference& operator=(const NativeT &val) {
114+
template <typename T>
115+
inline Reference& operator=(const T& val) {
115116
aliased_buffer_->SetValue(index_, val);
116117
return *this;
117118
}
118119

120+
// This is not caught by the template operator= above.
121+
inline Reference& operator=(const Reference& val) {
122+
return *this = static_cast<NativeT>(val);
123+
}
124+
119125
operator NativeT() const {
120126
return aliased_buffer_->GetValue(index_);
121127
}

src/async_wrap.cc

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

470470

471-
void AsyncWrap::AsyncIdStackSize(const FunctionCallbackInfo<Value>& args) {
472-
Environment* env = Environment::GetCurrent(args);
473-
args.GetReturnValue().Set(
474-
static_cast<double>(env->async_hooks()->stack_size()));
475-
}
476-
477-
478471
void AsyncWrap::ClearAsyncIdStack(const FunctionCallbackInfo<Value>& args) {
479472
Environment* env = Environment::GetCurrent(args);
480473
env->async_hooks()->clear_async_id_stack();
@@ -513,7 +506,6 @@ void AsyncWrap::Initialize(Local<Object> target,
513506
env->SetMethod(target, "setupHooks", SetupHooks);
514507
env->SetMethod(target, "pushAsyncIds", PushAsyncIds);
515508
env->SetMethod(target, "popAsyncIds", PopAsyncIds);
516-
env->SetMethod(target, "asyncIdStackSize", AsyncIdStackSize);
517509
env->SetMethod(target, "clearAsyncIdStack", ClearAsyncIdStack);
518510
env->SetMethod(target, "queueDestroyAsyncId", QueueDestroyAsyncId);
519511
env->SetMethod(target, "enablePromiseHook", EnablePromiseHook);
@@ -551,6 +543,11 @@ void AsyncWrap::Initialize(Local<Object> target,
551543
"async_id_fields",
552544
env->async_hooks()->async_id_fields().GetJSArray());
553545

546+
FORCE_SET_TARGET_FIELD(target,
547+
"async_ids_fast_stack",
548+
env->async_hooks()->async_ids_fast_stack()
549+
.GetJSArray());
550+
554551
Local<Object> constants = Object::New(isolate);
555552
#define SET_HOOKS_CONSTANT(name) \
556553
FORCE_SET_TARGET_FIELD( \
@@ -567,6 +564,8 @@ void AsyncWrap::Initialize(Local<Object> target,
567564
SET_HOOKS_CONSTANT(kTriggerAsyncId);
568565
SET_HOOKS_CONSTANT(kAsyncIdCounter);
569566
SET_HOOKS_CONSTANT(kDefaultTriggerAsyncId);
567+
SET_HOOKS_CONSTANT(kStackLength);
568+
SET_HOOKS_CONSTANT(kFastStackCapacity);
570569
#undef SET_HOOKS_CONSTANT
571570
FORCE_SET_TARGET_FIELD(target, "constants", constants);
572571

src/async_wrap.h

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

src/env-inl.h

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ inline MultiIsolatePlatform* IsolateData::platform() const {
5555

5656
inline Environment::AsyncHooks::AsyncHooks(v8::Isolate* isolate)
5757
: isolate_(isolate),
58+
async_ids_fast_stack_(isolate, kFastStackCapacity * 2),
5859
fields_(isolate, kFieldsCount),
5960
async_id_fields_(isolate, kUidFieldsCount) {
6061
v8::HandleScope handle_scope(isolate_);
@@ -101,6 +102,11 @@ Environment::AsyncHooks::async_id_fields() {
101102
return async_id_fields_;
102103
}
103104

105+
inline AliasedBuffer<double, v8::Float64Array>&
106+
Environment::AsyncHooks::async_ids_fast_stack() {
107+
return async_ids_fast_stack_;
108+
}
109+
104110
inline v8::Local<v8::String> Environment::AsyncHooks::provider_string(int idx) {
105111
return providers_[idx].Get(isolate_);
106112
}
@@ -110,6 +116,7 @@ inline void Environment::AsyncHooks::no_force_checks() {
110116
fields_[kCheck] = fields_[kCheck] - 1;
111117
}
112118

119+
// Remember to keep this code aligned with pushAsyncIds() in JS.
113120
inline void Environment::AsyncHooks::push_async_ids(double async_id,
114121
double trigger_async_id) {
115122
// Since async_hooks is experimental, do only perform the check
@@ -119,16 +126,26 @@ inline void Environment::AsyncHooks::push_async_ids(double async_id,
119126
CHECK_GE(trigger_async_id, -1);
120127
}
121128

122-
async_ids_stack_.push({ async_id_fields_[kExecutionAsyncId],
123-
async_id_fields_[kTriggerAsyncId] });
129+
uint32_t offset = fields_[kStackLength];
130+
if (offset < kFastStackCapacity) {
131+
async_ids_fast_stack_[2 * offset] = async_id_fields_[kExecutionAsyncId];
132+
async_ids_fast_stack_[2 * offset + 1] = async_id_fields_[kTriggerAsyncId];
133+
} else {
134+
async_ids_stack_.push({
135+
async_id_fields_[kExecutionAsyncId],
136+
async_id_fields_[kTriggerAsyncId]
137+
});
138+
}
139+
fields_[kStackLength] = fields_[kStackLength] + 1;
124140
async_id_fields_[kExecutionAsyncId] = async_id;
125141
async_id_fields_[kTriggerAsyncId] = trigger_async_id;
126142
}
127143

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

133150
// Ask for the async_id to be restored as a check that the stack
134151
// hasn't been corrupted.
@@ -150,22 +167,26 @@ inline bool Environment::AsyncHooks::pop_async_id(double async_id) {
150167
ABORT_NO_BACKTRACE();
151168
}
152169

153-
auto async_ids = async_ids_stack_.top();
154-
async_ids_stack_.pop();
155-
async_id_fields_[kExecutionAsyncId] = async_ids.async_id;
156-
async_id_fields_[kTriggerAsyncId] = async_ids.trigger_async_id;
157-
return !async_ids_stack_.empty();
158-
}
159-
160-
inline size_t Environment::AsyncHooks::stack_size() {
161-
return async_ids_stack_.size();
170+
uint32_t offset = fields_[kStackLength] - 1;
171+
if (offset >= kFastStackCapacity) {
172+
auto async_ids = async_ids_stack_.top();
173+
async_ids_stack_.pop();
174+
async_id_fields_[kExecutionAsyncId] = async_ids.async_id;
175+
async_id_fields_[kTriggerAsyncId] = async_ids.trigger_async_id;
176+
} else {
177+
async_id_fields_[kExecutionAsyncId] = async_ids_fast_stack_[2 * offset];
178+
async_id_fields_[kTriggerAsyncId] = async_ids_fast_stack_[2 * offset + 1];
179+
}
180+
fields_[kStackLength] = offset;
181+
return fields_[kStackLength] > 0;
162182
}
163183

164184
inline void Environment::AsyncHooks::clear_async_id_stack() {
165185
while (!async_ids_stack_.empty())
166186
async_ids_stack_.pop();
167187
async_id_fields_[kExecutionAsyncId] = 0;
168188
async_id_fields_[kTriggerAsyncId] = 0;
189+
fields_[kStackLength] = 0;
169190
}
170191

171192
inline Environment::AsyncHooks::DefaultTriggerAsyncIdScope

src/env.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@ class Environment {
369369
kPromiseResolve,
370370
kTotals,
371371
kCheck,
372+
kStackLength,
372373
kFieldsCount,
373374
};
374375

@@ -380,18 +381,22 @@ class Environment {
380381
kUidFieldsCount,
381382
};
382383

384+
enum FastStackFields {
385+
kFastStackCapacity = 64
386+
};
387+
383388
AsyncHooks() = delete;
384389

385390
inline AliasedBuffer<uint32_t, v8::Uint32Array>& fields();
386391
inline AliasedBuffer<double, v8::Float64Array>& async_id_fields();
392+
inline AliasedBuffer<double, v8::Float64Array>& async_ids_fast_stack();
387393

388394
inline v8::Local<v8::String> provider_string(int idx);
389395

390396
inline void no_force_checks();
391397

392398
inline void push_async_ids(double async_id, double trigger_async_id);
393399
inline bool pop_async_id(double async_id);
394-
inline size_t stack_size();
395400
inline void clear_async_id_stack(); // Used in fatal exceptions.
396401

397402
// Used to set the kDefaultTriggerAsyncId in a scope. This is instead of
@@ -420,6 +425,9 @@ class Environment {
420425
v8::Isolate* isolate_;
421426
// Stores the ids of the current execution context stack.
422427
std::stack<async_context> async_ids_stack_;
428+
// Stores the ids of the current execution context stack with limited
429+
// capacity, but with the benefit of much faster access from JS.
430+
AliasedBuffer<double, v8::Float64Array> async_ids_fast_stack_;
423431
// Attached to a Uint32Array that tracks the number of active hooks for
424432
// each type.
425433
AliasedBuffer<uint32_t, v8::Uint32Array> fields_;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
'use strict';
2+
require('../common');
3+
const assert = require('assert');
4+
const async_hooks = require('async_hooks');
5+
6+
// This test verifies that the async ID stack can grow indefinitely.
7+
8+
function recurse(n) {
9+
const a = new async_hooks.AsyncResource('foobar');
10+
a.emitBefore();
11+
assert.strictEqual(a.asyncId(), async_hooks.executionAsyncId());
12+
assert.strictEqual(a.triggerAsyncId(), async_hooks.triggerAsyncId());
13+
if (n >= 0)
14+
recurse(n - 1);
15+
assert.strictEqual(a.asyncId(), async_hooks.executionAsyncId());
16+
assert.strictEqual(a.triggerAsyncId(), async_hooks.triggerAsyncId());
17+
a.emitAfter();
18+
}
19+
20+
recurse(1000);

0 commit comments

Comments
 (0)