Skip to content

Commit 56ece73

Browse files
rootroot
authored andcommitted
worker: add worker.getMemoryUsage() API
1 parent 17fba60 commit 56ece73

File tree

7 files changed

+205
-0
lines changed

7 files changed

+205
-0
lines changed

doc/api/worker_threads.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1828,6 +1828,33 @@ This method returns a `Promise` that will resolve to an object identical to [`v8
18281828
or reject with an [`ERR_WORKER_NOT_RUNNING`][] error if the worker is no longer running.
18291829
This methods allows the statistics to be observed from outside the actual thread.
18301830

1831+
### `worker.getMemoryUsage()`
1832+
1833+
<!-- YAML
1834+
added:
1835+
changes:
1836+
- version: v25.2.0
1837+
pr-url: https://github.com/nodejs/node/pull/REPLACEME
1838+
description: Added `worker.getMemoryUsage()`.
1839+
-->
1840+
1841+
* Returns: {Promise}
1842+
1843+
Returns an object mirroring [`process.memoryUsage()`][] but scoped to the
1844+
worker's isolate:
1845+
1846+
* `rss` {integer} Resident Set Size. This value represents the RSS reported by
1847+
the worker thread and may still include memory shared across threads.
1848+
* `heapTotal` {integer} Total size of the V8 heap for the worker.
1849+
* `heapUsed` {integer} Heap space used by the worker.
1850+
* `external` {integer} Memory used by C++ objects bound to JavaScript objects in
1851+
the worker.
1852+
* `arrayBuffers` {integer} Memory allocated for `ArrayBuffer` and
1853+
`SharedArrayBuffer` instances within the worker.
1854+
1855+
The returned `Promise` rejects with [`ERR_WORKER_NOT_RUNNING`][] if called after
1856+
the worker has stopped.
1857+
18311858
### `worker.performance`
18321859
18331860
<!-- YAML

lib/internal/worker.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,20 @@ class Worker extends EventEmitter {
537537
});
538538
}
539539

540+
getMemoryUsage() {
541+
const taker = this[kHandle]?.getMemoryUsage();
542+
543+
return new Promise((resolve, reject) => {
544+
if (!taker) return reject(new ERR_WORKER_NOT_RUNNING());
545+
taker.ondone = (err, usage) => {
546+
if (err) {
547+
return reject(err);
548+
}
549+
resolve(usage);
550+
};
551+
});
552+
}
553+
540554
cpuUsage(prev) {
541555
if (prev) {
542556
validateObject(prev, 'prev');

src/async_wrap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ namespace node {
8484
V(WORKERHEAPPROFILE) \
8585
V(WORKERHEAPSNAPSHOT) \
8686
V(WORKERHEAPSTATISTICS) \
87+
V(WORKERMEMORYUSAGE) \
8788
V(WRITEWRAP) \
8889
V(ZLIB)
8990

src/env_properties.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@
406406
V(free_list_statistics_template, v8::DictionaryTemplate) \
407407
V(fsreqpromise_constructor_template, v8::ObjectTemplate) \
408408
V(handle_wrap_ctor_template, v8::FunctionTemplate) \
409+
V(memory_usage_template, v8::DictionaryTemplate) \
409410
V(heap_statistics_template, v8::DictionaryTemplate) \
410411
V(v8_heap_statistics_template, v8::DictionaryTemplate) \
411412
V(histogram_ctor_template, v8::FunctionTemplate) \
@@ -449,6 +450,7 @@
449450
V(write_wrap_template, v8::ObjectTemplate) \
450451
V(worker_cpu_profile_taker_template, v8::ObjectTemplate) \
451452
V(worker_cpu_usage_taker_template, v8::ObjectTemplate) \
453+
V(worker_memory_usage_taker_template, v8::ObjectTemplate) \
452454
V(worker_heap_profile_taker_template, v8::ObjectTemplate) \
453455
V(worker_heap_snapshot_taker_template, v8::ObjectTemplate) \
454456
V(worker_heap_statistics_taker_template, v8::ObjectTemplate) \

src/node_worker.cc

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,105 @@ class WorkerThreadData {
271271
friend class Worker;
272272
};
273273

274+
class WorkerMemoryUsageTaker : public AsyncWrap {
275+
public:
276+
WorkerMemoryUsageTaker(Environment* env, Local<Object> obj)
277+
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_WORKERMEMORYUSAGE) {}
278+
279+
SET_NO_MEMORY_INFO()
280+
SET_MEMORY_INFO_NAME(WorkerMemoryUsageTaker)
281+
SET_SELF_SIZE(WorkerMemoryUsageTaker)
282+
};
283+
284+
void Worker::GetMemoryUsage(const FunctionCallbackInfo<Value>& args) {
285+
Worker* w;
286+
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
287+
288+
Environment* env = w->env();
289+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(w);
290+
Local<Object> wrap;
291+
if (!env->worker_memory_usage_taker_template()
292+
->NewInstance(env->context())
293+
.ToLocal(&wrap)) {
294+
return;
295+
}
296+
297+
std::unique_ptr<BaseObjectPtr<WorkerMemoryUsageTaker>> taker =
298+
std::make_unique<BaseObjectPtr<WorkerMemoryUsageTaker>>(
299+
MakeDetachedBaseObject<WorkerMemoryUsageTaker>(env, wrap));
300+
301+
bool scheduled = w->RequestInterrupt([taker = std::move(taker),
302+
env](Environment* worker_env) mutable {
303+
auto stats = std::make_unique<uv_rusage_t>();
304+
int rss_err = uv_getrusage_thread(stats.get());
305+
HeapStatistics heap_stats;
306+
worker_env->isolate()->GetHeapStatistics(&heap_stats);
307+
NodeArrayBufferAllocator* allocator =
308+
worker_env->isolate_data()->node_allocator();
309+
310+
env->SetImmediateThreadsafe(
311+
[taker = std::move(taker),
312+
rss_err,
313+
stats = std::move(stats),
314+
heap_stats,
315+
allocator](Environment* env) mutable {
316+
Isolate* isolate = env->isolate();
317+
HandleScope handle_scope(isolate);
318+
Context::Scope context_scope(env->context());
319+
AsyncHooks::DefaultTriggerAsyncIdScope trigger_id_scope(taker->get());
320+
321+
auto tmpl = env->memory_usage_template();
322+
if (tmpl.IsEmpty()) {
323+
static constexpr std::string_view names[] = {
324+
"rss",
325+
"heapTotal",
326+
"heapUsed",
327+
"external",
328+
"arrayBuffers",
329+
};
330+
tmpl = DictionaryTemplate::New(isolate, names);
331+
env->set_memory_usage_template(tmpl);
332+
}
333+
334+
MaybeLocal<Value> values[] = {
335+
Number::New(isolate, rss_err ? 0 : stats->ru_maxrss * 1024),
336+
Number::New(isolate, heap_stats.total_heap_size()),
337+
Number::New(isolate, heap_stats.used_heap_size()),
338+
Number::New(isolate, heap_stats.external_memory()),
339+
Number::New(isolate, allocator == nullptr
340+
? 0
341+
: allocator->total_mem_usage()),
342+
};
343+
344+
Local<Value> argv[] = {
345+
Null(isolate),
346+
Undefined(isolate),
347+
};
348+
349+
if (rss_err) {
350+
argv[0] = UVException(isolate,
351+
rss_err,
352+
"uv_getrusage_thread",
353+
nullptr,
354+
nullptr,
355+
nullptr);
356+
} else if (!NewDictionaryInstanceNullProto(
357+
env->context(), tmpl, values)
358+
.ToLocal(&argv[1])) {
359+
return;
360+
}
361+
362+
taker->get()->MakeCallback(
363+
env->ondone_string(), arraysize(argv), argv);
364+
},
365+
CallbackFlags::kUnrefed);
366+
});
367+
368+
if (scheduled) {
369+
args.GetReturnValue().Set(wrap);
370+
}
371+
}
372+
274373
size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit,
275374
size_t initial_heap_limit) {
276375
Worker* worker = static_cast<Worker*>(data);
@@ -1489,6 +1588,7 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
14891588
SetProtoMethod(isolate, w, "loopIdleTime", Worker::LoopIdleTime);
14901589
SetProtoMethod(isolate, w, "loopStartTime", Worker::LoopStartTime);
14911590
SetProtoMethod(isolate, w, "getHeapStatistics", Worker::GetHeapStatistics);
1591+
SetProtoMethod(isolate, w, "getMemoryUsage", Worker::GetMemoryUsage);
14921592
SetProtoMethod(isolate, w, "cpuUsage", Worker::CpuUsage);
14931593
SetProtoMethod(isolate, w, "startCpuProfile", Worker::StartCpuProfile);
14941594
SetProtoMethod(isolate, w, "stopCpuProfile", Worker::StopCpuProfile);
@@ -1539,6 +1639,20 @@ void CreateWorkerPerIsolateProperties(IsolateData* isolate_data,
15391639
isolate_data->set_worker_cpu_usage_taker_template(wst->InstanceTemplate());
15401640
}
15411641

1642+
{
1643+
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);
1644+
1645+
wst->InstanceTemplate()->SetInternalFieldCount(
1646+
WorkerMemoryUsageTaker::kInternalFieldCount);
1647+
wst->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));
1648+
1649+
Local<String> wst_string =
1650+
FIXED_ONE_BYTE_STRING(isolate, "WorkerMemoryUsageTaker");
1651+
wst->SetClassName(wst_string);
1652+
isolate_data->set_worker_memory_usage_taker_template(
1653+
wst->InstanceTemplate());
1654+
}
1655+
15421656
{
15431657
Local<FunctionTemplate> wst = NewFunctionTemplate(isolate, nullptr);
15441658

@@ -1643,6 +1757,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
16431757
registry->Register(Worker::LoopIdleTime);
16441758
registry->Register(Worker::LoopStartTime);
16451759
registry->Register(Worker::GetHeapStatistics);
1760+
registry->Register(Worker::GetMemoryUsage);
16461761
registry->Register(Worker::CpuUsage);
16471762
registry->Register(Worker::StartCpuProfile);
16481763
registry->Register(Worker::StopCpuProfile);

src/node_worker.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class Worker : public AsyncWrap {
8484
static void StopCpuProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
8585
static void StartHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
8686
static void StopHeapProfile(const v8::FunctionCallbackInfo<v8::Value>& args);
87+
static void GetMemoryUsage(const v8::FunctionCallbackInfo<v8::Value>& args);
8788

8889
private:
8990
bool CreateEnvMessagePort(Environment* env);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const assert = require('assert');
5+
const { Worker, isMainThread } = require('worker_threads');
6+
7+
if (!isMainThread) {
8+
common.skip('This test only works on the main thread');
9+
}
10+
11+
const worker = new Worker(`
12+
const { parentPort } = require('worker_threads');
13+
parentPort.once('message', () => process.exit(0));
14+
parentPort.postMessage('ready');
15+
`, { eval: true });
16+
17+
worker.once('message', common.mustCall(async () => {
18+
const usage = await worker.getMemoryUsage();
19+
const keys = [
20+
'rss',
21+
'heapTotal',
22+
'heapUsed',
23+
'external',
24+
'arrayBuffers',
25+
].sort();
26+
27+
assert.deepStrictEqual(Object.keys(usage).sort(), keys);
28+
29+
for (const key of keys) {
30+
assert.strictEqual(typeof usage[key], 'number', `Expected ${key} to be a number`);
31+
assert.ok(usage[key] >= 0, `Expected ${key} to be >= 0`);
32+
}
33+
34+
assert.ok(usage.heapUsed <= usage.heapTotal,
35+
'heapUsed should not exceed heapTotal');
36+
37+
worker.postMessage('exit');
38+
}));
39+
40+
worker.once('exit', common.mustCall(async (code) => {
41+
assert.strictEqual(code, 0);
42+
await assert.rejects(worker.getMemoryUsage(), {
43+
code: 'ERR_WORKER_NOT_RUNNING'
44+
});
45+
}));

0 commit comments

Comments
 (0)