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`); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 5af6e86651af28..db85a64e33a903 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -66,7 +66,7 @@ const customTypesMap = { 'AsyncLocalStorage': 'async_context.html#class-asynclocalstorage', - 'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks', + 'AsyncHook': 'async_hooks.html#async_hookscreatehookoptions', 'AsyncResource': 'async_hooks.html#class-asyncresource', 'brotli options': 'zlib.html#class-brotlioptions', diff --git a/tools/gdbinit b/tools/gdbinit new file mode 100644 index 00000000000000..4c564153a0b457 --- /dev/null +++ b/tools/gdbinit @@ -0,0 +1,52 @@ +# Add a simple unwinder which, on x64, walks frame pointers when there +# is no source information available. + +python + +from gdb.unwinder import Unwinder + +class V8UnwinderFrameId(object): + def __init__(self, sp, pc): + self.sp = sp + self.pc = pc + +class V8Unwinder(Unwinder): + def __init__(self): + super(V8Unwinder, self).__init__("V8Unwinder") + self.enabled = True + + def __call__(self, pending_frame): + try: + # Only supported on x64. + if gdb.selected_inferior().architecture().name() != "i386:x86-64": + return None + + pc = pending_frame.read_register("rip") + sym_and_line = gdb.current_progspace().find_pc_line(int(pc)) + + if sym_and_line.symtab is not None: + return None + fp = pending_frame.read_register("rbp").reinterpret_cast( + gdb.lookup_type("void").pointer().pointer()) + + next_sp = fp + next_fp = fp.dereference() + next_pc = (fp+1).dereference() + + frame_info = V8UnwinderFrameId(next_sp, next_pc) + + # create_unwind_info seems to sometimes have issues accessing + # the frame_info if it's not first accessed in Python. + _lol_gdb_workaround = frame_info.pc + 1 + + unwind_info = pending_frame.create_unwind_info(frame_info) + unwind_info.add_saved_register("rsp", next_sp) + unwind_info.add_saved_register("rip", next_pc) + unwind_info.add_saved_register("rbp", next_fp) + return unwind_info + except Exception as e: + return None + +gdb.unwinder.register_unwinder(None, V8Unwinder(), replace=True) + +end