From 35b8dac56619749b052982166ade9bdcb84d569e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 19 Nov 2025 02:52:25 -0400 Subject: [PATCH] child_process: add getMemoryUsage() method to ChildProcess Signed-off-by: root --- doc/api/child_process.md | 26 ++++ doc/api/errors.md | 17 +++ lib/internal/child_process.js | 135 ++++++++++++++++++ lib/internal/errors.js | 4 + src/env_properties.h | 1 + src/process_wrap.cc | 54 +++++++ .../child-process-get-memory-usage-child.js | 14 ++ .../test-child-process-get-memory-usage.js | 58 ++++++++ 8 files changed, 309 insertions(+) create mode 100644 test/fixtures/child-process-get-memory-usage-child.js create mode 100644 test/parallel/test-child-process-get-memory-usage.js diff --git a/doc/api/child_process.md b/doc/api/child_process.md index ba7cc2af6bfb14..35464135a59938 100644 --- a/doc/api/child_process.md +++ b/doc/api/child_process.md @@ -1624,6 +1624,32 @@ running if `.unref()` has been called before. #### `subprocess.channel.unref()` +#### `subprocess.getMemoryUsage()` + + + +* Returns: {Promise} fulfilled with an object containing: + * `rss` + * `heapTotal` + * `heapUsed` + * `external` + * `arrayBuffers` + +Retrieves memory statistics for the child via an IPC round-trip. The promise rejects with +`ERR_CHILD_PROCESS_NOT_RUNNING` if the child has already exited or with `ERR_IPC_CHANNEL_CLOSED` when no IPC +channel is available. The child must have been spawned with an IPC channel (e.g., `'ipc'` in `stdio` or +`child_process.fork()`). + +```mjs +import { fork } from 'node:child_process'; + +const child = fork('./worker.js'); +const usage = await child.getMemoryUsage(); +console.log(usage.heapUsed); +``` + diff --git a/doc/api/errors.md b/doc/api/errors.md index 2ff3e206252cd5..3c39d76e4f20eb 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -774,6 +774,23 @@ A child process was closed before the parent received a reply. Used when a child process is being forked without specifying an IPC channel. + + +### `ERR_CHILD_PROCESS_MEMORY_USAGE_FAILED` + +Thrown from `ChildProcess.prototype.getMemoryUsage()` when a memory usage +request fails inside the child. The error's `cause` contains the serialized +error information reported by the child process, when available. + + + +### `ERR_CHILD_PROCESS_NOT_RUNNING` + +Raised when an operation expects an active child process but the process has +already exited or has not been started yet. For example, +`ChildProcess.prototype.getMemoryUsage()` rejects with this error after the +child terminates. + ### `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index f110557a9374f7..b374688b5c54c1 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -8,7 +8,11 @@ const { FunctionPrototype, FunctionPrototypeCall, ObjectSetPrototypeOf, + Promise, + PromiseReject, ReflectApply, + SafeMap, + String, StringPrototypeSlice, Symbol, SymbolDispose, @@ -18,6 +22,8 @@ const { const { ErrnoException, codes: { + ERR_CHILD_PROCESS_MEMORY_USAGE_FAILED, + ERR_CHILD_PROCESS_NOT_RUNNING, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_HANDLE_TYPE, @@ -84,6 +90,8 @@ const MAX_HANDLE_RETRANSMISSIONS = 3; const kChannelHandle = Symbol('kChannelHandle'); const kIsUsedAsStdio = Symbol('kIsUsedAsStdio'); const kPendingMessages = Symbol('kPendingMessages'); +const kMemoryUsageRequests = Symbol('kMemoryUsageRequests'); +const kNextMemoryUsageRequestId = Symbol('kNextMemoryUsageRequestId'); // This object contain function to convert TCP objects to native handle objects // and back again. @@ -266,6 +274,21 @@ function ChildProcess() { this._handle = new Process(); this._handle[owner_symbol] = this; + this[kMemoryUsageRequests] = new SafeMap(); + this[kNextMemoryUsageRequestId] = 0; + + this.once('exit', () => { + rejectAllMemoryUsageRequests( + this, + new ERR_CHILD_PROCESS_NOT_RUNNING()); + }); + + this.once('disconnect', () => { + rejectAllMemoryUsageRequests( + this, + new ERR_IPC_CHANNEL_CLOSED()); + }); + this._handle.onexit = (exitCode, signalCode) => { if (signalCode) { this.signalCode = signalCode; @@ -488,6 +511,70 @@ function onSpawnNT(self) { self.emit('spawn'); } +function resolveMemoryUsageRequest(target, requestId, usage) { + const requests = target[kMemoryUsageRequests]; + if (!requests) return; + const pending = requests.get(requestId); + if (!pending) return; + requests.delete(requestId); + pending.resolve(usage); +} + +function rejectMemoryUsageRequest(target, requestId, error) { + const requests = target[kMemoryUsageRequests]; + if (!requests) return; + const pending = requests.get(requestId); + if (!pending) return; + requests.delete(requestId); + pending.reject(error); +} + +function rejectAllMemoryUsageRequests(target, error) { + const requests = target[kMemoryUsageRequests]; + if (!requests || requests.size === 0) return; + for (const pending of requests.values()) { + pending.reject(error); + } + requests.clear(); +} + +function respondWithMemoryUsage(target, requestId) { + try { + const usage = process.memoryUsage(); + target._send({ + cmd: 'NODE_MEMORY_USAGE_RESULT', + requestId, + usage, + }, null, true); + } catch (err) { + target._send({ + cmd: 'NODE_MEMORY_USAGE_ERROR', + requestId, + error: serializeMemoryUsageError(err), + }, null, true); + } +} + +function serializeMemoryUsageError(err) { + if (err == null || typeof err !== 'object') { + return { message: String(err) }; + } + return { + message: err.message, + code: err.code, + name: err.name, + }; +} + +function createMemoryUsageError(serialized) { + if (!serialized) { + return new ERR_CHILD_PROCESS_MEMORY_USAGE_FAILED(); + } + const error = new ERR_CHILD_PROCESS_MEMORY_USAGE_FAILED(serialized); + error.cause = serialized; + return error; +} + ChildProcess.prototype.kill = function kill(sig) { @@ -532,6 +619,34 @@ ChildProcess.prototype.unref = function unref() { if (this._handle) this._handle.unref(); }; +ChildProcess.prototype.getMemoryUsage = function getMemoryUsage() { + if (this._handle === null) { + return PromiseReject(new ERR_CHILD_PROCESS_NOT_RUNNING()); + } + + if (!this.channel || !this.connected) { + return PromiseReject(new ERR_IPC_CHANNEL_CLOSED()); + } + + const requestId = ++this[kNextMemoryUsageRequestId]; + return new Promise((resolve, reject) => { + const requests = this[kMemoryUsageRequests]; + requests.set(requestId, { resolve, reject }); + + this._send( + { cmd: 'NODE_MEMORY_USAGE', requestId }, + null, + true, + (err) => { + if (err === null || err === undefined) return; + const pending = requests.get(requestId); + if (!pending) return; + requests.delete(requestId); + pending.reject(err); + }); + }); +}; + class Control extends EventEmitter { #channel = null; #refs = 0; @@ -632,8 +747,28 @@ function setupChannel(target, channel, serializationMode) { // Object where socket lists will live channel.sockets = { got: {}, send: {} }; + const isProcessTarget = target === process; + // Handlers will go through this target.on('internalMessage', function(message, handle) { + if (message && message.cmd === 'NODE_MEMORY_USAGE') { + if (isProcessTarget) { + respondWithMemoryUsage(target, message.requestId); + } + return; + } + + if (message && message.cmd === 'NODE_MEMORY_USAGE_RESULT') { + resolveMemoryUsageRequest(target, message.requestId, message.usage); + return; + } + + if (message && message.cmd === 'NODE_MEMORY_USAGE_ERROR') { + const error = createMemoryUsageError(message.error); + rejectMemoryUsageRequest(target, message.requestId, error); + return; + } + // Once acknowledged - continue sending handles. if (message.cmd === 'NODE_HANDLE_ACK' || message.cmd === 'NODE_HANDLE_NACK') { diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 5fa4437b09e556..062df45aea360f 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1159,6 +1159,10 @@ E('ERR_CHILD_CLOSED_BEFORE_REPLY', E('ERR_CHILD_PROCESS_IPC_REQUIRED', "Forked processes must have an IPC channel, missing value 'ipc' in %s", Error); +E('ERR_CHILD_PROCESS_MEMORY_USAGE_FAILED', + 'Child process memory usage request failed', Error); +E('ERR_CHILD_PROCESS_NOT_RUNNING', + 'Child process is not running', Error); E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded', RangeError); E('ERR_CONSOLE_WRITABLE_STREAM', diff --git a/src/env_properties.h b/src/env_properties.h index 903158ebbdc2b7..4494fe3a70e1ff 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -394,6 +394,7 @@ V(contextify_global_template, v8::ObjectTemplate) \ V(contextify_wrapper_template, v8::ObjectTemplate) \ V(cpu_usage_template, v8::DictionaryTemplate) \ + V(memory_usage_template, v8::DictionaryTemplate) \ V(crypto_key_object_handle_constructor, v8::FunctionTemplate) \ V(env_proxy_template, v8::ObjectTemplate) \ V(env_proxy_ctor_template, v8::FunctionTemplate) \ diff --git a/src/process_wrap.cc b/src/process_wrap.cc index d27ca7da7b587b..dabab9730e8819 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -35,9 +35,11 @@ namespace node { using v8::Array; using v8::Context; +using v8::DictionaryTemplate; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; +using v8::HeapStatistics; using v8::Int32; using v8::Integer; using v8::Isolate; @@ -45,6 +47,7 @@ using v8::Just; using v8::JustVoid; using v8::Local; using v8::Maybe; +using v8::MaybeLocal; using v8::Nothing; using v8::Number; using v8::Object; @@ -71,12 +74,15 @@ class ProcessWrap : public HandleWrap { SetProtoMethod(isolate, constructor, "kill", Kill); SetConstructorFunction(context, target, "Process", constructor); + + SetMethod(context, target, "getMemoryUsage", GetMemoryUsage); } static void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(New); registry->Register(Spawn); registry->Register(Kill); + registry->Register(GetMemoryUsage); } SET_NO_MEMORY_INFO() @@ -395,6 +401,54 @@ class ProcessWrap : public HandleWrap { args.GetReturnValue().Set(err); } + static void GetMemoryUsage(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + HeapStatistics heap_stats; + isolate->GetHeapStatistics(&heap_stats); + + NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator(); + + size_t rss; + int err = uv_resident_set_memory(&rss); + if (err != 0) { + return env->ThrowUVException(err, "uv_resident_set_memory"); + } + + auto tmpl = env->memory_usage_template(); + if (tmpl.IsEmpty()) { + std::string_view property_names[] = { + "rss", + "heapTotal", + "heapUsed", + "external", + "arrayBuffers", + }; + tmpl = DictionaryTemplate::New(isolate, property_names); + env->set_memory_usage_template(tmpl); + } + + MaybeLocal values[] = { + Number::New(isolate, static_cast(rss)), + Number::New(isolate, static_cast(heap_stats.total_heap_size())), + Number::New(isolate, static_cast(heap_stats.used_heap_size())), + Number::New(isolate, static_cast(heap_stats.external_memory())), + Number::New(isolate, + allocator == nullptr + ? 0 + : static_cast(allocator->total_mem_usage())), + }; + + Local result; + if (!NewDictionaryInstanceNullProto(env->context(), tmpl, values) + .ToLocal(&result)) { + return; + } + + args.GetReturnValue().Set(result); + } + static void OnExit(uv_process_t* handle, int64_t exit_status, int term_signal) { diff --git a/test/fixtures/child-process-get-memory-usage-child.js b/test/fixtures/child-process-get-memory-usage-child.js new file mode 100644 index 00000000000000..8540f0cacf2372 --- /dev/null +++ b/test/fixtures/child-process-get-memory-usage-child.js @@ -0,0 +1,14 @@ +'use strict'; + +const keepAlive = setInterval(() => {}, 1000); + +process.on('message', (msg) => { + if (msg === 'exit') { + clearInterval(keepAlive); + process.exit(0); + } +}); + +if (typeof process.send === 'function') { + process.send('ready'); +} diff --git a/test/parallel/test-child-process-get-memory-usage.js b/test/parallel/test-child-process-get-memory-usage.js new file mode 100644 index 00000000000000..c2cf61d8daed04 --- /dev/null +++ b/test/parallel/test-child-process-get-memory-usage.js @@ -0,0 +1,58 @@ +'use strict'; + +const common = require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); +const { fork } = require('child_process'); +const { once } = require('events'); + +const childScript = fixtures.path('child-process-get-memory-usage-child.js'); + +async function spawnChild(stdio = ['ignore', 'ignore', 'ignore', 'ipc']) { + const child = fork(childScript, [], { stdio }); + await once(child, 'message'); // wait for "ready" + return child; +} + +async function testSuccessfulMemoryUsageRetrieval() { + const child = await spawnChild(); + const usage = await child.getMemoryUsage(); + + assert.strictEqual(typeof usage, 'object'); + for (const key of ['rss', 'heapTotal', 'heapUsed', 'external', 'arrayBuffers']) { + assert.strictEqual(typeof usage[key], 'number'); + } + + child.send('exit'); + await once(child, 'exit'); +} + +async function testRejectsWhenNotRunning() { + const child = await spawnChild(); + child.kill(); + await once(child, 'exit'); + + await assert.rejects(child.getMemoryUsage(), { + code: 'ERR_CHILD_PROCESS_NOT_RUNNING', + }); +} + +async function testRejectsWhenChannelClosed() { + const child = await spawnChild(); + child.disconnect(); + + await assert.rejects(child.getMemoryUsage(), { + code: 'ERR_IPC_CHANNEL_CLOSED', + }); + + child.kill(); + await once(child, 'exit'); +} + +async function main() { + await testSuccessfulMemoryUsageRetrieval(); + await testRejectsWhenNotRunning(); + await testRejectsWhenChannelClosed(); +} + +main().then(common.mustCall());