Skip to content

Commit 4239d07

Browse files
joyeecheungjuanarbol
authored andcommitted
async_hooks: add trackPromises option to createHook()
This adds a trackPromises option that allows users to completely opt out of the promise hooks that are installed whenever an async hook is added. For those who do not need to track promises, this avoids the excessive hook invocation and the heavy overhead from it. This option was previously already implemented internally to skip the noise from promise hooks when debugging async operations via the V8 inspector. This patch just exposes it. PR-URL: #61415 Refs: #57148 Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
1 parent abd31da commit 4239d07

9 files changed

Lines changed: 145 additions & 9 deletions

‎doc/api/async_hooks.md‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,20 @@ function destroy(asyncId) { }
144144
functionpromiseResolve(asyncId) { }
145145
```
146146

147-
## `async_hooks.createHook(callbacks)`
147+
## `async_hooks.createHook(options)`
148148

149149
<!-- YAML
150150
added: v8.1.0
151151
-->
152152

153-
*`callbacks` {Object} The [Hook Callbacks][] to register
153+
*`options` {Object} The [Hook Callbacks][] to register
154154
*`init` {Function} The [`init` callback][].
155155
*`before` {Function} The [`before` callback][].
156156
*`after` {Function} The [`after` callback][].
157157
*`destroy` {Function} The [`destroy` callback][].
158158
*`promiseResolve` {Function} The [`promiseResolve` callback][].
159+
*`trackPromises` {boolean} Whether the hook should track `Promise`s. Cannot be `false` if
160+
`promiseResolve` is set. **Default**: `true`.
159161
* Returns: {AsyncHook} Instance used for disabling and enabling hooks
160162

161163
Registers functions to be called for different lifetime events of each async
@@ -354,7 +356,8 @@ Furthermore users of [`AsyncResource`][] create async resources independent
354356
of Node.js itself.
355357

356358
There is also the `PROMISE` resource type, which is used to track `Promise`
357-
instances and asynchronous work scheduled by them.
359+
instances and asynchronous work scheduled by them. The `Promise`s are only
360+
tracked when `trackPromises` option is set to `true`.
358361

359362
Users are able to define their own `type` when using the public embedder API.
360363

@@ -910,6 +913,38 @@ only on chained promises. That means promises not created by `then()`/`catch()`
910913
will not have the `before` and `after` callbacks fired on them. For more details
911914
see the details of the V8 [PromiseHooks][] API.
912915
916+
### Disabling promise execution tracking
917+
918+
Tracking promise execution can cause a significant performance overhead.
919+
To opt out of promise tracking, set `trackPromises` to `false`:
920+
921+
```cjs
922+
const { createHook } =require('node:async_hooks');
923+
const { writeSync } =require('node:fs');
924+
createHook({
925+
init(asyncId, type, triggerAsyncId, resource) {
926+
// This init hook does not get called when trackPromises is set to false.
927+
writeSync(1, `init hook triggered for ${type}\n`);
928+
},
929+
trackPromises:false, // Do not track promises.
930+
}).enable();
931+
Promise.resolve(1729);
932+
```
933+
934+
```mjs
935+
import { createHook } from'node:async_hooks';
936+
import { writeSync } from'node:fs';
937+
938+
createHook({
939+
init(asyncId, type, triggerAsyncId, resource) {
940+
// This init hook does not get called when trackPromises is set to false.
941+
writeSync(1, `init hook triggered for ${type}\n`);
942+
},
943+
trackPromises:false, // Do not track promises.
944+
}).enable();
945+
Promise.resolve(1729);
946+
```
947+
913948
## JavaScript embedder API
914949
915950
Library developers that handle their own asynchronous resources performing tasks
@@ -934,7 +969,7 @@ The documentation for this class has moved [`AsyncLocalStorage`][].
934969
[`Worker`]: worker_threads.md#class-worker
935970
[`after` callback]: #afterasyncid
936971
[`before` callback]: #beforeasyncid
937-
[`createHook`]: #async_hookscreatehookcallbacks
972+
[`createHook`]: #async_hookscreatehookoptions
938973
[`destroy` callback]: #destroyasyncid
939974
[`executionAsyncResource`]: #async_hooksexecutionasyncresource
940975
[`init` callback]: #initasyncid-type-triggerasyncid-resource

‎lib/async_hooks.js‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const {
1818
ERR_ASYNC_CALLBACK,
1919
ERR_ASYNC_TYPE,
2020
ERR_INVALID_ASYNC_ID,
21+
ERR_INVALID_ARG_TYPE,
22+
ERR_INVALID_ARG_VALUE,
2123
}=require('internal/errors').codes;
2224
const{
2325
deprecate,
@@ -73,7 +75,7 @@ const {
7375
// Listener API //
7476

7577
classAsyncHook{
76-
constructor({ init, before, after, destroy, promiseResolve }){
78+
constructor({ init, before, after, destroy, promiseResolve, trackPromises}){
7779
if(init!==undefined&&typeofinit!=='function')
7880
thrownewERR_ASYNC_CALLBACK('hook.init');
7981
if(before!==undefined&&typeofbefore!=='function')
@@ -84,13 +86,25 @@ class AsyncHook {
8486
thrownewERR_ASYNC_CALLBACK('hook.destroy');
8587
if(promiseResolve!==undefined&&typeofpromiseResolve!=='function')
8688
thrownewERR_ASYNC_CALLBACK('hook.promiseResolve');
89+
if(trackPromises!==undefined&&typeoftrackPromises!=='boolean'){
90+
thrownewERR_INVALID_ARG_TYPE('trackPromises','boolean',trackPromises);
91+
}
8792

8893
this[init_symbol]=init;
8994
this[before_symbol]=before;
9095
this[after_symbol]=after;
9196
this[destroy_symbol]=destroy;
9297
this[promise_resolve_symbol]=promiseResolve;
93-
this[kNoPromiseHook]=false;
98+
if(trackPromises===false){
99+
if(promiseResolve){
100+
thrownewERR_INVALID_ARG_VALUE('trackPromises',
101+
trackPromises,'must not be false when promiseResolve is enabled');
102+
}
103+
this[kNoPromiseHook]=true;
104+
}else{
105+
// Default to tracking promises for now.
106+
this[kNoPromiseHook]=false;
107+
}
94108
}
95109

96110
enable(){

‎lib/internal/inspector_async_hook.js‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ function lazyHookCreation() {
77
constinspector=internalBinding('inspector');
88
const{ createHook }=require('async_hooks');
99
config=internalBinding('config');
10-
const{ kNoPromiseHook }=require('internal/async_hooks');
1110

1211
hook=createHook({
1312
init(asyncId,type,triggerAsyncId,resource){
@@ -30,8 +29,8 @@ function lazyHookCreation() {
3029
destroy(asyncId){
3130
inspector.asyncTaskCanceled(asyncId);
3231
},
32+
trackPromises: false,
3333
});
34-
hook[kNoPromiseHook]=true;
3534
}
3635

3736
functionenable(){
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
// Test that trackPromises default to true.
3+
constcommon=require('../common');
4+
const{ createHook }=require('node:async_hooks');
5+
constassert=require('node:assert');
6+
7+
letres;
8+
createHook({
9+
init: common.mustCall((asyncId,type,triggerAsyncId,resource)=>{
10+
assert.strictEqual(type,'PROMISE');
11+
res=resource;
12+
}),
13+
}).enable();
14+
15+
constpromise=Promise.resolve(1729);
16+
assert.strictEqual(res,promise);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Flags: --expose-internals
2+
'use strict';
3+
// Test that trackPromises: false prevents promise hooks from being installed.
4+
5+
require('../common');
6+
const{ internalBinding }=require('internal/test/binding');
7+
const{ getPromiseHooks }=internalBinding('async_wrap');
8+
const{ createHook }=require('node:async_hooks');
9+
constassert=require('node:assert');
10+
11+
createHook({
12+
init(){
13+
// This can get called for writes to stdout due to the warning about internals.
14+
},
15+
trackPromises: false,
16+
}).enable();
17+
18+
Promise.resolve(1729);
19+
assert.deepStrictEqual(getPromiseHooks(),[undefined,undefined,undefined,undefined]);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
// Test that trackPromises: false works.
3+
constcommon=require('../common');
4+
const{ createHook }=require('node:async_hooks');
5+
6+
createHook({
7+
init: common.mustNotCall(),
8+
trackPromises: false,
9+
}).enable();
10+
11+
Promise.resolve(1729);
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
// Test that trackPromises: true works.
3+
constcommon=require('../common');
4+
const{ createHook }=require('node:async_hooks');
5+
constassert=require('node:assert');
6+
7+
letres;
8+
createHook({
9+
init: common.mustCall((asyncId,type,triggerAsyncId,resource)=>{
10+
assert.strictEqual(type,'PROMISE');
11+
res=resource;
12+
}),
13+
trackPromises: true,
14+
}).enable();
15+
16+
constpromise=Promise.resolve(1729);
17+
assert.strictEqual(res,promise);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
// Test validation of trackPromises option.
3+
4+
require('../common');
5+
const{ createHook }=require('node:async_hooks');
6+
constassert=require('node:assert');
7+
const{ inspect }=require('util');
8+
9+
for(constinvalidof[0,null,1,NaN,Symbol(0),function(){},'test']){
10+
assert.throws(
11+
()=>createHook({
12+
init(){},
13+
trackPromises: invalid,
14+
}),
15+
{code: 'ERR_INVALID_ARG_TYPE'},
16+
`trackPromises: ${inspect(invalid)} should throw`);
17+
}
18+
19+
assert.throws(
20+
()=>createHook({
21+
trackPromises: false,
22+
promiseResolve(){},
23+
}),
24+
{code: 'ERR_INVALID_ARG_VALUE'},
25+
`trackPromises: false and promiseResolve() are incompatible`);

‎tools/doc/type-parser.mjs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ const customTypesMap = {
6464

6565
'AsyncLocalStorage': 'async_context.html#class-asynclocalstorage',
6666

67-
'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks',
67+
'AsyncHook': 'async_hooks.html#async_hookscreatehookoptions',
6868
'AsyncResource': 'async_hooks.html#class-asyncresource',
6969

7070
'brotli options': 'zlib.html#class-brotlioptions',

0 commit comments

Comments
 (0)