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
27 changes: 27 additions & 0 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,33 @@
or reject with an [`ERR_WORKER_NOT_RUNNING`][] error if the worker is no longer running.
This methods allows the statistics to be observed from outside the actual thread.

### `worker.getMemoryUsage()`

<!-- YAML
added:
changes:
- version: v25.2.0
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
- version: v25.2.0
- version: REPLACEME

pr-url: https://github.com/nodejs/node/pull/REPLACEME

Check warning on line 1837 in doc/api/worker_threads.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
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
pr-url: https://github.com/nodejs/node/pull/REPLACEME
pr-url: https://github.com/nodejs/node/pull/60778

description: Added `worker.getMemoryUsage()`.
-->

* Returns: {Promise}

Returns an object mirroring [`process.memoryUsage()`][] but scoped to the
worker's isolate:
* `rss` {integer} Resident Set Size. This value represents the RSS reported by
Copy link
Member

Choose a reason for hiding this comment

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

Isn't RSS a per process thing?
Therefore RSS value should be the same on all workers so quite pointless to read it from a different thread.

the worker thread and may still include memory shared across threads.
* `heapTotal` {integer} Total size of the V8 heap for the worker.
* `heapUsed` {integer} Heap space used by the worker.
* `external` {integer} Memory used by C++ objects bound to JavaScript objects in
the worker.
* `arrayBuffers` {integer} Memory allocated for `ArrayBuffer` and
`SharedArrayBuffer` instances within the worker.
The returned `Promise` rejects with [`ERR_WORKER_NOT_RUNNING`][] if called after
the worker has stopped.
Copy link
Member

Choose a reason for hiding this comment

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

This should explain the relationship to worker.getHeapStatistics() and how it differs from that

### `worker.performance`
<!-- YAML
Expand Down
14 changes: 14 additions & 0 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,20 @@ class Worker extends EventEmitter {
});
}

getMemoryUsage() {
const taker = this[kHandle]?.getMemoryUsage();

return new Promise((resolve, reject) => {
if (!taker) return reject(new ERR_WORKER_NOT_RUNNING());
taker.ondone = (err, usage) => {
if (err) {
return reject(err);
}
resolve(usage);
};
});
}

cpuUsage(prev) {
if (prev) {
validateObject(prev, 'prev');
Expand Down
1 change: 1 addition & 0 deletions src/async_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ namespace node {
V(WORKERHEAPPROFILE) \
V(WORKERHEAPSNAPSHOT) \
V(WORKERHEAPSTATISTICS) \
V(WORKERMEMORYUSAGE) \
V(WRITEWRAP) \
V(ZLIB)

Expand Down
2 changes: 2 additions & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@
V(free_list_statistics_template, v8::DictionaryTemplate) \
V(fsreqpromise_constructor_template, v8::ObjectTemplate) \
V(handle_wrap_ctor_template, v8::FunctionTemplate) \
V(memory_usage_template, v8::DictionaryTemplate) \
V(heap_statistics_template, v8::DictionaryTemplate) \
V(v8_heap_statistics_template, v8::DictionaryTemplate) \
V(histogram_ctor_template, v8::FunctionTemplate) \
Expand Down Expand Up @@ -449,6 +450,7 @@
V(write_wrap_template, v8::ObjectTemplate) \
V(worker_cpu_profile_taker_template, v8::ObjectTemplate) \
V(worker_cpu_usage_taker_template, v8::ObjectTemplate) \
V(worker_memory_usage_taker_template, v8::ObjectTemplate) \
V(worker_heap_profile_taker_template, v8::ObjectTemplate) \
V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
V(worker_heap_statistics_taker_template, v8::ObjectTemplate) \
Expand Down
115 changes: 115 additions & 0 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,105 @@ class WorkerThreadData {
friend class Worker;
};

class WorkerMemoryUsageTaker : public AsyncWrap {
public:
WorkerMemoryUsageTaker(Environment* env, Local<Object> obj)
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_WORKERMEMORYUSAGE) {}

SET_NO_MEMORY_INFO()
SET_MEMORY_INFO_NAME(WorkerMemoryUsageTaker)
SET_SELF_SIZE(WorkerMemoryUsageTaker)
};

void Worker::GetMemoryUsage(const FunctionCallbackInfo<Value>& args) {
Worker* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());

Environment* env = w->env();
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
Local<Object> wrap;
if (!env->worker_memory_usage_taker_template()
->NewInstance(env->context())
.ToLocal(&wrap)) {
return;
}

std::unique_ptr<BaseObjectPtr<WorkerMemoryUsageTaker>> taker =
std::make_unique<BaseObjectPtr<WorkerMemoryUsageTaker>>(
MakeDetachedBaseObject<WorkerMemoryUsageTaker>(env, wrap));

bool scheduled = w->RequestInterrupt([taker = std::move(taker),
env](Environment* worker_env) mutable {
auto stats = std::make_unique<uv_rusage_t>();
int rss_err = uv_getrusage_thread(stats.get());
HeapStatistics heap_stats;
worker_env->isolate()->GetHeapStatistics(&heap_stats);
NodeArrayBufferAllocator* allocator =
worker_env->isolate_data()->node_allocator();

env->SetImmediateThreadsafe(
[taker = std::move(taker),
rss_err,
stats = std::move(stats),
heap_stats,
allocator](Environment* env) mutable {
Isolate* isolate = env->isolate();
HandleScope handle_scope(isolate);
Context::Scope context_scope(env->context());
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(taker->get());

auto tmpl = env->memory_usage_template();
if (tmpl.IsEmpty()) {
static constexpr std::string_view names[] = {
"rss",
"heapTotal",
"heapUsed",
"external",
"arrayBuffers",
};
tmpl = DictionaryTemplate::New(isolate, names);
env->set_memory_usage_template(tmpl);
}

MaybeLocal<Value> values[] = {
Number::New(isolate, rss_err ? 0 : stats->ru_maxrss * 1024),
Number::New(isolate, heap_stats.total_heap_size()),
Number::New(isolate, heap_stats.used_heap_size()),
Number::New(isolate, heap_stats.external_memory()),
Number::New(isolate, allocator == nullptr
? 0
: allocator->total_mem_usage()),
Copy link
Member

Choose a reason for hiding this comment

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

There's no guarantee that allocator is still alive at this point

};

Local<Value> argv[] = {
Null(isolate),
Undefined(isolate),
};

if (rss_err) {
argv[0] = UVException(isolate,
rss_err,
"uv_getrusage_thread",
nullptr,
nullptr,
nullptr);
} else if (!NewDictionaryInstanceNullProto(
env->context(), tmpl, values)
.ToLocal(&argv[1])) {
return;
}

taker->get()->MakeCallback(
env->ondone_string(), arraysize(argv), argv);
},
CallbackFlags::kUnrefed);
});

if (scheduled) {
args.GetReturnValue().Set(wrap);
}
}

size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit,
size_t initial_heap_limit) {
Worker* worker = static_cast<Worker*>(data);
Expand Down Expand Up @@ -1489,6 +1588,7 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
SetProtoMethod(isolate, w, "loopIdleTime", Worker::LoopIdleTime);
SetProtoMethod(isolate, w, "loopStartTime", Worker::LoopStartTime);
SetProtoMethod(isolate, w, "getHeapStatistics", Worker::GetHeapStatistics);
SetProtoMethod(isolate, w, "getMemoryUsage", Worker::GetMemoryUsage);
SetProtoMethod(isolate, w, "cpuUsage", Worker::CpuUsage);
SetProtoMethod(isolate, w, "startCpuProfile", Worker::StartCpuProfile);
SetProtoMethod(isolate, w, "stopCpuProfile", Worker::StopCpuProfile);
Expand Down Expand Up @@ -1539,6 +1639,20 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
isolate_data->set_worker_cpu_usage_taker_template(wst->InstanceTemplate());
}

