-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
worker: add worker.getMemoryUsage() API #60778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
| pr-url: https://github.com/nodejs/node/pull/REPLACEME | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't RSS a per process thing? |
||||||
| 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. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should explain the relationship to |
||||||
| ### `worker.performance` | ||||||
| <!-- YAML | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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()), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no guarantee that |
||
| }; | ||
|
|
||
| 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); | ||
|
|
@@ -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); | ||
|
|
@@ -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); | ||
|
|
||
|
|
@@ -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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
| const { Worker, isMainThread } = require('worker_threads'); | ||
|
|
||
| if (!isMainThread) { | ||
| common.skip('This test only works on the main thread'); | ||
| } | ||
|
|
||
| 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' | ||
| }); | ||
| })); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.