From b605b15346f2a594d6b9031934eadc5625f871f9 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Sat, 9 Sep 2017 17:50:42 +0200 Subject: [PATCH] async_hooks: support promise resolve hook Add a `promiseResolve()` hook. PR-URL: https://github.com/nodejs/node/pull/15296 Reviewed-By: Trevor Norris Reviewed-By: James M Snell --- doc/api/async_hooks.md | 38 ++++++++++++++++++++++- lib/async_hooks.js | 21 ++++++++++--- src/async-wrap.cc | 26 ++++++++++++++-- src/async-wrap.h | 1 + src/env.h | 2 ++ test/parallel/test-async-hooks-promise.js | 5 +++ 6 files changed, 86 insertions(+), 7 deletions(-) diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 4771180ae8daf1..54b13f2f0aa707 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -36,7 +36,8 @@ const eid = async_hooks.executionAsyncId(); const tid = async_hooks.triggerAsyncId(); // Create a new AsyncHook instance. All of these callbacks are optional. -const asyncHook = async_hooks.createHook({ init, before, after, destroy }); +const asyncHook = + async_hooks.createHook({ init, before, after, destroy, promiseResolve }); // Allow callbacks of this AsyncHook instance to call. This is not an implicit // action after running the constructor, and must be explicitly run to begin @@ -65,6 +66,11 @@ function after(asyncId) { } // destroy is called when an AsyncWrap instance is destroyed. function destroy(asyncId) { } + +// promiseResolve is called only for promise resources, when the +// `resolve` function passed to the `Promise` constructor is invoked +// (either directly or through other means of resolving a promise). +function promiseResolve(asyncId) { } ``` #### `async_hooks.createHook(callbacks)` @@ -430,6 +436,36 @@ reference is made to the `resource` object passed to `init` it is possible that the resource does not depend on garbage collection, then this will not be an issue. +##### `promiseResolve(asyncId)` + +* `asyncId` {number} + +Called when the `resolve` function passed to the `Promise` constructor is +invoked (either directly or through other means of resolving a promise). + +Note that `resolve()` does not do any observable synchronous work. + +*Note:* This does not necessarily mean that the `Promise` is fulfilled or +rejected at this point, if the `Promise` was resolved by assuming the state +of another `Promise`. + +For example: + +```js +new Promise((resolve) => resolve(true)).then((a) => {}); +``` + +calls the following callbacks: + +``` +init for PROMISE with id 5, trigger id: 1 + promise resolve 5 # corresponds to resolve(true) +init for PROMISE with id 6, trigger id: 5 # the Promise returned by then() + before 6 # the then() callback is entered + promise resolve 6 # the then() callback resolves the promise by returning + after 6 +``` + #### `async_hooks.executionAsyncId()` * Returns {number} the `asyncId` of the current execution context. Useful to diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 249ffe64384876..37ff5650647f66 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -58,8 +58,8 @@ const active_hooks = { // Each constant tracks how many callbacks there are for any given step of // async execution. These are tracked so if the user didn't include callbacks // for a given step, that step can bail out early. -const { kInit, kBefore, kAfter, kDestroy, kTotals, kCurrentAsyncId, - kCurrentTriggerId, kAsyncUidCntr, +const { kInit, kBefore, kAfter, kDestroy, kPromiseResolve, kTotals, + kCurrentAsyncId, kCurrentTriggerId, kAsyncUidCntr, kInitTriggerId } = async_wrap.constants; // Symbols used to store the respective ids on both AsyncResource instances and @@ -72,9 +72,12 @@ const init_symbol = Symbol('init'); const before_symbol = Symbol('before'); const after_symbol = Symbol('after'); const destroy_symbol = Symbol('destroy'); +const promise_resolve_symbol = Symbol('promiseResolve'); const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative'); const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative'); const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative'); +const emitPromiseResolveNative = + emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative'); // TODO(refack): move to node-config.cc const abort_regex = /^--abort[_-]on[_-]uncaught[_-]exception$/; @@ -86,7 +89,8 @@ const abort_regex = /^--abort[_-]on[_-]uncaught[_-]exception$/; async_wrap.setupHooks({ init: emitInitNative, before: emitBeforeNative, after: emitAfterNative, - destroy: emitDestroyNative }); + destroy: emitDestroyNative, + promise_resolve: emitPromiseResolveNative }); // Used to fatally abort the process if a callback throws. function fatalError(e) { @@ -107,7 +111,7 @@ function fatalError(e) { // Public API // class AsyncHook { - constructor({ init, before, after, destroy }) { + constructor({ init, before, after, destroy, promiseResolve }) { if (init !== undefined && typeof init !== 'function') throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'init'); if (before !== undefined && typeof before !== 'function') @@ -116,11 +120,14 @@ class AsyncHook { throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before'); if (destroy !== undefined && typeof destroy !== 'function') throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before'); + if (promiseResolve !== undefined && typeof promiseResolve !== 'function') + throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'promiseResolve'); this[init_symbol] = init; this[before_symbol] = before; this[after_symbol] = after; this[destroy_symbol] = destroy; + this[promise_resolve_symbol] = promiseResolve; } enable() { @@ -144,6 +151,8 @@ class AsyncHook { hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol]; hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol]; hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol]; + hook_fields[kTotals] += + hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol]; hooks_array.push(this); if (prev_kTotals === 0 && hook_fields[kTotals] > 0) @@ -166,6 +175,8 @@ class AsyncHook { hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol]; hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol]; hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol]; + hook_fields[kTotals] += + hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol]; hooks_array.splice(index, 1); if (prev_kTotals > 0 && hook_fields[kTotals] === 0) @@ -198,6 +209,7 @@ function storeActiveHooks() { active_hooks.tmp_fields[kBefore] = async_hook_fields[kBefore]; active_hooks.tmp_fields[kAfter] = async_hook_fields[kAfter]; active_hooks.tmp_fields[kDestroy] = async_hook_fields[kDestroy]; + active_hooks.tmp_fields[kPromiseResolve] = async_hook_fields[kPromiseResolve]; } @@ -209,6 +221,7 @@ function restoreActiveHooks() { async_hook_fields[kBefore] = active_hooks.tmp_fields[kBefore]; async_hook_fields[kAfter] = active_hooks.tmp_fields[kAfter]; async_hook_fields[kDestroy] = active_hooks.tmp_fields[kDestroy]; + async_hook_fields[kPromiseResolve] = active_hooks.tmp_fields[kPromiseResolve]; active_hooks.tmp_array = null; active_hooks.tmp_fields = null; diff --git a/src/async-wrap.cc b/src/async-wrap.cc index b678f1500bb47f..9b20d1c42bf37e 100644 --- a/src/async-wrap.cc +++ b/src/async-wrap.cc @@ -181,6 +181,25 @@ static void PushBackDestroyId(Environment* env, double id) { } +void AsyncWrap::EmitPromiseResolve(Environment* env, double async_id) { + AsyncHooks* async_hooks = env->async_hooks(); + + if (async_hooks->fields()[AsyncHooks::kPromiseResolve] == 0) + return; + + Local uid = Number::New(env->isolate(), async_id); + Local fn = env->async_hooks_promise_resolve_function(); + TryCatch try_catch(env->isolate()); + MaybeLocal ar = fn->Call( + env->context(), Undefined(env->isolate()), 1, &uid); + if (ar.IsEmpty()) { + ClearFatalExceptionHandlers(env); + FatalException(env->isolate(), try_catch); + UNREACHABLE(); + } +} + + void AsyncWrap::EmitBefore(Environment* env, double async_id) { AsyncHooks* async_hooks = env->async_hooks(); @@ -303,8 +322,6 @@ static void PromiseHook(PromiseHookType type, Local promise, } wrap = PromiseWrap::New(env, promise, parent_wrap, silent); - } else if (type == PromiseHookType::kResolve) { - // TODO(matthewloring): need to expose this through the async hooks api. } CHECK_NE(wrap, nullptr); @@ -321,6 +338,8 @@ static void PromiseHook(PromiseHookType type, Local promise, // PromiseHookType::kBefore that was not witnessed by the PromiseHook. env->async_hooks()->pop_ids(wrap->get_id()); } + } else if (type == PromiseHookType::kResolve) { + AsyncWrap::EmitPromiseResolve(wrap->env(), wrap->get_id()); } } @@ -349,6 +368,7 @@ static void SetupHooks(const FunctionCallbackInfo& args) { SET_HOOK_FN(before); SET_HOOK_FN(after); SET_HOOK_FN(destroy); + SET_HOOK_FN(promise_resolve); #undef SET_HOOK_FN { @@ -500,6 +520,7 @@ void AsyncWrap::Initialize(Local target, SET_HOOKS_CONSTANT(kBefore); SET_HOOKS_CONSTANT(kAfter); SET_HOOKS_CONSTANT(kDestroy); + SET_HOOKS_CONSTANT(kPromiseResolve); SET_HOOKS_CONSTANT(kTotals); SET_HOOKS_CONSTANT(kCurrentAsyncId); SET_HOOKS_CONSTANT(kCurrentTriggerId); @@ -533,6 +554,7 @@ void AsyncWrap::Initialize(Local target, env->set_async_hooks_before_function(Local()); env->set_async_hooks_after_function(Local()); env->set_async_hooks_destroy_function(Local()); + env->set_async_hooks_promise_resolve_function(Local()); } diff --git a/src/async-wrap.h b/src/async-wrap.h index 681d4fd13b9af9..dd9d038582f207 100644 --- a/src/async-wrap.h +++ b/src/async-wrap.h @@ -123,6 +123,7 @@ class AsyncWrap : public BaseObject { static void EmitBefore(Environment* env, double id); static void EmitAfter(Environment* env, double id); + static void EmitPromiseResolve(Environment* env, double id); inline ProviderType provider_type() const; diff --git a/src/env.h b/src/env.h index 8de937eb37fcbe..cc2e2d987b7ab4 100644 --- a/src/env.h +++ b/src/env.h @@ -295,6 +295,7 @@ struct performance_state; V(async_hooks_init_function, v8::Function) \ V(async_hooks_before_function, v8::Function) \ V(async_hooks_after_function, v8::Function) \ + V(async_hooks_promise_resolve_function, v8::Function) \ V(binding_cache_object, v8::Object) \ V(buffer_prototype_object, v8::Object) \ V(context, v8::Context) \ @@ -377,6 +378,7 @@ class Environment { kBefore, kAfter, kDestroy, + kPromiseResolve, kTotals, kFieldsCount, }; diff --git a/test/parallel/test-async-hooks-promise.js b/test/parallel/test-async-hooks-promise.js index c2d7bc76da64fb..d712fd616c647b 100644 --- a/test/parallel/test-async-hooks-promise.js +++ b/test/parallel/test-async-hooks-promise.js @@ -4,11 +4,16 @@ const assert = require('assert'); const async_hooks = require('async_hooks'); const initCalls = []; +const resolveCalls = []; async_hooks.createHook({ init: common.mustCall((id, type, triggerId, resource) => { assert.strictEqual(type, 'PROMISE'); initCalls.push({ id, triggerId, resource }); + }, 2), + promiseResolve: common.mustCall((id) => { + assert.strictEqual(initCalls[resolveCalls.length].id, id); + resolveCalls.push(id); }, 2) }).enable();