v8: add v8.getCppHeapStatistics() method

Expose `CppHeap` data via
`cppgc::CppHeap::CollectStatistics()` in the v8 module.

PR-URL: https://github.com/nodejs/node/pull/57146
Fixes: https://github.com/nodejs/node/issues/56533
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Aditi 2025-03-04 16:44:34 +05:30 committed by GitHub
parent a914f173d2
commit d3064e8ddb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 440 additions and 1 deletions

View File

@ -271,6 +271,92 @@ buffers and external strings.
}
```
## `v8.getCppHeapStatistics([detailLevel])`
Retrieves [CppHeap][] statistics regarding memory consumption and
utilization using the V8 [`CollectStatistics()`][] function which
may change from one V8 version to the
next.
* `detailLevel` {string|undefined}: **Default:** `'detailed'`.
Specifies the level of detail in the returned statistics.
Accepted values are:
* `'brief'`: Brief statistics contain only the top-level
allocated and used
memory statistics for the entire heap.
* `'detailed'`: Detailed statistics also contain a break
down per space and page, as well as freelist statistics
and object type histograms.
It returns an object with a structure similar to the
[`cppgc::HeapStatistics`][] object. See the [V8 documentation][`cppgc::HeapStatistics struct`]
for more information about the properties of the object.
```js
// Detailed
({
committed_size_bytes: 131072,
resident_size_bytes: 131072,
used_size_bytes: 152,
space_statistics: [
{
name: 'NormalPageSpace0',
committed_size_bytes: 0,
resident_size_bytes: 0,
used_size_bytes: 0,
page_stats: [{}],
free_list_stats: {},
},
{
name: 'NormalPageSpace1',
committed_size_bytes: 131072,
resident_size_bytes: 131072,
used_size_bytes: 152,
page_stats: [{}],
free_list_stats: {},
},
{
name: 'NormalPageSpace2',
committed_size_bytes: 0,
resident_size_bytes: 0,
used_size_bytes: 0,
page_stats: [{}],
free_list_stats: {},
},
{
name: 'NormalPageSpace3',
committed_size_bytes: 0,
resident_size_bytes: 0,
used_size_bytes: 0,
page_stats: [{}],
free_list_stats: {},
},
{
name: 'LargePageSpace',
committed_size_bytes: 0,
resident_size_bytes: 0,
used_size_bytes: 0,
page_stats: [{}],
free_list_stats: {},
},
],
type_names: [],
detail_level: 'detailed',
});
```
```js
// Brief
({
committed_size_bytes: 131072,
resident_size_bytes: 131072,
used_size_bytes: 128864,
space_statistics: [],
type_names: [],
detail_level: 'brief',
});
```
## `v8.queryObjects(ctor[, options])`
<!-- YAML
@ -1343,12 +1429,14 @@ writeString('hello');
writeString('你好');
```
[CppHeap]: https://v8docs.nodesource.com/node-22.4/d9/dc4/classv8_1_1_cpp_heap.html
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[Hook Callbacks]: #hook-callbacks
[V8]: https://developers.google.com/v8/
[`--heapsnapshot-near-heap-limit`]: cli.md#--heapsnapshot-near-heap-limitmax_count
[`AsyncLocalStorage`]: async_context.md#class-asynclocalstorage
[`Buffer`]: buffer.md
[`CollectStatistics()`]: https://v8docs.nodesource.com/node-22.4/d9/dc4/classv8_1_1_cpp_heap.html#a3a5d09567758e608fffde50eeabc2feb
[`DefaultDeserializer`]: #class-v8defaultdeserializer
[`DefaultSerializer`]: #class-v8defaultserializer
[`Deserializer`]: #class-v8deserializer
@ -1362,6 +1450,8 @@ writeString('你好');
[`async_hooks`]: async_hooks.md
[`before` callback]: #beforepromise
[`buffer.constants.MAX_LENGTH`]: buffer.md#bufferconstantsmax_length
[`cppgc::HeapStatistics struct`]: https://v8docs.nodesource.com/node-22.4/df/d2f/structcppgc_1_1_heap_statistics.html
[`cppgc::HeapStatistics`]: https://v8docs.nodesource.com/node-22.4/d7/d51/heap-statistics_8h_source.html
[`deserializer._readHostObject()`]: #deserializer_readhostobject
[`deserializer.transferArrayBuffer()`]: #deserializertransferarraybufferid-arraybuffer
[`init` callback]: #initpromise-parent

