Skip to content

Commit

Permalink
process: add getActiveResources()
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 21, 2021
1 parent 2b0087f commit 4657537
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 7 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.getActiveResources()`
<!-- 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.getActiveResources()` 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 { getActiveResources } from 'process';
import { setTimeout } from 'timers';

console.log('Before:', getActiveResources());
setTimeout(() => {}, 1000);
console.log('After:', getActiveResources());
// 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 { getActiveResources } = require('process');
const { setTimeout } = require('timers');

console.log('Before:', getActiveResources());
setTimeout(() => {}, 1000);
console.log('After:', getActiveResources());
// 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
42 changes: 40 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,37 @@ const rawMethods = internalBinding('process_methods');
process._getActiveRequests = rawMethods._getActiveRequests;
process._getActiveHandles = rawMethods._getActiveHandles;

process.getActiveResources = function() {
const resources = ArrayPrototypeConcat(rawMethods._getActiveRequests2(),
rawMethods._getActiveHandles2());

ArrayPrototypeForEach(ObjectValues(internalTimers.timerListMap), (list) => {
let timeout = list._idlePrev === list ? null : list._idlePrev;
while (timeout !== null) {
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) {
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 +396,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
65 changes: 65 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 @@ -250,13 +251,44 @@ static void GetActiveRequests(const FunctionCallbackInfo<Value>& args) {
AsyncWrap* w = req_wrap->GetAsyncWrap();
if (w->persistent().IsEmpty())
continue;

request_v.emplace_back(w->GetOwner());
}

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

static void GetActiveRequests2(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 = Object::New(env->isolate());
if (request->Set(env->context(),
OneByteString(env->isolate(), "type"),
OneByteString(env->isolate(), w->MemoryInfoName().c_str()))
.IsNothing() ||
request->Set(env->context(),
OneByteString(env->isolate(), "asyncId"),
Number::New(env->isolate(), w->get_async_id()))
.IsNothing() ||
request->Set(env->context(),
OneByteString(env->isolate(), "triggerAsyncId"),
Number::New(env->isolate(), w->get_trigger_async_id()))
.IsNothing()) 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 +304,35 @@ void GetActiveHandles(const FunctionCallbackInfo<Value>& args) {
Array::New(env->isolate(), handle_v.data(), handle_v.size()));
}

void GetActiveHandles2(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 = Object::New(env->isolate());
if (handle->Set(env->context(),
OneByteString(env->isolate(), "type"),
OneByteString(env->isolate(), w->MemoryInfoName().c_str()))
.IsNothing() ||
handle->Set(env->context(),
OneByteString(env->isolate(), "asyncId"),
Number::New(env->isolate(), w->get_async_id()))
.IsNothing() ||
handle->Set(env->context(),
OneByteString(env->isolate(), "triggerAsyncId"),
Number::New(env->isolate(), w->get_trigger_async_id()))
.IsNothing()) 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 +608,9 @@ static void Initialize(Local<Object> target,
env->SetMethod(target, "resourceUsage", ResourceUsage);

env->SetMethod(target, "_getActiveRequests", GetActiveRequests);
env->SetMethod(target, "_getActiveRequests2", GetActiveRequests2);
env->SetMethod(target, "_getActiveHandles", GetActiveHandles);
env->SetMethod(target, "_getActiveHandles2", GetActiveHandles2);
env->SetMethod(target, "_kill", Kill);

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

registry->Register(GetActiveRequests);
registry->Register(GetActiveRequests2);
registry->Register(GetActiveHandles);
registry->Register(GetActiveHandles2);
registry->Register(Kill);

registry->Register(Cwd);
Expand Down

0 comments on commit 4657537

Please sign in to comment.