{
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);

wst->InstanceTemplate()->SetInternalFieldCount(
WorkerMemoryUsageTaker::kInternalFieldCount);
wst->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));

Local<String> wst_string =
FIXED_ONE_BYTE_STRING(isolate, "WorkerMemoryUsageTaker");
wst->SetClassName(wst_string);
isolate_data->set_worker_memory_usage_taker_template(
wst->InstanceTemplate());
}

{
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);

Expand Down Expand Up @@ -1643,6 +1757,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(Worker::LoopIdleTime);
registry->Register(Worker::LoopStartTime);
registry->Register(Worker::GetHeapStatistics);
registry->Register(Worker::GetMemoryUsage);
registry->Register(Worker::CpuUsage);
registry->Register(Worker::StartCpuProfile);
registry->Register(Worker::StopCpuProfile);
Expand Down
1 change: 1 addition & 0 deletions src/node_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class Worker : public AsyncWrap {
static void StopCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StartHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
static void StopHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
static void GetMemoryUsage(const v8::FunctionCallbackInfo<v8::Value>& args);

private:
bool CreateEnvMessagePort(Environment* env);
Expand Down
45 changes: 45 additions & 0 deletions test/parallel/test-worker-get-memory-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

Check failure on line 1 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'

Check failure on line 2 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
const common = require('../common');

Check failure on line 3 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
const assert = require('assert');

Check failure on line 4 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
const { Worker, isMainThread } = require('worker_threads');

Check failure on line 5 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'

Check failure on line 6 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
if (!isMainThread) {

Check failure on line 7 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
common.skip('This test only works on the main thread');

Check failure on line 8 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
}

Check failure on line 9 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'

Check failure on line 10 in test/parallel/test-worker-get-memory-usage.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Expected linebreaks to be 'LF' but found 'CRLF'
const worker = new Worker(`
const { parentPort } = require('worker_threads');
parentPort.once('message', () => process.exit(0));
parentPort.postMessage('ready');
`, { eval: true });

worker.once('message', common.mustCall(async () => {
const usage = await worker.getMemoryUsage();
const keys = [
'rss',
'heapTotal',
'heapUsed',
'external',
'arrayBuffers',
].sort();

assert.deepStrictEqual(Object.keys(usage).sort(), keys);

for (const key of keys) {
assert.strictEqual(typeof usage[key], 'number', `Expected ${key} to be a number`);
assert.ok(usage[key] >= 0, `Expected ${key} to be >= 0`);
}

assert.ok(usage.heapUsed <= usage.heapTotal,
'heapUsed should not exceed heapTotal');

worker.postMessage('exit');
}));

worker.once('exit', common.mustCall(async (code) => {
assert.strictEqual(code, 0);
await assert.rejects(worker.getMemoryUsage(), {
code: 'ERR_WORKER_NOT_RUNNING'
});
}));
Loading