View File

@ -37,7 +37,11 @@ const {
} = primordials;
const { Buffer } = require('buffer');
const { validateString, validateUint32 } = require('internal/validators');
const {
validateString,
validateUint32,
validateOneOf,
} = require('internal/validators');
const {
Serializer,
Deserializer,
@ -146,6 +150,8 @@ const {
heapStatisticsBuffer,
heapCodeStatisticsBuffer,
heapSpaceStatisticsBuffer,
getCppHeapStatistics: _getCppHeapStatistics,
detailLevel,
} = binding;
const kNumberOfHeapSpaces = kHeapSpaces.length;
@ -271,6 +277,19 @@ function setHeapSnapshotNearHeapLimit(limit) {
_setHeapSnapshotNearHeapLimit(limit);
}
const detailLevelDict = {
__proto__: null,
detailed: detailLevel.DETAILED,
brief: detailLevel.BRIEF,
};
function getCppHeapStatistics(type = 'detailed') {
validateOneOf(type, 'type', ['brief', 'detailed']);
const result = _getCppHeapStatistics(detailLevelDict[type]);
result.detail_level = type;
return result;
}
/* V8 serialization API */
/* JS methods for the base objects */
@ -442,6 +461,7 @@ module.exports = {
getHeapStatistics,
getHeapSpaceStatistics,
getHeapCodeStatistics,
getCppHeapStatistics,
setFlagsFromString,
Serializer,
Deserializer,

View File

@ -40,9 +40,13 @@ using v8::HandleScope;
using v8::HeapCodeStatistics;
using v8::HeapSpaceStatistics;
using v8::HeapStatistics;
using v8::Int32;
using v8::Integer;
using v8::Isolate;
using v8::Local;
using v8::LocalVector;
using v8::MaybeLocal;
using v8::Name;
using v8::Object;
using v8::ScriptCompiler;
using v8::String;
@ -313,6 +317,191 @@ static void SetHeapStatistics(JSONWriter* writer, Isolate* isolate) {
writer->json_arrayend();
}
static MaybeLocal<Object> ConvertHeapStatsToJSObject(
Isolate* isolate, const cppgc::HeapStatistics& stats) {
Local<Context> context = isolate->GetCurrentContext();
// Space Statistics
LocalVector<Value> space_statistics_array(isolate);
space_statistics_array.reserve(stats.space_stats.size());
for (size_t i = 0; i < stats.space_stats.size(); i++) {
const cppgc::HeapStatistics::SpaceStatistics& space_stats =
stats.space_stats[i];
// Page Statistics
LocalVector<Value> page_statistics_array(isolate);
page_statistics_array.reserve(space_stats.page_stats.size());
for (size_t j = 0; j < space_stats.page_stats.size(); j++) {
const cppgc::HeapStatistics::PageStatistics& page_stats =
space_stats.page_stats[j];
// Object Statistics
LocalVector<Value> object_statistics_array(isolate);
object_statistics_array.reserve(page_stats.object_statistics.size());
for (size_t k = 0; k < page_stats.object_statistics.size(); k++) {
const cppgc::HeapStatistics::ObjectStatsEntry& object_stats =
page_stats.object_statistics[k];
Local<Name> object_stats_names[] = {
FIXED_ONE_BYTE_STRING(isolate, "allocated_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "object_count")};
Local<Value> object_stats_values[] = {
Uint32::NewFromUnsigned(
isolate, static_cast<uint32_t>(object_stats.allocated_bytes)),
Uint32::NewFromUnsigned(
isolate, static_cast<uint32_t>(object_stats.object_count))};
Local<Object> object_stats_object =
Object::New(isolate,
Null(isolate),
object_stats_names,
object_stats_values,
arraysize(object_stats_names));
object_statistics_array.emplace_back(object_stats_object);
}
// Set page statistics
Local<Name> page_stats_names[] = {
FIXED_ONE_BYTE_STRING(isolate, "committed_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "resident_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "used_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "object_statistics")};
Local<Value> page_stats_values[] = {
Uint32::NewFromUnsigned(
isolate, static_cast<uint32_t>(page_stats.committed_size_bytes)),
Uint32::NewFromUnsigned(
isolate, static_cast<uint32_t>(page_stats.resident_size_bytes)),
Uint32::NewFromUnsigned(
isolate, static_cast<uint32_t>(page_stats.used_size_bytes)),
Array::New(isolate,
object_statistics_array.data(),
object_statistics_array.size())};
Local<Object> page_stats_object =
Object::New(isolate,
Null(isolate),
page_stats_names,
page_stats_values,
arraysize(page_stats_names));
page_statistics_array.emplace_back(page_stats_object);
}
// Free List Statistics
Local<Name> free_list_statistics_names[] = {
FIXED_ONE_BYTE_STRING(isolate, "bucket_size"),
FIXED_ONE_BYTE_STRING(isolate, "free_count"),
FIXED_ONE_BYTE_STRING(isolate, "free_size")};
Local<Value> bucket_size_value;
if (!ToV8Value(context, space_stats.free_list_stats.bucket_size)
.ToLocal(&bucket_size_value)) {
return MaybeLocal<Object>();
}
Local<Value> free_count_value;
if (!ToV8Value(context, space_stats.free_list_stats.free_count)
.ToLocal(&free_count_value)) {
return MaybeLocal<Object>();
}
Local<Value> free_size_value;
if (!ToV8Value(context, space_stats.free_list_stats.free_size)
.ToLocal(&free_size_value)) {
return MaybeLocal<Object>();
}
Local<Value> free_list_statistics_values[] = {
bucket_size_value, free_count_value, free_size_value};
Local<Object> free_list_statistics_obj =
Object::New(isolate,
Null(isolate),
free_list_statistics_names,
free_list_statistics_values,
arraysize(free_list_statistics_names));
// Set Space Statistics
Local<Name> space_stats_names[] = {
FIXED_ONE_BYTE_STRING(isolate, "name"),
FIXED_ONE_BYTE_STRING(isolate, "committed_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "resident_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "used_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "page_stats"),
FIXED_ONE_BYTE_STRING(isolate, "free_list_stats")};
Local<Value> name_value;
if (!ToV8Value(context, stats.space_stats[i].name, isolate)
.ToLocal(&name_value)) {
return MaybeLocal<Object>();
}
Local<Value> space_stats_values[] = {
name_value,
Uint32::NewFromUnsigned(
isolate,
static_cast<uint32_t>(stats.space_stats[i].committed_size_bytes)),
Uint32::NewFromUnsigned(
isolate,
static_cast<uint32_t>(stats.space_stats[i].resident_size_bytes)),
Uint32::NewFromUnsigned(
isolate,
static_cast<uint32_t>(stats.space_stats[i].used_size_bytes)),
Array::New(isolate,
page_statistics_array.data(),
page_statistics_array.size()),
free_list_statistics_obj,
};
Local<Object> space_stats_object =
Object::New(isolate,
Null(isolate),
space_stats_names,
space_stats_values,
arraysize(space_stats_names));
space_statistics_array.emplace_back(space_stats_object);
}
// Set heap statistics
Local<Name> heap_statistics_names[] = {
FIXED_ONE_BYTE_STRING(isolate, "committed_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "resident_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "used_size_bytes"),
FIXED_ONE_BYTE_STRING(isolate, "space_statistics"),
FIXED_ONE_BYTE_STRING(isolate, "type_names")};
Local<Value> type_names_value;
if (!ToV8Value(context, stats.type_names, isolate)
.ToLocal(&type_names_value)) {
return MaybeLocal<Object>();
}
Local<Value> heap_statistics_values[] = {
Uint32::NewFromUnsigned(
isolate, static_cast<uint32_t>(stats.committed_size_bytes)),
Uint32::NewFromUnsigned(isolate,
static_cast<uint32_t>(stats.resident_size_bytes)),
Uint32::NewFromUnsigned(isolate,
static_cast<uint32_t>(stats.used_size_bytes)),
Array::New(isolate,
space_statistics_array.data(),
space_statistics_array.size()),
type_names_value};
Local<Object> heap_statistics_object =
Object::New(isolate,
Null(isolate),
heap_statistics_names,
heap_statistics_values,
arraysize(heap_statistics_names));
return heap_statistics_object;
}
static void GetCppHeapStatistics(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope handle_scope(isolate);
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsInt32());
cppgc::HeapStatistics stats = isolate->GetCppHeap()->CollectStatistics(
static_cast<cppgc::HeapStatistics::DetailLevel>(
args[0].As<Int32>()->Value()));
Local<Object> result;
if (!ConvertHeapStatsToJSObject(isolate, stats).ToLocal(&result)) {
return;
}
args.GetReturnValue().Set(result);
}
static void BeforeGCCallback(Isolate* isolate,
v8::GCType gc_type,
v8::GCCallbackFlags flags,
@ -457,6 +646,8 @@ void Initialize(Local<Object> target,
target,
"updateHeapCodeStatisticsBuffer",
UpdateHeapCodeStatisticsBuffer);
SetMethodNoSideEffect(
context, target, "getCppHeapStatistics", GetCppHeapStatistics);
size_t number_of_heap_spaces = env->isolate()->NumberOfHeapSpaces();
@ -510,6 +701,18 @@ void Initialize(Local<Object> target,
SetProtoMethod(env->isolate(), t, "start", GCProfiler::Start);
SetProtoMethod(env->isolate(), t, "stop", GCProfiler::Stop);
SetConstructorFunction(context, target, "GCProfiler", t);
{
Isolate* isolate = env->isolate();
Local<Object> detail_level = Object::New(isolate);
cppgc::HeapStatistics::DetailLevel DETAILED =
cppgc::HeapStatistics::DetailLevel::kDetailed;
cppgc::HeapStatistics::DetailLevel BRIEF =
cppgc::HeapStatistics::DetailLevel::kBrief;
NODE_DEFINE_CONSTANT(detail_level, DETAILED);
NODE_DEFINE_CONSTANT(detail_level, BRIEF);
READONLY_PROPERTY(target, "detailLevel", detail_level);
}
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
@ -522,6 +725,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GCProfiler::New);
registry->Register(GCProfiler::Start);
registry->Register(GCProfiler::Stop);
registry->Register(GetCppHeapStatistics);
registry->Register(IsStringOneByteRepresentation);
registry->Register(FastIsStringOneByteRepresentation);
registry->Register(fast_is_string_one_byte_representation_.GetTypeInfo());

View File

@ -0,0 +1,125 @@
'use strict';
// Tests that v8.getCppHeapStatistics() returns an object with an expected shape.
require('../common');
const assert = require('assert');
const v8 = require('v8');
// Detailed heap statistics
const heapStatsDetailed = v8.getCppHeapStatistics('detailed');
assert.strictEqual(heapStatsDetailed.detail_level, 'detailed');
const expectedTopLevelKeys = [
'committed_size_bytes',
'resident_size_bytes',
'used_size_bytes',
'detail_level',
'space_statistics',
'type_names',
].sort();
// Check top level properties
const actualTopLevelKeys = Object.keys(heapStatsDetailed).sort();
assert.deepStrictEqual(actualTopLevelKeys, expectedTopLevelKeys);
// Check types of top level properties
assert.strictEqual(typeof heapStatsDetailed.committed_size_bytes, 'number');
assert.strictEqual(typeof heapStatsDetailed.resident_size_bytes, 'number');
assert.strictEqual(typeof heapStatsDetailed.used_size_bytes, 'number');
assert.strictEqual(typeof heapStatsDetailed.detail_level, 'string');
assert.strictEqual(Array.isArray(heapStatsDetailed.space_statistics), true);
assert.strictEqual(Array.isArray(heapStatsDetailed.type_names), true);
// Check space statistics array
const expectedSpaceKeys = [
'name',
'committed_size_bytes',
'resident_size_bytes',
'used_size_bytes',
'page_stats',
'free_list_stats',
].sort();
heapStatsDetailed.space_statistics.forEach((space) => {
const actualSpaceKeys = Object.keys(space).sort();
assert.deepStrictEqual(actualSpaceKeys, expectedSpaceKeys);
assert.strictEqual(typeof space.name, 'string');
assert.strictEqual(typeof space.committed_size_bytes, 'number');
assert.strictEqual(typeof space.resident_size_bytes, 'number');
assert.strictEqual(typeof space.used_size_bytes, 'number');
assert.strictEqual(Array.isArray(space.page_stats), true);
assert.strictEqual(typeof space.free_list_stats, 'object');
});
// Check page statistics array
const expectedPageKeys = [
'committed_size_bytes',
'resident_size_bytes',
'used_size_bytes',
'object_statistics',
].sort();
heapStatsDetailed.space_statistics.forEach((space) => {
space.page_stats.forEach((page) => {
const actualPageKeys = Object.keys(page).sort();
assert.deepStrictEqual(actualPageKeys, expectedPageKeys);
assert.strictEqual(typeof page.committed_size_bytes, 'number');
assert.strictEqual(typeof page.resident_size_bytes, 'number');
assert.strictEqual(typeof page.used_size_bytes, 'number');
assert.strictEqual(Array.isArray(page.object_statistics), true);
});
});
// Check free list statistics
const expectedFreeListKeys = ['bucket_size', 'free_count', 'free_size'].sort();
heapStatsDetailed.space_statistics.forEach((space) => {
const actualFreeListKeys = Object.keys(space.free_list_stats).sort();
assert.deepStrictEqual(actualFreeListKeys, expectedFreeListKeys);
assert.strictEqual(Array.isArray(space.free_list_stats.bucket_size), true);
assert.strictEqual(Array.isArray(space.free_list_stats.free_count), true);
assert.strictEqual(Array.isArray(space.free_list_stats.free_size), true);
});
// Check object statistics
const expectedObjectStatsKeys = ['allocated_bytes', 'object_count'].sort();
heapStatsDetailed.space_statistics.forEach((space) => {
space.page_stats.forEach((page) => {
page.object_statistics.forEach((objectStats) => {
const actualObjectStatsKeys = Object.keys(objectStats).sort();
assert.deepStrictEqual(actualObjectStatsKeys, expectedObjectStatsKeys);
assert.strictEqual(typeof objectStats.allocated_bytes, 'number');
assert.strictEqual(typeof objectStats.object_count, 'number');
});
});
});
// Check type names
heapStatsDetailed.type_names.forEach((typeName) => {
assert.strictEqual(typeof typeName, 'string');
});
// Brief heap statistics
const heapStatsBrief = v8.getCppHeapStatistics('brief');
const expectedBriefKeys = [
'committed_size_bytes',
'resident_size_bytes',
'used_size_bytes',
'detail_level',
'space_statistics',
'type_names',
].sort();
// Check top level properties
const actualBriefKeys = Object.keys(heapStatsBrief).sort();
assert.strictEqual(heapStatsBrief.detail_level, 'brief');
assert.deepStrictEqual(actualBriefKeys, expectedBriefKeys);
// Check types of top level properties
assert.strictEqual(typeof heapStatsBrief.committed_size_bytes, 'number');
assert.strictEqual(typeof heapStatsBrief.resident_size_bytes, 'number');
assert.strictEqual(typeof heapStatsBrief.used_size_bytes, 'number');
assert.strictEqual(typeof heapStatsBrief.detail_level, 'string');
assert.strictEqual(Array.isArray(heapStatsBrief.space_statistics), true);
assert.strictEqual(Array.isArray(heapStatsBrief.type_names), true);