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