Skip to content

Commit b605b15

Browse files
committed
async_hooks: support promise resolve hook
Add a `promiseResolve()` hook. PR-URL: #15296 Reviewed-By: Trevor Norris <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 7a95392 commit b605b15

File tree

6 files changed

+86
-7
lines changed

6 files changed

+86
-7
lines changed

doc/api/async_hooks.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const eid = async_hooks.executionAsyncId();
3636
const tid = async_hooks.triggerAsyncId();
3737

3838
// Create a new AsyncHook instance. All of these callbacks are optional.
39-
const asyncHook = async_hooks.createHook({ init, before, after, destroy });
39+
const asyncHook =
40+
async_hooks.createHook({ init, before, after, destroy, promiseResolve });
4041

4142
// Allow callbacks of this AsyncHook instance to call. This is not an implicit
4243
// action after running the constructor, and must be explicitly run to begin
@@ -65,6 +66,11 @@ function after(asyncId) { }
6566

6667
// destroy is called when an AsyncWrap instance is destroyed.
6768
function destroy(asyncId) { }
69+
70+
// promiseResolve is called only for promise resources, when the
71+
// `resolve` function passed to the `Promise` constructor is invoked
72+
// (either directly or through other means of resolving a promise).
73+
function promiseResolve(asyncId) { }
6874
```
6975

7076
#### `async_hooks.createHook(callbacks)`
@@ -430,6 +436,36 @@ reference is made to the `resource` object passed to `init` it is possible that
430436
the resource does not depend on garbage collection, then this will not be an
431437
issue.
432438

439+
##### `promiseResolve(asyncId)`
440+
441+
* `asyncId` {number}
442+
443+
Called when the `resolve` function passed to the `Promise` constructor is
444+
invoked (either directly or through other means of resolving a promise).
445+
446+
Note that `resolve()` does not do any observable synchronous work.
447+
448+
*Note:* This does not necessarily mean that the `Promise` is fulfilled or
449+
rejected at this point, if the `Promise` was resolved by assuming the state
450+
of another `Promise`.
451+
452+
For example:
453+
454+
```js
455+
new Promise((resolve) => resolve(true)).then((a) => {});
456+
```
457+
458+
calls the following callbacks:
459+
460+
```
461+
init for PROMISE with id 5, trigger id: 1
462+
promise resolve 5 # corresponds to resolve(true)
463+
init for PROMISE with id 6, trigger id: 5 # the Promise returned by then()
464+
before 6 # the then() callback is entered
465+
promise resolve 6 # the then() callback resolves the promise by returning
466+
after 6
467+
```
468+
433469
#### `async_hooks.executionAsyncId()`
434470

435471
* Returns {number} the `asyncId` of the current execution context. Useful to

lib/async_hooks.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ const active_hooks = {
5858
// Each constant tracks how many callbacks there are for any given step of
5959
// async execution. These are tracked so if the user didn't include callbacks
6060
// for a given step, that step can bail out early.
61-
const { kInit, kBefore, kAfter, kDestroy, kTotals, kCurrentAsyncId,
62-
kCurrentTriggerId, kAsyncUidCntr,
61+
const { kInit, kBefore, kAfter, kDestroy, kPromiseResolve, kTotals,
62+
kCurrentAsyncId, kCurrentTriggerId, kAsyncUidCntr,
6363
kInitTriggerId } = async_wrap.constants;
6464

6565
// Symbols used to store the respective ids on both AsyncResource instances and
@@ -72,9 +72,12 @@ const init_symbol = Symbol('init');
7272
const before_symbol = Symbol('before');
7373
const after_symbol = Symbol('after');
7474
const destroy_symbol = Symbol('destroy');
75+
const promise_resolve_symbol = Symbol('promiseResolve');
7576
const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative');
7677
const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative');
7778
const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
79+
const emitPromiseResolveNative =
80+
emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative');
7881

7982
// TODO(refack): move to node-config.cc
8083
const abort_regex = /^--abort[_-]on[_-]uncaught[_-]exception$/;
@@ -86,7 +89,8 @@ const abort_regex = /^--abort[_-]on[_-]uncaught[_-]exception$/;
8689
async_wrap.setupHooks({ init: emitInitNative,
8790
before: emitBeforeNative,
8891
after: emitAfterNative,
89-
destroy: emitDestroyNative });
92+
destroy: emitDestroyNative,
93+
promise_resolve: emitPromiseResolveNative });
9094

9195
// Used to fatally abort the process if a callback throws.
9296
function fatalError(e) {
@@ -107,7 +111,7 @@ function fatalError(e) {
107111
// Public API //
108112

109113
class AsyncHook {
110-
constructor({ init, before, after, destroy }) {
114+
constructor({ init, before, after, destroy, promiseResolve }) {
111115
if (init !== undefined && typeof init !== 'function')
112116
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'init');
113117
if (before !== undefined && typeof before !== 'function')
@@ -116,11 +120,14 @@ class AsyncHook {
116120
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
117121
if (destroy !== undefined && typeof destroy !== 'function')
118122
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
123+
if (promiseResolve !== undefined && typeof promiseResolve !== 'function')
124+
throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'promiseResolve');
119125

120126
this[init_symbol] = init;
121127
this[before_symbol] = before;
122128
this[after_symbol] = after;
123129
this[destroy_symbol] = destroy;
130+
this[promise_resolve_symbol] = promiseResolve;
124131
}
125132

126133
enable() {
@@ -144,6 +151,8 @@ class AsyncHook {
144151
hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
145152
hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
146153
hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
154+
hook_fields[kTotals] +=
155+
hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
147156
hooks_array.push(this);
148157

149158
if (prev_kTotals === 0 && hook_fields[kTotals] > 0)
@@ -166,6 +175,8 @@ class AsyncHook {
166175
hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
167176
hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
168177
hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
178+
hook_fields[kTotals] +=
179+
hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol];
169180
hooks_array.splice(index, 1);
170181

171182
if (prev_kTotals > 0 && hook_fields[kTotals] === 0)
@@ -198,6 +209,7 @@ function storeActiveHooks() {
198209
active_hooks.tmp_fields[kBefore] = async_hook_fields[kBefore];
199210
active_hooks.tmp_fields[kAfter] = async_hook_fields[kAfter];
200211
active_hooks.tmp_fields[kDestroy] = async_hook_fields[kDestroy];
212+
active_hooks.tmp_fields[kPromiseResolve] = async_hook_fields[kPromiseResolve];
201213
}
202214

203215

@@ -209,6 +221,7 @@ function restoreActiveHooks() {
209221
async_hook_fields[kBefore] = active_hooks.tmp_fields[kBefore];
210222
async_hook_fields[kAfter] = active_hooks.tmp_fields[kAfter];
211223
async_hook_fields[kDestroy] = active_hooks.tmp_fields[kDestroy];
224+
async_hook_fields[kPromiseResolve] = active_hooks.tmp_fields[kPromiseResolve];
212225

213226
active_hooks.tmp_array = null;
214227
active_hooks.tmp_fields = null;

src/async-wrap.cc

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,25 @@ static void PushBackDestroyId(Environment* env, double id) {
181181
}
182182

183183

184+
void AsyncWrap::EmitPromiseResolve(Environment* env, double async_id) {
185+
AsyncHooks* async_hooks = env->async_hooks();
186+
187+
if (async_hooks->fields()[AsyncHooks::kPromiseResolve] == 0)
188+
return;
189+
190+
Local<Value> uid = Number::New(env->isolate(), async_id);
191+
Local<Function> fn = env->async_hooks_promise_resolve_function();
192+
TryCatch try_catch(env->isolate());
193+
MaybeLocal<Value> ar = fn->Call(
194+
env->context(), Undefined(env->isolate()), 1, &uid);
195+
if (ar.IsEmpty()) {
196+
ClearFatalExceptionHandlers(env);
197+
FatalException(env->isolate(), try_catch);
198+
UNREACHABLE();
199+
}
200+
}
201+
202+
184203
void AsyncWrap::EmitBefore(Environment* env, double async_id) {
185204
AsyncHooks* async_hooks = env->async_hooks();
186205

@@ -303,8 +322,6 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise,
303322
}
304323

305324
wrap = PromiseWrap::New(env, promise, parent_wrap, silent);
306-
} else if (type == PromiseHookType::kResolve) {
307-
// TODO(matthewloring): need to expose this through the async hooks api.
308325
}
309326

310327
CHECK_NE(wrap, nullptr);
@@ -321,6 +338,8 @@ static void PromiseHook(PromiseHookType type, Local<Promise> promise,
321338
// PromiseHookType::kBefore that was not witnessed by the PromiseHook.
322339
env->async_hooks()->pop_ids(wrap->get_id());
323340
}
341+
} else if (type == PromiseHookType::kResolve) {
342+
AsyncWrap::EmitPromiseResolve(wrap->env(), wrap->get_id());
324343
}
325344
}
326345

@@ -349,6 +368,7 @@ static void SetupHooks(const FunctionCallbackInfo<Value>& args) {
349368
SET_HOOK_FN(before);
350369
SET_HOOK_FN(after);
351370
SET_HOOK_FN(destroy);
371+
SET_HOOK_FN(promise_resolve);
352372
#undef SET_HOOK_FN
353373

354374
{
@@ -500,6 +520,7 @@ void AsyncWrap::Initialize(Local<Object> target,
500520
SET_HOOKS_CONSTANT(kBefore);
501521
SET_HOOKS_CONSTANT(kAfter);
502522
SET_HOOKS_CONSTANT(kDestroy);
523+
SET_HOOKS_CONSTANT(kPromiseResolve);
503524
SET_HOOKS_CONSTANT(kTotals);
504525
SET_HOOKS_CONSTANT(kCurrentAsyncId);
505526
SET_HOOKS_CONSTANT(kCurrentTriggerId);
@@ -533,6 +554,7 @@ void AsyncWrap::Initialize(Local<Object> target,
533554
env->set_async_hooks_before_function(Local<Function>());
534555
env->set_async_hooks_after_function(Local<Function>());
535556
env->set_async_hooks_destroy_function(Local<Function>());
557+
env->set_async_hooks_promise_resolve_function(Local<Function>());
536558
}
537559

538560

src/async-wrap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class AsyncWrap : public BaseObject {
123123

124124
static void EmitBefore(Environment* env, double id);
125125
static void EmitAfter(Environment* env, double id);
126+
static void EmitPromiseResolve(Environment* env, double id);
126127

127128
inline ProviderType provider_type() const;
128129

src/env.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ struct performance_state;
295295
V(async_hooks_init_function, v8::Function) \
296296
V(async_hooks_before_function, v8::Function) \
297297
V(async_hooks_after_function, v8::Function) \
298+
V(async_hooks_promise_resolve_function, v8::Function) \
298299
V(binding_cache_object, v8::Object) \
299300
V(buffer_prototype_object, v8::Object) \
300301
V(context, v8::Context) \
@@ -377,6 +378,7 @@ class Environment {
377378
kBefore,
378379
kAfter,
379380
kDestroy,
381+
kPromiseResolve,
380382
kTotals,
381383
kFieldsCount,
382384
};

test/parallel/test-async-hooks-promise.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ const assert = require('assert');
44
const async_hooks = require('async_hooks');
55

66
const initCalls = [];
7+
const resolveCalls = [];
78

89
async_hooks.createHook({
910
init: common.mustCall((id, type, triggerId, resource) => {
1011
assert.strictEqual(type, 'PROMISE');
1112
initCalls.push({ id, triggerId, resource });
13+
}, 2),
14+
promiseResolve: common.mustCall((id) => {
15+
assert.strictEqual(initCalls[resolveCalls.length].id, id);
16+
resolveCalls.push(id);
1217
}, 2)
1318
}).enable();
1419

0 commit comments

Comments
 (0)