Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions doc/api/child_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -1624,6 +1624,32 @@ running if `.unref()` has been called before.

#### `subprocess.channel.unref()`

#### `subprocess.getMemoryUsage()`

<!-- YAML
added: v25.2.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
added: v25.2.1
added: REPLACEME

-->

* 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);
```

<!-- YAML
added: v7.1.0
-->
Expand Down
17 changes: 17 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="ERR_CHILD_PROCESS_MEMORY_USAGE_FAILED"></a>

### `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.

<a id="ERR_CHILD_PROCESS_NOT_RUNNING"></a>

### `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.

<a id="ERR_CHILD_PROCESS_STDIO_MAXBUFFER"></a>

### `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`
Expand Down
135 changes: 135 additions & 0 deletions lib/internal/child_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ const {
FunctionPrototype,
FunctionPrototypeCall,
ObjectSetPrototypeOf,
Promise,
PromiseReject,
ReflectApply,
SafeMap,
String,
StringPrototypeSlice,
Symbol,
SymbolDispose,
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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') {
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
54 changes: 54 additions & 0 deletions src/process_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ 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;
using v8::Just;
using v8::JustVoid;
using v8::Local;
using v8::Maybe;
using v8::MaybeLocal;
using v8::Nothing;
using v8::Number;
using v8::Object;
Expand All @@ -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()
Expand Down Expand Up @@ -395,6 +401,54 @@ class ProcessWrap : public HandleWrap {
args.GetReturnValue().Set(err);
}

static void GetMemoryUsage(const FunctionCallbackInfo<Value>& 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<Value> values[] = {
Number::New(isolate, static_cast<double>(rss)),
Number::New(isolate, static_cast<double>(heap_stats.total_heap_size())),
Number::New(isolate, static_cast<double>(heap_stats.used_heap_size())),
Number::New(isolate, static_cast<double>(heap_stats.external_memory())),
Number::New(isolate,
allocator == nullptr
? 0
: static_cast<double>(allocator->total_mem_usage())),
};

Local<Object> 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) {
Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/child-process-get-memory-usage-child.js
Original file line number Diff line number Diff line change
@@ -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');
}
Loading