Skip to content
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

process: report ArrayBuffer memory in memoryUsage() #31550

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 17 additions & 10 deletions doc/api/process.md
Expand Up @@ -1510,6 +1510,9 @@ is no entry script.
<!-- YAML
added: v0.1.16
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/???
addaleax marked this conversation as resolved.
Show resolved Hide resolved
description: Added `arrayBuffers` to the returned object.
- version: v7.2.0
pr-url: https://github.com/nodejs/node/pull/9587
description: Added `external` to the returned object.
Expand All @@ -1520,6 +1523,7 @@ changes:
* `heapTotal` {integer}
* `heapUsed` {integer}
* `external` {integer}
* `arrayBuffers` {integer}

The `process.memoryUsage()` method returns an object describing the memory usage
of the Node.js process measured in bytes.
Expand All @@ -1538,19 +1542,21 @@ Will generate:
rss: 4935680,
heapTotal: 1826816,
heapUsed: 650472,
external: 49879
external: 49879,
arrayBuffers: 9386
}
```

`heapTotal` and `heapUsed` refer to V8's memory usage.
`external` refers to the memory usage of C++ objects bound to JavaScript
objects managed by V8. `rss`, Resident Set Size, is the amount of space
occupied in the main memory device (that is a subset of the total allocated
memory) for the process, which includes the _heap_, _code segment_ and _stack_.

The _heap_ is where objects, strings, and closures are stored. Variables are
stored in the _stack_ and the actual JavaScript code resides in the
_code segment_.
* `heapTotal` and `heapUsed` refer to V8's memory usage.
* `external` refers to the memory usage of C++ objects bound to JavaScript
objects managed by V8.
* `rss`, Resident Set Size, is the amount of space occupied in the main
memory device (that is a subset of the total allocated memory) for the
process, including all C++ and JavaScript objects and code.
* `arrayBuffers` refers to memory allocated for `ArrayBuffer`s and
`SharedArrayBuffer`s, including all Node.js [`Buffer`][]s.
This is also included in the `external` value. When Node.js is used as an
embedded library, this value may be `0`.
Copy link
Member

Choose a reason for hiding this comment

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

sorry for being raw here, but I have these questions:

When Node.js is used as an embedded library, this value may be 0.

  • why? is it because Node.js cannot truly account for the allocations of the embedder?
  • where? which part of the code takes the decision based on the embedded state?
  • may be 0 - why again - why this decision cannot be deterministic as opposed to probabilistic? which factors decide the outcome? how would one use those factors to decide whether the value is trustworthy or not?

Copy link
Member Author

Choose a reason for hiding this comment

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

sorry for being raw here, but I have these questions:

When Node.js is used as an embedded library, this value may be 0.

  • why? is it because Node.js cannot truly account for the allocations of the embedder?

Because embedders can specify their own ArrayBuffer::Allocator, which would not necessarily track allocations (e.g. Electron does this).

  • where? which part of the code takes the decision based on the embedded state?

Inside the C++ MemoryUsage() binding, there’s a array_buffer_allocator == nullptr check. If there is no NodeArrayBufferAllocator* that the embedder informed us about, then the method sets the field for array buffers to 0.

  • may be 0 - why again - why this decision cannot be deterministic as opposed to probabilistic?

I don’t really understand this question, sorry – there is nothing probabilistic involved here, I think?

which factors decide the outcome?

I think that’s also answered by the first question, but let me know if I’m misunderstanding.

how would one use those factors to decide whether the value is trustworthy or not?

I think it’s safe to say that the value is trustworthy when it’s not 0.

I didn’t want to go into a ton of detail in the documentation here – imo there should be an indication that the value may not always be provided, and that tools should not make the expectation that the value is non-zero. But for a tool it’s not really going to matter why exactly the value can’t be reported, I think, other than that it’s related to the fact that the Node.js code is running in a specific environment that behaves differently.

Copy link
Member

Choose a reason for hiding this comment

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

there is nothing probabilistic involved here, I think?

I termed it as probabilistic because of may be 0 phrase. But based on your explanation to the rest of the questions, I understand it as:

if embedders use only non-node.js allocators without a tracker, then this value WILL be 0 (not truly reflecting actual consumption), and if embedders rely on node.js' allocators, then the value will be accurate - zero or non-zero

Are we on the same page?

Copy link
Member Author

Choose a reason for hiding this comment

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

@gireeshpunathil Yes, we are 👍 I feel like the most important thing is that the doc makes clear that this does not affect standard Node.js binaries.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe a simple "because such allocations may not be tracked" would be good?

Copy link
Member Author

Choose a reason for hiding this comment

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

@jasnell Sure, done 👍


When using [`Worker`][] threads, `rss` will be a value that is valid for the
entire process, while the other fields will only refer to the current thread.
Expand Down Expand Up @@ -2518,6 +2524,7 @@ cases:
[`'exit'`]: #process_event_exit
[`'message'`]: child_process.html#child_process_event_message
[`'uncaughtException'`]: #process_event_uncaughtexception
[`Buffer`]: buffer.html
[`ChildProcess.disconnect()`]: child_process.html#child_process_subprocess_disconnect
[`ChildProcess.send()`]: child_process.html#child_process_subprocess_send_message_sendhandle_options_callback
[`ChildProcess`]: child_process.html#child_process_class_childprocess
Expand Down
5 changes: 3 additions & 2 deletions lib/internal/process/per_thread.js
Expand Up @@ -146,14 +146,15 @@ function wrapProcessMethods(binding) {
return hrBigintValues[0];
}

const memValues = new Float64Array(4);
const memValues = new Float64Array(5);
function memoryUsage() {
_memoryUsage(memValues);
return {
rss: memValues[0],
heapTotal: memValues[1],
heapUsed: memValues[2],
external: memValues[3]
external: memValues[3],
arrayBuffers: memValues[4]
};
}

Expand Down
20 changes: 20 additions & 0 deletions src/api/environment.cc
Expand Up @@ -87,12 +87,30 @@ static void HostCleanupFinalizationGroupCallback(
}

void* NodeArrayBufferAllocator::Allocate(size_t size) {
total_mem_usage_ += size;
if (zero_fill_field_ || per_process::cli_options->zero_fill_all_buffers)
return UncheckedCalloc(size);
richardlau marked this conversation as resolved.
Show resolved Hide resolved
else
return UncheckedMalloc(size);
}

void* NodeArrayBufferAllocator::AllocateUninitialized(size_t size) {
total_mem_usage_ += size;
return node::UncheckedMalloc(size);
}

void* NodeArrayBufferAllocator::Reallocate(
void* data, size_t old_size, size_t size) {
total_mem_usage_ += size - old_size;
return static_cast<void*>(
UncheckedRealloc<char>(static_cast<char*>(data), size));
}

void NodeArrayBufferAllocator::Free(void* data, size_t size) {
total_mem_usage_ -= size;
free(data);
}

DebuggingArrayBufferAllocator::~DebuggingArrayBufferAllocator() {
CHECK(allocations_.empty());
}
Expand Down Expand Up @@ -140,11 +158,13 @@ void* DebuggingArrayBufferAllocator::Reallocate(void* data,

void DebuggingArrayBufferAllocator::RegisterPointer(void* data, size_t size) {
Mutex::ScopedLock lock(mutex_);
NodeArrayBufferAllocator::RegisterPointer(data, size);
RegisterPointerInternal(data, size);
}

void DebuggingArrayBufferAllocator::UnregisterPointer(void* data, size_t size) {
Mutex::ScopedLock lock(mutex_);
NodeArrayBufferAllocator::UnregisterPointer(data, size);
UnregisterPointerInternal(data, size);
}

Expand Down
18 changes: 10 additions & 8 deletions src/node_internals.h
Expand Up @@ -109,20 +109,22 @@ class NodeArrayBufferAllocator : public ArrayBufferAllocator {
inline uint32_t* zero_fill_field() { return &zero_fill_field_; }

void* Allocate(size_t size) override; // Defined in src/node.cc
void* AllocateUninitialized(size_t size) override
{ return node::UncheckedMalloc(size); }
void Free(void* data, size_t) override { free(data); }
virtual void* Reallocate(void* data, size_t old_size, size_t size) {
return static_cast<void*>(
UncheckedRealloc<char>(static_cast<char*>(data), size));
void* AllocateUninitialized(size_t size) override;
void Free(void* data, size_t size) override;
virtual void* Reallocate(void* data, size_t old_size, size_t size);
virtual void RegisterPointer(void* data, size_t size) {
total_mem_usage_ += size;
}
virtual void UnregisterPointer(void* data, size_t size) {
total_mem_usage_ -= size;
}
virtual void RegisterPointer(void* data, size_t size) {}
virtual void UnregisterPointer(void* data, size_t size) {}

NodeArrayBufferAllocator* GetImpl() final { return this; }
inline uint64_t total_mem_usage() const { return total_mem_usage_; }

private:
uint32_t zero_fill_field_ = 1; // Boolean but exposed as uint32 to JS land.
std::atomic<uint64_t> total_mem_usage_ {0};
addaleax marked this conversation as resolved.
Show resolved Hide resolved
};

class DebuggingArrayBufferAllocator final : public NodeArrayBufferAllocator {
Expand Down
7 changes: 6 additions & 1 deletion src/node_process_methods.cc
Expand Up @@ -200,17 +200,22 @@ static void MemoryUsage(const FunctionCallbackInfo<Value>& args) {
HeapStatistics v8_heap_stats;
isolate->GetHeapStatistics(&v8_heap_stats);

NodeArrayBufferAllocator* array_buffer_allocator =
env->isolate_data()->node_allocator();

// Get the double array pointer from the Float64Array argument.
CHECK(args[0]->IsFloat64Array());
Local<Float64Array> array = args[0].As<Float64Array>();
CHECK_EQ(array->Length(), 4);
CHECK_EQ(array->Length(), 5);
Local<ArrayBuffer> ab = array->Buffer();
double* fields = static_cast<double*>(ab->GetBackingStore()->Data());

fields[0] = rss;
fields[1] = v8_heap_stats.total_heap_size();
fields[2] = v8_heap_stats.used_heap_size();
fields[3] = v8_heap_stats.external_memory();
fields[4] = array_buffer_allocator == nullptr ?
0 : array_buffer_allocator->total_mem_usage();
}

void RawDebug(const FunctionCallbackInfo<Value>& args) {
Expand Down
13 changes: 13 additions & 0 deletions test/parallel/test-memory-usage.js
Expand Up @@ -30,3 +30,16 @@ if (!common.isIBMi)
assert.ok(r.heapTotal > 0);
assert.ok(r.heapUsed > 0);
assert.ok(r.external > 0);

assert.strictEqual(typeof r.arrayBuffers, 'number');
if (r.arrayBuffers > 0) {
const size = 10 * 1024 * 1024;
// eslint-disable-next-line no-unused-vars
const ab = new ArrayBuffer(size);

const after = process.memoryUsage();
assert(after.external - r.external >= size,
`${after.external} - ${r.external} >= ${size}`);
assert.strictEqual(after.arrayBuffers - r.arrayBuffers, size,
`${after.arrayBuffers} - ${r.arrayBuffers} >= ${size}`);
}