diff --git a/doc/api/async_hooks.md b/doc/api/async_hooks.md index 07793fca90e445..a863350fc2d26d 100644 --- a/doc/api/async_hooks.md +++ b/doc/api/async_hooks.md @@ -144,18 +144,20 @@ function destroy(asyncId) { } function promiseResolve(asyncId) { } ``` -## `async_hooks.createHook(callbacks)` +## `async_hooks.createHook(options)` -* `callbacks` {Object} The [Hook Callbacks][] to register +* `options` {Object} The [Hook Callbacks][] to register * `init` {Function} The [`init` callback][]. * `before` {Function} The [`before` callback][]. * `after` {Function} The [`after` callback][]. * `destroy` {Function} The [`destroy` callback][]. * `promiseResolve` {Function} The [`promiseResolve` callback][]. + * `trackPromises` {boolean} Whether the hook should track `Promise`s. Cannot be `false` if + `promiseResolve` is set. **Default**: `true`. * Returns: {AsyncHook} Instance used for disabling and enabling hooks Registers functions to be called for different lifetime events of each async @@ -354,7 +356,8 @@ Furthermore users of [`AsyncResource`][] create async resources independent of Node.js itself. There is also the `PROMISE` resource type, which is used to track `Promise` -instances and asynchronous work scheduled by them. +instances and asynchronous work scheduled by them. The `Promise`s are only +tracked when `trackPromises` option is set to `true`. Users are able to define their own `type` when using the public embedder API. @@ -910,6 +913,38 @@ only on chained promises. That means promises not created by `then()`/`catch()` will not have the `before` and `after` callbacks fired on them. For more details see the details of the V8 [PromiseHooks][] API. +### Disabling promise execution tracking + +Tracking promise execution can cause a significant performance overhead. +To opt out of promise tracking, set `trackPromises` to `false`: + +```cjs +const { createHook } = require('node:async_hooks'); +const { writeSync } = require('node:fs'); +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + // This init hook does not get called when trackPromises is set to false. + writeSync(1, `init hook triggered for ${type}\n`); + }, + trackPromises: false, // Do not track promises. +}).enable(); +Promise.resolve(1729); +``` + +```mjs +import { createHook } from 'node:async_hooks'; +import { writeSync } from 'node:fs'; + +createHook({ + init(asyncId, type, triggerAsyncId, resource) { + // This init hook does not get called when trackPromises is set to false. + writeSync(1, `init hook triggered for ${type}\n`); + }, + trackPromises: false, // Do not track promises. +}).enable(); +Promise.resolve(1729); +``` + ## JavaScript embedder API Library developers that handle their own asynchronous resources performing tasks @@ -934,7 +969,7 @@ The documentation for this class has moved [`AsyncLocalStorage`][]. [`Worker`]: worker_threads.md#class-worker [`after` callback]: #afterasyncid [`before` callback]: #beforeasyncid -[`createHook`]: #async_hookscreatehookcallbacks +[`createHook`]: #async_hookscreatehookoptions [`destroy` callback]: #destroyasyncid [`executionAsyncResource`]: #async_hooksexecutionasyncresource [`init` callback]: #initasyncid-type-triggerasyncid-resource diff --git a/lib/async_hooks.js b/lib/async_hooks.js index 3e3982a7ac61e5..5922971b9be68b 100644 --- a/lib/async_hooks.js +++ b/lib/async_hooks.js @@ -18,6 +18,8 @@ const { ERR_ASYNC_CALLBACK, ERR_ASYNC_TYPE, ERR_INVALID_ASYNC_ID, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, } = require('internal/errors').codes; const { kEmptyObject, @@ -71,7 +73,7 @@ const { // Listener API // class AsyncHook { - constructor({ init, before, after, destroy, promiseResolve }) { + constructor({ init, before, after, destroy, promiseResolve, trackPromises }) { if (init !== undefined && typeof init !== 'function') throw new ERR_ASYNC_CALLBACK('hook.init'); if (before !== undefined && typeof before !== 'function') @@ -82,13 +84,25 @@ class AsyncHook { throw new ERR_ASYNC_CALLBACK('hook.destroy'); if (promiseResolve !== undefined && typeof promiseResolve !== 'function') throw new ERR_ASYNC_CALLBACK('hook.promiseResolve'); + if (trackPromises !== undefined && typeof trackPromises !== 'boolean') { + throw new ERR_INVALID_ARG_TYPE('trackPromises', 'boolean', trackPromises); + } this[init_symbol] = init; this[before_symbol] = before; this[after_symbol] = after; this[destroy_symbol] = destroy; this[promise_resolve_symbol] = promiseResolve; - this[kNoPromiseHook] = false; + if (trackPromises === false) { + if (promiseResolve) { + throw new ERR_INVALID_ARG_VALUE('trackPromises', + trackPromises, 'must not be false when promiseResolve is enabled'); + } + this[kNoPromiseHook] = true; + } else { + // Default to tracking promises for now. + this[kNoPromiseHook] = false; + } } enable() { diff --git a/lib/internal/inspector_async_hook.js b/lib/internal/inspector_async_hook.js index db78e05f0a1da9..8ab833ea8ef1e9 100644 --- a/lib/internal/inspector_async_hook.js +++ b/lib/internal/inspector_async_hook.js @@ -7,7 +7,6 @@ function lazyHookCreation() { const inspector = internalBinding('inspector'); const { createHook } = require('async_hooks'); config = internalBinding('config'); - const { kNoPromiseHook } = require('internal/async_hooks'); hook = createHook({ init(asyncId, type, triggerAsyncId, resource) { @@ -30,8 +29,8 @@ function lazyHookCreation() { destroy(asyncId) { inspector.asyncTaskCanceled(asyncId); }, + trackPromises: false, }); - hook[kNoPromiseHook] = true; } function enable() { diff --git a/test/async-hooks/test-track-promises-default.js b/test/async-hooks/test-track-promises-default.js new file mode 100644 index 00000000000000..071f8faaecc712 --- /dev/null +++ b/test/async-hooks/test-track-promises-default.js @@ -0,0 +1,16 @@ +'use strict'; +// Test that trackPromises default to true. +const common = require('../common'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); + +let res; +createHook({ + init: common.mustCall((asyncId, type, triggerAsyncId, resource) => { + assert.strictEqual(type, 'PROMISE'); + res = resource; + }), +}).enable(); + +const promise = Promise.resolve(1729); +assert.strictEqual(res, promise); diff --git a/test/async-hooks/test-track-promises-false-check.js b/test/async-hooks/test-track-promises-false-check.js new file mode 100644 index 00000000000000..7ff4142d0d1ff5 --- /dev/null +++ b/test/async-hooks/test-track-promises-false-check.js @@ -0,0 +1,19 @@ +// Flags: --expose-internals +'use strict'; +// Test that trackPromises: false prevents promise hooks from being installed. + +require('../common'); +const { internalBinding } = require('internal/test/binding'); +const { getPromiseHooks } = internalBinding('async_wrap'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); + +createHook({ + init() { + // This can get called for writes to stdout due to the warning about internals. + }, + trackPromises: false, +}).enable(); + +Promise.resolve(1729); +assert.deepStrictEqual(getPromiseHooks(), [undefined, undefined, undefined, undefined]); diff --git a/test/async-hooks/test-track-promises-false.js b/test/async-hooks/test-track-promises-false.js new file mode 100644 index 00000000000000..1a3b5eda2e2f3c --- /dev/null +++ b/test/async-hooks/test-track-promises-false.js @@ -0,0 +1,11 @@ +'use strict'; +// Test that trackPromises: false works. +const common = require('../common'); +const { createHook } = require('node:async_hooks'); + +createHook({ + init: common.mustNotCall(), + trackPromises: false, +}).enable(); + +Promise.resolve(1729); diff --git a/test/async-hooks/test-track-promises-true.js b/test/async-hooks/test-track-promises-true.js new file mode 100644 index 00000000000000..cc53f3a9c926e8 --- /dev/null +++ b/test/async-hooks/test-track-promises-true.js @@ -0,0 +1,17 @@ +'use strict'; +// Test that trackPromises: true works. +const common = require('../common'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); + +let res; +createHook({ + init: common.mustCall((asyncId, type, triggerAsyncId, resource) => { + assert.strictEqual(type, 'PROMISE'); + res = resource; + }), + trackPromises: true, +}).enable(); + +const promise = Promise.resolve(1729); +assert.strictEqual(res, promise); diff --git a/test/async-hooks/test-track-promises-validation.js b/test/async-hooks/test-track-promises-validation.js new file mode 100644 index 00000000000000..108f82ff2562ce --- /dev/null +++ b/test/async-hooks/test-track-promises-validation.js @@ -0,0 +1,25 @@ +'use strict'; +// Test validation of trackPromises option. + +require('../common'); +const { createHook } = require('node:async_hooks'); +const assert = require('node:assert'); +const { inspect } = require('util'); + +for (const invalid of [0, null, 1, NaN, Symbol(0), function() {}, 'test']) { + assert.throws( + () => createHook({ + init() {}, + trackPromises: invalid, + }), + { code: 'ERR_INVALID_ARG_TYPE' }, + `trackPromises: ${inspect(invalid)} should throw`); +} + +assert.throws( + () => createHook({ + trackPromises: false, + promiseResolve() {}, + }), + { code: 'ERR_INVALID_ARG_VALUE' }, + `trackPromises: false and promiseResolve() are incompatible`);