Repository: nodejs/node
Pull request: v8: add v8.getCppHeapStatistics() method

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

Changed files:
filename: doc/api/v8.md
patch: @@ -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

filename: lib/v8.js
patch: @@ -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,

filename: src/node_v8.cc
patch: @@ -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());

filename: test/parallel/test-cppheap-stats.js
patch: @@ -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);