Skip to content

Commit

Permalink
process: add getActiveResourcesInfo()
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
RaisinTen committed Nov 24, 2021
1 parent 2b0087f commit 323306b
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 10 deletions.
69 changes: 69 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,71 @@ a code.
Specifying a code to [`process.exit(code)`][`process.exit()`] will override any
previous setting of `process.exitCode`.
## `process.getActiveResourcesInfo()`
<!-- YAML
added: REPLACEME
-->
> Stability: 1 - Experimental
* Returns: {Array} of objects containing the `type`, `asyncId` and the
`triggerAsyncId` of the currently active requests, handles and timers.
The `process.getActiveResourcesInfo()` method can be called at any moment and it
would return an array of objects containing some information about all the
resources that are currently keeping the event loop alive. This can be useful
while debugging.
The same functionality can be implemented using [`async_hooks`][] by setting up
an [`AsyncHook`][] that keeps a track of the calls made to [`init` callbacks][]
and [`destroy` callbacks][] but this function should be preferred because it
doesn't incur the overhead of creating any [`AsyncHook`][].
```mjs
import { getActiveResourcesInfo } from 'process';
import { setTimeout } from 'timers';

console.log('Before:', getActiveResourcesInfo());
setTimeout(() => {}, 1000);
console.log('After:', getActiveResourcesInfo());
// Prints:
// Before: [
// { type: 'CloseReq', asyncId: 6, triggerAsyncId: 0 },
// { type: 'TTYWrap', asyncId: 7, triggerAsyncId: 0 },
// { type: 'TTYWrap', asyncId: 9, triggerAsyncId: 0 },
// { type: 'TTYWrap', asyncId: 10, triggerAsyncId: 0 }
// ]
// After: [
// { type: 'CloseReq', asyncId: 6, triggerAsyncId: 0 },
// { type: 'TTYWrap', asyncId: 7, triggerAsyncId: 0 },
// { type: 'TTYWrap', asyncId: 9, triggerAsyncId: 0 },
// { type: 'TTYWrap', asyncId: 10, triggerAsyncId: 0 },
// { type: 'Timeout', asyncId: 12, triggerAsyncId: 0 }
// ]
```
```cjs
const { getActiveResourcesInfo } = require('process');
const { setTimeout } = require('timers');

console.log('Before:', getActiveResourcesInfo());
setTimeout(() => {}, 1000);
console.log('After:', getActiveResourcesInfo());
// Prints:
// Before: [
// { type: 'TTYWrap', asyncId: 2, triggerAsyncId: 1 },
// { type: 'TTYWrap', asyncId: 4, triggerAsyncId: 1 },
// { type: 'TTYWrap', asyncId: 5, triggerAsyncId: 1 }
// ]
// After: [
// { type: 'TTYWrap', asyncId: 2, triggerAsyncId: 1 },
// { type: 'TTYWrap', asyncId: 4, triggerAsyncId: 1 },
// { type: 'TTYWrap', asyncId: 5, triggerAsyncId: 1 },
// { type: 'Timeout', asyncId: 7, triggerAsyncId: 1 }
// ]
```
## `process.getegid()`
<!-- YAML
Expand Down Expand Up @@ -3792,6 +3857,7 @@ cases:
[`'message'`]: child_process.md#event-message
[`'uncaughtException'`]: #event-uncaughtexception
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
[`AsyncHook`]: async_hooks.md#class-asynchook
[`Buffer`]: buffer.md
[`ChildProcess.disconnect()`]: child_process.md#subprocessdisconnect
[`ChildProcess.send()`]: child_process.md#subprocesssendmessage-sendhandle-options-callback
Expand All @@ -3802,9 +3868,12 @@ cases:
[`Promise.race()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
[`Worker`]: worker_threads.md#class-worker
[`Worker` constructor]: worker_threads.md#new-workerfilename-options
[`async_hooks`]: async_hooks.md
[`console.error()`]: console.md#consoleerrordata-args
[`console.log()`]: console.md#consolelogdata-args
[`destroy` callbacks]: async_hooks.md#destroyasyncid
[`domain`]: domain.md
[`init` callbacks]: async_hooks.md#initasyncid-type-triggerasyncid-resource
[`net.Server`]: net.md#class-netserver
[`net.Socket`]: net.md#class-netsocket
[`os.constants.dlopen`]: os.md#dlopen-constants
Expand Down
46 changes: 44 additions & 2 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,24 @@
setupPrepareStackTrace();

const {
ArrayPrototypeConcat,
ArrayPrototypeForEach,
ArrayPrototypePush,
FunctionPrototypeCall,
JSONParse,
ObjectDefineProperty,
ObjectDefineProperties,
ObjectGetPrototypeOf,
ObjectPreventExtensions,
ObjectSetPrototypeOf,
ObjectValues,
ReflectGet,
ReflectSet,
SymbolToStringTag,
globalThis,
} = primordials;
const config = internalBinding('config');
const internalTimers = require('internal/timers');
const { deprecate, lazyDOMExceptionClass } = require('internal/util');

setupProcessObject();
Expand Down Expand Up @@ -150,6 +155,41 @@ const rawMethods = internalBinding('process_methods');
process._getActiveRequests = rawMethods._getActiveRequests;
process._getActiveHandles = rawMethods._getActiveHandles;

process.getActiveResourcesInfo = function() {
const resources = ArrayPrototypeConcat(rawMethods._getActiveRequestsInfo(),
rawMethods._getActiveHandlesInfo());

ArrayPrototypeForEach(ObjectValues(internalTimers.timerListMap), (list) => {
let timeout = list._idlePrev === list ? null : list._idlePrev;
while (timeout !== null) {
if (timeout.hasRef()) {
ArrayPrototypePush(resources, {
type: 'Timeout',
asyncId: timeout[internalTimers.async_id_symbol],
triggerAsyncId: timeout[internalTimers.trigger_async_id_symbol],
});
}
timeout = timeout._idlePrev === list ? null : list._idlePrev;
}
});

const queue = internalTimers.outstandingQueue.head !== null ?
internalTimers.outstandingQueue : internalTimers.immediateQueue;
let immediate = queue.head;
while (immediate !== null) {
if (immediate.hasRef()) {
ArrayPrototypePush(resources, {
type: 'Immediate',
asyncId: immediate[internalTimers.async_id_symbol],
triggerAsyncId: immediate[internalTimers.trigger_async_id_symbol],
});
}
immediate = immediate._idleNext;
}

return resources;
};

// TODO(joyeecheung): remove these
process.reallyExit = rawMethods.reallyExit;
process._kill = rawMethods._kill;
Expand Down Expand Up @@ -360,9 +400,11 @@ process.emitWarning = emitWarning;
// TODO(joyeecheung): either remove it or make it public
process._tickCallback = runNextTicks;

const { getTimerCallbacks } = require('internal/timers');
const { setupTimers } = internalBinding('timers');
const { processImmediate, processTimers } = getTimerCallbacks(runNextTicks);
const {
processImmediate,
processTimers,
} = internalTimers.getTimerCallbacks(runNextTicks);
// Sets two per-Environment callbacks that will be run from libuv:
// - processImmediate will be run in the callback of the per-Environment
// check handle.
Expand Down
11 changes: 6 additions & 5 deletions lib/internal/timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ const kRefed = Symbol('refed');
// Create a single linked list instance only once at startup
const immediateQueue = new ImmediateList();

// If an uncaught exception was thrown during execution of immediateQueue,
// this queue will store all remaining Immediates that need to run upon
// resolution of all error handling (if process is still alive).
const outstandingQueue = new ImmediateList();

let nextExpiry = Infinity;
let refCount = 0;

Expand Down Expand Up @@ -413,11 +418,6 @@ function setPosition(node, pos) {
}

function getTimerCallbacks(runNextTicks) {
// If an uncaught exception was thrown during execution of immediateQueue,
// this queue will store all remaining Immediates that need to run upon
// resolution of all error handling (if process is still alive).
const outstandingQueue = new ImmediateList();

function processImmediate() {
const queue = outstandingQueue.head !== null ?
outstandingQueue : immediateQueue;
Expand Down Expand Up @@ -649,6 +649,7 @@ module.exports = {
setUnrefTimeout,
getTimerDuration,
immediateQueue,
outstandingQueue,
getTimerCallbacks,
immediateInfoFields: {
kCount,
Expand Down
57 changes: 57 additions & 0 deletions src/node_process_methods.cc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "async_wrap-inl.h"
#include "base_object-inl.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
Expand Down Expand Up @@ -44,6 +45,7 @@ using v8::HeapStatistics;
using v8::Integer;
using v8::Isolate;
using v8::Local;
using v8::MaybeLocal;
using v8::NewStringType;
using v8::Number;
using v8::Object;
Expand Down Expand Up @@ -242,6 +244,26 @@ static void Uptime(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}

static MaybeLocal<Object> GetResourceInfo(Environment* env, AsyncWrap* w) {
Local<Object> resource_info = Object::New(env->isolate());

if (resource_info->Set(env->context(),
OneByteString(env->isolate(), "type"),
OneByteString(env->isolate(),
w->MemoryInfoName().c_str()))
.IsNothing() ||
resource_info->Set(env->context(),
OneByteString(env->isolate(), "asyncId"),
Number::New(env->isolate(), w->get_async_id()))
.IsNothing() ||
resource_info->Set(env->context(),
OneByteString(env->isolate(), "triggerAsyncId"),
Number::New(env->isolate(), w->get_trigger_async_id()))
.IsNothing()) return MaybeLocal<Object>();

return resource_info;
}

static void GetActiveRequests(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

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

static void GetActiveRequestsInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

std::vector<Local<Value>> requests;
for (ReqWrapBase* req_wrap : *env->req_wrap_queue()) {
AsyncWrap* w = req_wrap->GetAsyncWrap();
if (w->persistent().IsEmpty()) continue;
Local<Object> request;
if (!GetResourceInfo(env, w).ToLocal(&request)) return;
requests.emplace_back(request);
}

args.GetReturnValue().Set(
Array::New(env->isolate(), requests.data(), requests.size()));
}

// Non-static, friend of HandleWrap. Could have been a HandleWrap method but
// implemented here for consistency with GetActiveRequests().
void GetActiveHandles(const FunctionCallbackInfo<Value>& args) {
Expand All @@ -272,6 +310,21 @@ void GetActiveHandles(const FunctionCallbackInfo<Value>& args) {
Array::New(env->isolate(), handle_v.data(), handle_v.size()));
}

void GetActiveHandlesInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

std::vector<Local<Value>> handles;
for (HandleWrap* w : *env->handle_wrap_queue()) {
if (w->persistent().IsEmpty() || !HandleWrap::HasRef(w)) continue;
Local<Object> handle;
if (!GetResourceInfo(env, w).ToLocal(&handle)) return;
handles.emplace_back(handle);
}

args.GetReturnValue().Set(
Array::New(env->isolate(), handles.data(), handles.size()));
}

static void ResourceUsage(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

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

env->SetMethod(target, "_getActiveRequests", GetActiveRequests);
env->SetMethod(target, "_getActiveRequestsInfo", GetActiveRequestsInfo);
env->SetMethod(target, "_getActiveHandles", GetActiveHandles);
env->SetMethod(target, "_getActiveHandlesInfo", GetActiveHandlesInfo);
env->SetMethod(target, "_kill", Kill);

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

registry->Register(GetActiveRequests);
registry->Register(GetActiveRequestsInfo);
registry->Register(GetActiveHandles);
registry->Register(GetActiveHandlesInfo);
registry->Register(Kill);

registry->Register(Cwd);
Expand Down
26 changes: 26 additions & 0 deletions test/parallel/test-handle-wrap-isrefed.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,31 @@ const { kStateSymbol } = require('internal/dgram');
false, 'tcp_wrap: not unrefed on close')));
}

// timers
{
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Timeout').length, 0);
const timeout = setTimeout(() => {}, 500);
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Timeout').length, 1);
timeout.unref();
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Timeout').length, 0);
timeout.ref();
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Timeout').length, 1);

strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Immediate').length, 0);
const immediate = setImmediate(() => {});
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Immediate').length, 1);
immediate.unref();
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Immediate').length, 0);
immediate.ref();
strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'Immediate').length, 1);
}

// See also test/pseudo-tty/test-handle-wrap-isrefed-tty.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use strict';

require('../common');
const assert = require('assert');
const net = require('net');
const NUM = 8;
const connections = [];
const clients = [];
let clients_counter = 0;

const server = net.createServer(function listener(c) {
connections.push(c);
}).listen(0, makeConnection);


function makeConnection() {
if (clients_counter >= NUM) return;
net.connect(server.address().port, function connected() {
clientConnected(this);
makeConnection();
});
}


function clientConnected(client) {
clients.push(client);
if (++clients_counter >= NUM)
checkAll();
}


function checkAll() {
assert.strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'TCPSocketWrap').length,
clients.length + connections.length);

clients.forEach((item) => item.destroy());
connections.forEach((item) => item.end());

assert.strictEqual(process.getActiveResourcesInfo().filter(
({ type }) => type === 'TCPServerWrap').length, 1);

server.close();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const common = require('../common');
const assert = require('assert');
const fs = require('fs');

for (let i = 0; i < 12; i++)
fs.open(__filename, 'r', common.mustCall());

assert.strictEqual(process.getActiveResourcesInfo().length, 12);
Loading

0 comments on commit 323306b

Please sign in to comment.