Skip to content

Commit 323306b

Browse files
committed
process: add getActiveResourcesInfo()
This is supposed to be a public alternative of the private APIs, `process._getActiveResources()` and `process._getActiveHandles()`. When called, it returns an array of objects containing the `type`, `asyncId` and the `triggerAsyncId` of the currently active `requests`, `handles` and `timers`. Signed-off-by: Darshan Sen <[email protected]>
1 parent 2b0087f commit 323306b

9 files changed

+278
-10
lines changed

doc/api/process.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1817,6 +1817,71 @@ a code.
18171817
Specifying a code to [`process.exit(code)`][`process.exit()`] will override any
18181818
previous setting of `process.exitCode`.
18191819
1820+
## `process.getActiveResourcesInfo()`
1821+
1822+
<!-- YAML
1823+
added: REPLACEME
1824+
-->
1825+
1826+
> Stability: 1 - Experimental
1827+
1828+
* Returns: {Array} of objects containing the `type`, `asyncId` and the
1829+
`triggerAsyncId` of the currently active requests, handles and timers.
1830+
1831+
The `process.getActiveResourcesInfo()` method can be called at any moment and it
1832+
would return an array of objects containing some information about all the
1833+
resources that are currently keeping the event loop alive. This can be useful
1834+
while debugging.
1835+
1836+
The same functionality can be implemented using [`async_hooks`][] by setting up
1837+
an [`AsyncHook`][] that keeps a track of the calls made to [`init` callbacks][]
1838+
and [`destroy` callbacks][] but this function should be preferred because it
1839+
doesn't incur the overhead of creating any [`AsyncHook`][].
1840+
1841+
```mjs
1842+
import { getActiveResourcesInfo } from 'process';
1843+
import { setTimeout } from 'timers';
1844+
1845+
console.log('Before:', getActiveResourcesInfo());
1846+
setTimeout(() => {}, 1000);
1847+
console.log('After:', getActiveResourcesInfo());
1848+
// Prints:
1849+
// Before: [
1850+
// { type: 'CloseReq', asyncId: 6, triggerAsyncId: 0 },
1851+
// { type: 'TTYWrap', asyncId: 7, triggerAsyncId: 0 },
1852+
// { type: 'TTYWrap', asyncId: 9, triggerAsyncId: 0 },
1853+
// { type: 'TTYWrap', asyncId: 10, triggerAsyncId: 0 }
1854+
// ]
1855+
// After: [
1856+
// { type: 'CloseReq', asyncId: 6, triggerAsyncId: 0 },
1857+
// { type: 'TTYWrap', asyncId: 7, triggerAsyncId: 0 },
1858+
// { type: 'TTYWrap', asyncId: 9, triggerAsyncId: 0 },
1859+
// { type: 'TTYWrap', asyncId: 10, triggerAsyncId: 0 },
1860+
// { type: 'Timeout', asyncId: 12, triggerAsyncId: 0 }
1861+
// ]
1862+
```
1863+
1864+
```cjs
1865+
const { getActiveResourcesInfo } = require('process');
1866+
const { setTimeout } = require('timers');
1867+
1868+
console.log('Before:', getActiveResourcesInfo());
1869+
setTimeout(() => {}, 1000);
1870+
console.log('After:', getActiveResourcesInfo());
1871+
// Prints:
1872+
// Before: [
1873+
// { type: 'TTYWrap', asyncId: 2, triggerAsyncId: 1 },
1874+
// { type: 'TTYWrap', asyncId: 4, triggerAsyncId: 1 },
1875+
// { type: 'TTYWrap', asyncId: 5, triggerAsyncId: 1 }
1876+
// ]
1877+
// After: [
1878+
// { type: 'TTYWrap', asyncId: 2, triggerAsyncId: 1 },
1879+
// { type: 'TTYWrap', asyncId: 4, triggerAsyncId: 1 },
1880+
// { type: 'TTYWrap', asyncId: 5, triggerAsyncId: 1 },
1881+
// { type: 'Timeout', asyncId: 7, triggerAsyncId: 1 }
1882+
// ]
1883+
```
1884+
18201885
## `process.getegid()`
18211886
18221887
<!-- YAML
@@ -3792,6 +3857,7 @@ cases:
37923857
[`'message'`]: child_process.md#event-message
37933858
[`'uncaughtException'`]: #event-uncaughtexception
37943859
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
3860+
[`AsyncHook`]: async_hooks.md#class-asynchook
37953861
[`Buffer`]: buffer.md
37963862
[`ChildProcess.disconnect()`]: child_process.md#subprocessdisconnect
37973863
[`ChildProcess.send()`]: child_process.md#subprocesssendmessage-sendhandle-options-callback
@@ -3802,9 +3868,12 @@ cases:
38023868
[`Promise.race()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
38033869
[`Worker`]: worker_threads.md#class-worker
38043870
[`Worker` constructor]: worker_threads.md#new-workerfilename-options
3871+
[`async_hooks`]: async_hooks.md
38053872
[`console.error()`]: console.md#consoleerrordata-args
38063873
[`console.log()`]: console.md#consolelogdata-args
3874+
[`destroy` callbacks]: async_hooks.md#destroyasyncid
38073875
[`domain`]: domain.md
3876+
[`init` callbacks]: async_hooks.md#initasyncid-type-triggerasyncid-resource
38083877
[`net.Server`]: net.md#class-netserver
38093878
[`net.Socket`]: net.md#class-netsocket
38103879
[`os.constants.dlopen`]: os.md#dlopen-constants

lib/internal/bootstrap/node.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,24 @@
3939
setupPrepareStackTrace();
4040

4141
const {
42+
ArrayPrototypeConcat,
43+
ArrayPrototypeForEach,
44+
ArrayPrototypePush,
4245
FunctionPrototypeCall,
4346
JSONParse,
4447
ObjectDefineProperty,
4548
ObjectDefineProperties,
4649
ObjectGetPrototypeOf,
4750
ObjectPreventExtensions,
4851
ObjectSetPrototypeOf,
52+
ObjectValues,
4953
ReflectGet,
5054
ReflectSet,
5155
SymbolToStringTag,
5256
globalThis,
5357
} = primordials;
5458
const config = internalBinding('config');
59+
const internalTimers = require('internal/timers');
5560
const { deprecate, lazyDOMExceptionClass } = require('internal/util');
5661

5762
setupProcessObject();
@@ -150,6 +155,41 @@ const rawMethods = internalBinding('process_methods');
150155
process._getActiveRequests = rawMethods._getActiveRequests;
151156
process._getActiveHandles = rawMethods._getActiveHandles;
152157

158+
process.getActiveResourcesInfo = function() {
159+
const resources = ArrayPrototypeConcat(rawMethods._getActiveRequestsInfo(),
160+
rawMethods._getActiveHandlesInfo());
161+
162+
ArrayPrototypeForEach(ObjectValues(internalTimers.timerListMap), (list) => {
163+
let timeout = list._idlePrev === list ? null : list._idlePrev;
164+
while (timeout !== null) {
165+
if (timeout.hasRef()) {
166+
ArrayPrototypePush(resources, {
167+
type: 'Timeout',
168+
asyncId: timeout[internalTimers.async_id_symbol],
169+
triggerAsyncId: timeout[internalTimers.trigger_async_id_symbol],
170+
});
171+
}
172+
timeout = timeout._idlePrev === list ? null : list._idlePrev;
173+
}
174+
});
175+
176+
const queue = internalTimers.outstandingQueue.head !== null ?
177+
internalTimers.outstandingQueue : internalTimers.immediateQueue;
178+
let immediate = queue.head;
179+
while (immediate !== null) {
180+
if (immediate.hasRef()) {
181+
ArrayPrototypePush(resources, {
182+
type: 'Immediate',
183+
asyncId: immediate[internalTimers.async_id_symbol],
184+
triggerAsyncId: immediate[internalTimers.trigger_async_id_symbol],
185+
});
186+
}
187+
immediate = immediate._idleNext;
188+
}
189+
190+
return resources;
191+
};
192+
153193
// TODO(joyeecheung): remove these
154194
process.reallyExit = rawMethods.reallyExit;
155195
process._kill = rawMethods._kill;
@@ -360,9 +400,11 @@ process.emitWarning = emitWarning;
360400
// TODO(joyeecheung): either remove it or make it public
361401
process._tickCallback = runNextTicks;
362402

363-
const { getTimerCallbacks } = require('internal/timers');
364403
const { setupTimers } = internalBinding('timers');
365-
const { processImmediate, processTimers } = getTimerCallbacks(runNextTicks);
404+
const {
405+
processImmediate,
406+
processTimers,
407+
} = internalTimers.getTimerCallbacks(runNextTicks);
366408
// Sets two per-Environment callbacks that will be run from libuv:
367409
// - processImmediate will be run in the callback of the per-Environment
368410
// check handle.

lib/internal/timers.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ const kRefed = Symbol('refed');
139139
// Create a single linked list instance only once at startup
140140
const immediateQueue = new ImmediateList();
141141

142+
// If an uncaught exception was thrown during execution of immediateQueue,
143+
// this queue will store all remaining Immediates that need to run upon
144+
// resolution of all error handling (if process is still alive).
145+
const outstandingQueue = new ImmediateList();
146+
142147
let nextExpiry = Infinity;
143148
let refCount = 0;
144149

@@ -413,11 +418,6 @@ function setPosition(node, pos) {
413418
}
414419

415420
function getTimerCallbacks(runNextTicks) {
416-
// If an uncaught exception was thrown during execution of immediateQueue,
417-
// this queue will store all remaining Immediates that need to run upon
418-
// resolution of all error handling (if process is still alive).
419-
const outstandingQueue = new ImmediateList();
420-
421421
function processImmediate() {
422422
const queue = outstandingQueue.head !== null ?
423423
outstandingQueue : immediateQueue;
@@ -649,6 +649,7 @@ module.exports = {
649649
setUnrefTimeout,
650650
getTimerDuration,
651651
immediateQueue,
652+
outstandingQueue,
652653
getTimerCallbacks,
653654
immediateInfoFields: {
654655
kCount,

src/node_process_methods.cc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "async_wrap-inl.h"
12
#include "base_object-inl.h"
23
#include "debug_utils-inl.h"
34
#include "env-inl.h"
@@ -44,6 +45,7 @@ using v8::HeapStatistics;
4445
using v8::Integer;
4546
using v8::Isolate;
4647
using v8::Local;
48+
using v8::MaybeLocal;
4749
using v8::NewStringType;
4850
using v8::Number;
4951
using v8::Object;
@@ -242,6 +244,26 @@ static void Uptime(const FunctionCallbackInfo<Value>& args) {
242244
args.GetReturnValue().Set(result);
243245
}
244246

247+
static MaybeLocal<Object> GetResourceInfo(Environment* env, AsyncWrap* w) {
248+
Local<Object> resource_info = Object::New(env->isolate());
249+
250+
if (resource_info->Set(env->context(),
251+
OneByteString(env->isolate(), "type"),
252+
OneByteString(env->isolate(),
253+
w->MemoryInfoName().c_str()))
254+
.IsNothing() ||
255+
resource_info->Set(env->context(),
256+
OneByteString(env->isolate(), "asyncId"),
257+
Number::New(env->isolate(), w->get_async_id()))
258+
.IsNothing() ||
259+
resource_info->Set(env->context(),
260+
OneByteString(env->isolate(), "triggerAsyncId"),
261+
Number::New(env->isolate(), w->get_trigger_async_id()))
262+
.IsNothing()) return MaybeLocal<Object>();
263+
264+
return resource_info;
265+
}
266+
245267
static void GetActiveRequests(const FunctionCallbackInfo<Value>& args) {
246268
Environment* env = Environment::GetCurrent(args);
247269

@@ -257,6 +279,22 @@ static void GetActiveRequests(const FunctionCallbackInfo<Value>& args) {
257279
Array::New(env->isolate(), request_v.data(), request_v.size()));
258280
}
259281

282+
static void GetActiveRequestsInfo(const FunctionCallbackInfo<Value>& args) {
283+
Environment* env = Environment::GetCurrent(args);
284+
285+
std::vector<Local<Value>> requests;
286+
for (ReqWrapBase* req_wrap : *env->req_wrap_queue()) {
287+
AsyncWrap* w = req_wrap->GetAsyncWrap();
288+
if (w->persistent().IsEmpty()) continue;
289+
Local<Object> request;
290+
if (!GetResourceInfo(env, w).ToLocal(&request)) return;
291+
requests.emplace_back(request);
292+
}
293+
294+
args.GetReturnValue().Set(
295+
Array::New(env->isolate(), requests.data(), requests.size()));
296+
}
297+
260298
// Non-static, friend of HandleWrap. Could have been a HandleWrap method but
261299
// implemented here for consistency with GetActiveRequests().
262300
void GetActiveHandles(const FunctionCallbackInfo<Value>& args) {
@@ -272,6 +310,21 @@ void GetActiveHandles(const FunctionCallbackInfo<Value>& args) {
272310
Array::New(env->isolate(), handle_v.data(), handle_v.size()));
273311
}
274312

313+
void GetActiveHandlesInfo(const FunctionCallbackInfo<Value>& args) {
314+
Environment* env = Environment::GetCurrent(args);
315+
316+
std::vector<Local<Value>> handles;
317+
for (HandleWrap* w : *env->handle_wrap_queue()) {
318+
if (w->persistent().IsEmpty() || !HandleWrap::HasRef(w)) continue;
319+
Local<Object> handle;
320+
if (!GetResourceInfo(env, w).ToLocal(&handle)) return;
321+
handles.emplace_back(handle);
322+
}
323+
324+
args.GetReturnValue().Set(
325+
Array::New(env->isolate(), handles.data(), handles.size()));
326+
}
327+
275328
static void ResourceUsage(const FunctionCallbackInfo<Value>& args) {
276329
Environment* env = Environment::GetCurrent(args);
277330

@@ -547,7 +600,9 @@ static void Initialize(Local<Object> target,
547600
env->SetMethod(target, "resourceUsage", ResourceUsage);
548601

549602
env->SetMethod(target, "_getActiveRequests", GetActiveRequests);
603+
env->SetMethod(target, "_getActiveRequestsInfo", GetActiveRequestsInfo);
550604
env->SetMethod(target, "_getActiveHandles", GetActiveHandles);
605+
env->SetMethod(target, "_getActiveHandlesInfo", GetActiveHandlesInfo);
551606
env->SetMethod(target, "_kill", Kill);
552607

553608
env->SetMethodNoSideEffect(target, "cwd", Cwd);
@@ -574,7 +629,9 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
574629
registry->Register(ResourceUsage);
575630

576631
registry->Register(GetActiveRequests);
632+
registry->Register(GetActiveRequestsInfo);
577633
registry->Register(GetActiveHandles);
634+
registry->Register(GetActiveHandlesInfo);
578635
registry->Register(Kill);
579636

580637
registry->Register(Cwd);

test/parallel/test-handle-wrap-isrefed.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,31 @@ const { kStateSymbol } = require('internal/dgram');
106106
false, 'tcp_wrap: not unrefed on close')));
107107
}
108108

109+
// timers
110+
{
111+
strictEqual(process.getActiveResourcesInfo().filter(
112+
({ type }) => type === 'Timeout').length, 0);
113+
const timeout = setTimeout(() => {}, 500);
114+
strictEqual(process.getActiveResourcesInfo().filter(
115+
({ type }) => type === 'Timeout').length, 1);
116+
timeout.unref();
117+
strictEqual(process.getActiveResourcesInfo().filter(
118+
({ type }) => type === 'Timeout').length, 0);
119+
timeout.ref();
120+
strictEqual(process.getActiveResourcesInfo().filter(
121+
({ type }) => type === 'Timeout').length, 1);
122+
123+
strictEqual(process.getActiveResourcesInfo().filter(
124+
({ type }) => type === 'Immediate').length, 0);
125+
const immediate = setImmediate(() => {});
126+
strictEqual(process.getActiveResourcesInfo().filter(
127+
({ type }) => type === 'Immediate').length, 1);
128+
immediate.unref();
129+
strictEqual(process.getActiveResourcesInfo().filter(
130+
({ type }) => type === 'Immediate').length, 0);
131+
immediate.ref();
132+
strictEqual(process.getActiveResourcesInfo().filter(
133+
({ type }) => type === 'Immediate').length, 1);
134+
}
109135

110136
// See also test/pseudo-tty/test-handle-wrap-isrefed-tty.js
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const net = require('net');
6+
const NUM = 8;
7+
const connections = [];
8+
const clients = [];
9+
let clients_counter = 0;
10+
11+
const server = net.createServer(function listener(c) {
12+
connections.push(c);
13+
}).listen(0, makeConnection);
14+
15+
16+
function makeConnection() {
17+
if (clients_counter >= NUM) return;
18+
net.connect(server.address().port, function connected() {
19+
clientConnected(this);
20+
makeConnection();
21+
});
22+
}
23+
24+
25+
function clientConnected(client) {
26+
clients.push(client);
27+
if (++clients_counter >= NUM)
28+
checkAll();
29+
}
30+
31+
32+
function checkAll() {
33+
assert.strictEqual(process.getActiveResourcesInfo().filter(
34+
({ type }) => type === 'TCPSocketWrap').length,
35+
clients.length + connections.length);
36+
37+
clients.forEach((item) => item.destroy());
38+
connections.forEach((item) => item.end());
39+
40+
assert.strictEqual(process.getActiveResourcesInfo().filter(
41+
({ type }) => type === 'TCPServerWrap').length, 1);
42+
43+
server.close();
44+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const fs = require('fs');
6+
7+
for (let i = 0; i < 12; i++)
8+
fs.open(__filename, 'r', common.mustCall());
9+
10+
assert.strictEqual(process.getActiveResourcesInfo().length, 12);

0 commit comments

Comments
 (0)