From 6d4a3489d2f0ef69c625e11d1f9b4816199212fc Mon Sep 17 00:00:00 2001 From: theanarkh Date: Mon, 13 Oct 2025 00:34:58 +0800 Subject: [PATCH 1/4] v8: add heap profile API --- doc/api/v8.md | 41 ++++++++++++++++++ lib/v8.js | 28 ++++++++++++ src/node_v8.cc | 28 ++++++++++++ src/node_worker.cc | 62 +-------------------------- src/node_worker.h | 1 + src/util.cc | 61 ++++++++++++++++++++++++++ src/util.h | 1 + test/parallel/test-v8-heap-profile.js | 15 +++++++ 8 files changed, 177 insertions(+), 60 deletions(-) create mode 100644 test/parallel/test-v8-heap-profile.js diff --git a/doc/api/v8.md b/doc/api/v8.md index 7ee7a748674cae..a3a461a2469f9a 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1617,6 +1617,30 @@ added: Stopping collecting the profile and the profile will be discarded. +## Class: `SyncHeapProfileHandle` + + + +### `syncHeapProfileHandle.stop()` + + + +* Returns: {string} + +Stopping collecting the profile and return the profile data. + +### `syncHeapProfileHandle[Symbol.dispose]()` + + + +Stopping collecting the profile and the profile will be discarded. + ## Class: `CPUProfileHandle` + +* Returns: {SyncHeapProfileHandle} + +Starting a heap profile then return a `SyncHeapProfileHandle` object. +This API supports `using` syntax. + +```cjs +const handle = v8.startHeapProfile(); +const profile = handle.stop(); +console.log(profile); +``` + [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 diff --git a/lib/v8.js b/lib/v8.js index c0d4074aac21d5..875eb6bf561bd6 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -115,6 +115,8 @@ const { setFlagsFromString: _setFlagsFromString, startCpuProfile: _startCpuProfile, stopCpuProfile: _stopCpuProfile, + startHeapProfile: _startHeapProfile, + stopHeapProfile: _stopHeapProfile, isStringOneByteRepresentation: _isStringOneByteRepresentation, updateHeapStatisticsBuffer, updateHeapSpaceStatisticsBuffer, @@ -191,6 +193,22 @@ class SyncCPUProfileHandle { } } +class SyncHeapProfileHandle { + #stopped = false; + + stop() { + if (this.#stopped) { + return; + } + this.#stopped = true; + return _stopHeapProfile(); + }; + + [SymbolDispose]() { + this.stop(); + } +} + /** * Starting CPU Profile. * @returns {SyncCPUProfileHandle} @@ -200,6 +218,15 @@ function startCpuProfile() { return new SyncCPUProfileHandle(id); } +/** + * Starting Heap Profile. + * @returns {SyncHeapProfileHandle} + */ +function startHeapProfile() { + _startHeapProfile(); + return new SyncHeapProfileHandle(); +} + /** * Return whether this string uses one byte as underlying representation or not. * @param {string} content @@ -518,4 +545,5 @@ module.exports = { GCProfiler, isStringOneByteRepresentation, startCpuProfile, + startHeapProfile, }; diff --git a/src/node_v8.cc b/src/node_v8.cc index 12972f83ea0f61..d114f2f2bc3342 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -280,6 +280,30 @@ void StopCpuProfile(const FunctionCallbackInfo& args) { } } +void StartHeapProfile(const FunctionCallbackInfo& args) { + Isolate* isolate = args.GetIsolate(); + if (isolate->GetHeapProfiler()->StartSamplingHeapProfiler()) { + return; + } + THROW_ERR_HEAP_PROFILE_HAVE_BEEN_STARTED(isolate, + "Heap profile has been started"); +} + +void StopHeapProfile(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + std::ostringstream out_stream; + bool success = node::SerializeHeapProfile(isolate, out_stream); + if (success) { + Local result; + if (ToV8Value(env->context(), out_stream.str(), isolate).ToLocal(&result)) { + args.GetReturnValue().Set(result); + } + } else { + THROW_ERR_HEAP_PROFILE_NOT_STARTED(isolate, "heap profile not started"); + } +} + static void IsStringOneByteRepresentation( const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); @@ -740,6 +764,8 @@ void Initialize(Local target, SetMethod(context, target, "startCpuProfile", StartCpuProfile); SetMethod(context, target, "stopCpuProfile", StopCpuProfile); + SetMethod(context, target, "startHeapProfile", StartHeapProfile); + SetMethod(context, target, "stopHeapProfile", StopHeapProfile); // Export symbols used by v8.isStringOneByteRepresentation() SetFastMethodNoSideEffect(context, @@ -787,6 +813,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(fast_is_string_one_byte_representation_); registry->Register(StartCpuProfile); registry->Register(StopCpuProfile); + registry->Register(StartHeapProfile); + registry->Register(StopHeapProfile); } } // namespace v8_utils diff --git a/src/node_worker.cc b/src/node_worker.cc index c8fb93c9313aca..4aa07e39faa95a 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -20,7 +20,6 @@ using node::kAllowedInEnvvar; using node::kDisallowedInEnvvar; -using v8::AllocationProfile; using v8::Array; using v8::ArrayBuffer; using v8::Boolean; @@ -33,7 +32,6 @@ using v8::Float64Array; using v8::FunctionCallbackInfo; using v8::FunctionTemplate; using v8::HandleScope; -using v8::HeapProfiler; using v8::HeapStatistics; using v8::Integer; using v8::Isolate; @@ -1086,63 +1084,6 @@ void Worker::StartHeapProfile(const FunctionCallbackInfo& args) { } } -static void buildHeapProfileNode(Isolate* isolate, - const AllocationProfile::Node* node, - JSONWriter* writer) { - size_t selfSize = 0; - for (const auto& allocation : node->allocations) - selfSize += allocation.size * allocation.count; - - writer->json_keyvalue("selfSize", selfSize); - writer->json_keyvalue("id", node->node_id); - writer->json_objectstart("callFrame"); - writer->json_keyvalue("scriptId", node->script_id); - writer->json_keyvalue("lineNumber", node->line_number - 1); - writer->json_keyvalue("columnNumber", node->column_number - 1); - node::Utf8Value name(isolate, node->name); - node::Utf8Value script_name(isolate, node->script_name); - writer->json_keyvalue("functionName", *name); - writer->json_keyvalue("url", *script_name); - writer->json_objectend(); - - writer->json_arraystart("children"); - for (const auto* child : node->children) { - writer->json_start(); - buildHeapProfileNode(isolate, child, writer); - writer->json_end(); - } - writer->json_arrayend(); -} - -static bool serializeProfile(Isolate* isolate, std::ostringstream& out_stream) { - HandleScope scope(isolate); - HeapProfiler* profiler = isolate->GetHeapProfiler(); - std::unique_ptr profile(profiler->GetAllocationProfile()); - if (!profile) { - return false; - } - profiler->StopSamplingHeapProfiler(); - JSONWriter writer(out_stream, true); - writer.json_start(); - - writer.json_arraystart("samples"); - for (const auto& sample : profile->GetSamples()) { - writer.json_start(); - writer.json_keyvalue("size", sample.size * sample.count); - writer.json_keyvalue("nodeId", sample.node_id); - writer.json_keyvalue("ordinal", static_cast(sample.sample_id)); - writer.json_end(); - } - writer.json_arrayend(); - - writer.json_objectstart("head"); - buildHeapProfileNode(isolate, profile->GetRootNode(), &writer); - writer.json_objectend(); - - writer.json_end(); - return true; -} - void Worker::StopHeapProfile(const FunctionCallbackInfo& args) { Worker* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); @@ -1162,7 +1103,8 @@ void Worker::StopHeapProfile(const FunctionCallbackInfo& args) { bool scheduled = w->RequestInterrupt([taker = std::move(taker), env](Environment* worker_env) mutable { std::ostringstream out_stream; - bool success = serializeProfile(worker_env->isolate(), out_stream); + bool success = + node::SerializeHeapProfile(worker_env->isolate(), out_stream); env->SetImmediateThreadsafe( [taker = std::move(taker), out_stream = std::move(out_stream), diff --git a/src/node_worker.h b/src/node_worker.h index 3ad318803d8ae0..fd02b1d381edc1 100644 --- a/src/node_worker.h +++ b/src/node_worker.h @@ -8,6 +8,7 @@ #include "json_utils.h" #include "node_exit_code.h" #include "node_messaging.h" +#include "util.h" #include "uv.h" namespace node { diff --git a/src/util.cc b/src/util.cc index 1ea51cf7012963..2d03b8b0fe4f10 100644 --- a/src/util.cc +++ b/src/util.cc @@ -26,6 +26,7 @@ #include "debug_utils-inl.h" #include "env-inl.h" +#include "json_utils.h" #include "node_buffer.h" #include "node_errors.h" #include "node_internals.h" @@ -85,10 +86,13 @@ constexpr int kMaximumCopyMode = namespace node { +using v8::AllocationProfile; using v8::ArrayBuffer; using v8::ArrayBufferView; using v8::Context; using v8::FunctionTemplate; +using v8::HandleScope; +using v8::HeapProfiler; using v8::Isolate; using v8::Local; using v8::Object; @@ -812,4 +816,61 @@ v8::Maybe GetValidFileMode(Environment* env, return v8::Just(mode); } +static void buildHeapProfileNode(Isolate* isolate, + const AllocationProfile::Node* node, + JSONWriter* writer) { + size_t selfSize = 0; + for (const auto& allocation : node->allocations) + selfSize += allocation.size * allocation.count; + + writer->json_keyvalue("selfSize", selfSize); + writer->json_keyvalue("id", node->node_id); + writer->json_objectstart("callFrame"); + writer->json_keyvalue("scriptId", node->script_id); + writer->json_keyvalue("lineNumber", node->line_number - 1); + writer->json_keyvalue("columnNumber", node->column_number - 1); + Utf8Value name(isolate, node->name); + Utf8Value script_name(isolate, node->script_name); + writer->json_keyvalue("functionName", *name); + writer->json_keyvalue("url", *script_name); + writer->json_objectend(); + + writer->json_arraystart("children"); + for (const auto* child : node->children) { + writer->json_start(); + buildHeapProfileNode(isolate, child, writer); + writer->json_end(); + } + writer->json_arrayend(); +} + +bool SerializeHeapProfile(Isolate* isolate, std::ostringstream& out_stream) { + HandleScope scope(isolate); + HeapProfiler* profiler = isolate->GetHeapProfiler(); + std::unique_ptr profile(profiler->GetAllocationProfile()); + if (!profile) { + return false; + } + JSONWriter writer(out_stream, false); + writer.json_start(); + + writer.json_arraystart("samples"); + for (const auto& sample : profile->GetSamples()) { + writer.json_start(); + writer.json_keyvalue("size", sample.size * sample.count); + writer.json_keyvalue("nodeId", sample.node_id); + writer.json_keyvalue("ordinal", static_cast(sample.sample_id)); + writer.json_end(); + } + writer.json_arrayend(); + + writer.json_objectstart("head"); + buildHeapProfileNode(isolate, profile->GetRootNode(), &writer); + writer.json_objectend(); + + writer.json_end(); + profiler->StopSamplingHeapProfiler(); + return true; +} + } // namespace node diff --git a/src/util.h b/src/util.h index 873a204f9cd794..170eb60829a045 100644 --- a/src/util.h +++ b/src/util.h @@ -1066,6 +1066,7 @@ inline v8::Local Uint32ToString(v8::Local context, ->ToString(context) .ToLocalChecked(); } +bool SerializeHeapProfile(v8::Isolate* isolate, std::ostringstream& out_stream); } // namespace node diff --git a/test/parallel/test-v8-heap-profile.js b/test/parallel/test-v8-heap-profile.js new file mode 100644 index 00000000000000..84f50707d0a1ac --- /dev/null +++ b/test/parallel/test-v8-heap-profile.js @@ -0,0 +1,15 @@ +'use strict'; + +require('../common'); +const assert = require('assert'); +const v8 = require('v8'); + +const handle = v8.startHeapProfile(); +try { + v8.startHeapProfile(); +} catch (err) { + assert.strictEqual(err.code, 'ERR_HEAP_PROFILE_HAVE_BEEN_STARTED'); +} +const profile = handle.stop(); +assert.ok(typeof profile === 'string'); +assert.ok(profile.length > 0); From 5861bc1f25fe2d5e6f8800c3b3ad2e27aa037f14 Mon Sep 17 00:00:00 2001 From: ishabi Date: Sun, 15 Mar 2026 21:05:36 +0100 Subject: [PATCH 2/4] support heap sampling params --- doc/api/v8.md | 73 ++++++++++++++++++++++- doc/api/worker_threads.md | 40 ++++++++++++- lib/internal/v8/heap_profile.js | 49 +++++++++++++++ lib/internal/worker.js | 17 +++++- lib/v8.js | 23 ++++++- src/node_v8.cc | 41 ++++++++++++- src/node_worker.cc | 8 ++- src/util.cc | 25 +++++++- src/util.h | 10 ++++ test/parallel/test-v8-heap-profile.js | 71 +++++++++++++++++++--- test/parallel/test-worker-heap-profile.js | 44 +++++++++++++- 11 files changed, 378 insertions(+), 23 deletions(-) create mode 100644 lib/internal/v8/heap_profile.js diff --git a/doc/api/v8.md b/doc/api/v8.md index a3a461a2469f9a..6bb6229d802628 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1788,23 +1788,92 @@ const profile = handle.stop(); console.log(profile); ``` -## `v8.startHeapProfile()` +## `v8.heapProfilerConstants` +* {Object} + +A frozen object containing constants used with [`v8.startHeapProfile()`][]. + +The following constants are available: + +| Constant | Description | +| ------------------------------------------------ | --------------------------------------------------- | +| `SAMPLING_NO_FLAGS` | No flags. | +| `SAMPLING_FORCE_GC` | Force garbage collection before taking the profile. | +| `SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC` | Include objects collected by major GC. | +| `SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC` | Include objects collected by minor GC. | + +These constants can be combined using bitwise OR to pass as the `flags` +parameter. + +## `v8.startHeapProfile([sampleInterval[, stackDepth[, flags]]])` + + + +* `sampleInterval` {number} The average sampling interval in bytes. + **Default:** `524288` (512 KiB). +* `stackDepth` {integer} The maximum stack depth for samples. + **Default:** `16`. +* `flags` {integer} Flags to control sampling behavior. Use constants from + [`v8.heapProfilerConstants`][]. Multiple flags can be combined with bitwise + OR. **Default:** `v8.heapProfilerConstants.SAMPLING_NO_FLAGS`. * Returns: {SyncHeapProfileHandle} Starting a heap profile then return a `SyncHeapProfileHandle` object. This API supports `using` syntax. ```cjs +const v8 = require('node:v8'); + const handle = v8.startHeapProfile(); const profile = handle.stop(); console.log(profile); ``` +```mjs +import v8 from 'node:v8'; + +const handle = v8.startHeapProfile(); +const profile = handle.stop(); +console.log(profile); +``` + +With custom parameters: + +```cjs +const v8 = require('node:v8'); +const { heapProfilerConstants } = v8; + +const handle = v8.startHeapProfile( + 1024, + 8, + heapProfilerConstants.SAMPLING_FORCE_GC | + heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC, +); +const profile = handle.stop(); +console.log(profile); +``` + +```mjs +import v8 from 'node:v8'; +const { heapProfilerConstants } = v8; + +const handle = v8.startHeapProfile( + 1024, + 8, + heapProfilerConstants.SAMPLING_FORCE_GC | + heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC, +); +const profile = handle.stop(); +console.log(profile); +``` + [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 @@ -1839,6 +1908,8 @@ console.log(profile); [`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer [`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer [`settled` callback]: #settledpromise +[`v8.heapProfilerConstants`]: #v8heapprofilerconstants +[`v8.startHeapProfile()`]: #v8startheapprofilesampleinterval-stackdepth-flags [`v8.stopCoverage()`]: #v8stopcoverage [`v8.takeCoverage()`]: #v8takecoverage [`vm.Script`]: vm.md#new-vmscriptcode-options diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 32024e972932cd..79568ffb04b1c5 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -2005,7 +2005,7 @@ w.on('online', async () => { }); ``` -### `worker.startHeapProfile()` +### `worker.startHeapProfile([sampleInterval[, stackDepth[, flags]]])` +* `sampleInterval` {number} The average sampling interval in bytes. + **Default:** `524288` (512 KiB). +* `stackDepth` {integer} The maximum stack depth for samples. + **Default:** `16`. +* `flags` {integer} Flags to control sampling behavior. Use constants from + [`v8.heapProfilerConstants`][]. Multiple flags can be combined with bitwise + OR. **Default:** `v8.heapProfilerConstants.SAMPLING_NO_FLAGS`. * Returns: {Promise} Starting a Heap profile then return a Promise that fulfills with an error @@ -2034,6 +2041,22 @@ worker.on('online', async () => { }); ``` +```mjs +import { Worker } from 'node:worker_threads'; + +const worker = new Worker(` + const { parentPort } = require('node:worker_threads'); + parentPort.on('message', () => {}); + `, { eval: true }); + +worker.on('online', async () => { + const handle = await worker.startHeapProfile(); + const profile = await handle.stop(); + console.log(profile); + worker.terminate(); +}); +``` + `await using` example. ```cjs @@ -2050,6 +2073,20 @@ w.on('online', async () => { }); ``` +```mjs +import { Worker } from 'node:worker_threads'; + +const w = new Worker(` + const { parentPort } = require('node:worker_threads'); + parentPort.on('message', () => {}); + `, { eval: true }); + +w.on('online', async () => { + // Stop profile automatically when return and profile will be discarded + await using handle = await w.startHeapProfile(); +}); +``` + ### `worker.stderr` -* {Object} - -A frozen object containing constants used with [`v8.startHeapProfile()`][]. - -The following constants are available: - -| Constant | Description | -| ------------------------------------------------ | --------------------------------------------------- | -| `SAMPLING_NO_FLAGS` | No flags. | -| `SAMPLING_FORCE_GC` | Force garbage collection before taking the profile. | -| `SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC` | Include objects collected by major GC. | -| `SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC` | Include objects collected by minor GC. | - -These constants can be combined using bitwise OR to pass as the `flags` -parameter. - -## `v8.startHeapProfile([sampleInterval[, stackDepth[, flags]]])` - - - -* `sampleInterval` {number} The average sampling interval in bytes. - **Default:** `524288` (512 KiB). -* `stackDepth` {integer} The maximum stack depth for samples. - **Default:** `16`. -* `flags` {integer} Flags to control sampling behavior. Use constants from - [`v8.heapProfilerConstants`][]. Multiple flags can be combined with bitwise - OR. **Default:** `v8.heapProfilerConstants.SAMPLING_NO_FLAGS`. +* `options` {Object} + * `sampleInterval` {number} The average sampling interval in bytes. + **Default:** `524288` (512 KiB). + * `stackDepth` {integer} The maximum stack depth for samples. + **Default:** `16`. + * `forceGC` {boolean} Force garbage collection before taking the profile. + **Default:** `false`. + * `includeObjectsCollectedByMajorGC` {boolean} Include objects collected + by major GC. **Default:** `false`. + * `includeObjectsCollectedByMinorGC` {boolean} Include objects collected + by minor GC. **Default:** `false`. * Returns: {SyncHeapProfileHandle} Starting a heap profile then return a `SyncHeapProfileHandle` object. @@ -1848,28 +1830,26 @@ With custom parameters: ```cjs const v8 = require('node:v8'); -const { heapProfilerConstants } = v8; - -const handle = v8.startHeapProfile( - 1024, - 8, - heapProfilerConstants.SAMPLING_FORCE_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC, -); + +const handle = v8.startHeapProfile({ + sampleInterval: 1024, + stackDepth: 8, + forceGC: true, + includeObjectsCollectedByMajorGC: true, +}); const profile = handle.stop(); console.log(profile); ``` ```mjs import v8 from 'node:v8'; -const { heapProfilerConstants } = v8; - -const handle = v8.startHeapProfile( - 1024, - 8, - heapProfilerConstants.SAMPLING_FORCE_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC, -); + +const handle = v8.startHeapProfile({ + sampleInterval: 1024, + stackDepth: 8, + forceGC: true, + includeObjectsCollectedByMajorGC: true, +}); const profile = handle.stop(); console.log(profile); ``` @@ -1908,8 +1888,6 @@ console.log(profile); [`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer [`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer [`settled` callback]: #settledpromise -[`v8.heapProfilerConstants`]: #v8heapprofilerconstants -[`v8.startHeapProfile()`]: #v8startheapprofilesampleinterval-stackdepth-flags [`v8.stopCoverage()`]: #v8stopcoverage [`v8.takeCoverage()`]: #v8takecoverage [`vm.Script`]: vm.md#new-vmscriptcode-options diff --git a/doc/api/worker_threads.md b/doc/api/worker_threads.md index 79568ffb04b1c5..6fee0d8368a914 100644 --- a/doc/api/worker_threads.md +++ b/doc/api/worker_threads.md @@ -2005,7 +2005,7 @@ w.on('online', async () => { }); ``` -### `worker.startHeapProfile([sampleInterval[, stackDepth[, flags]]])` +### `worker.startHeapProfile([options])` -* `sampleInterval` {number} The average sampling interval in bytes. - **Default:** `524288` (512 KiB). -* `stackDepth` {integer} The maximum stack depth for samples. - **Default:** `16`. -* `flags` {integer} Flags to control sampling behavior. Use constants from - [`v8.heapProfilerConstants`][]. Multiple flags can be combined with bitwise - OR. **Default:** `v8.heapProfilerConstants.SAMPLING_NO_FLAGS`. +* `options` {Object} + * `sampleInterval` {number} The average sampling interval in bytes. + **Default:** `524288` (512 KiB). + * `stackDepth` {integer} The maximum stack depth for samples. + **Default:** `16`. + * `forceGC` {boolean} Force garbage collection before taking the profile. + **Default:** `false`. + * `includeObjectsCollectedByMajorGC` {boolean} Include objects collected + by major GC. **Default:** `false`. + * `includeObjectsCollectedByMinorGC` {boolean} Include objects collected + by minor GC. **Default:** `false`. * Returns: {Promise} Starting a Heap profile then return a Promise that fulfills with an error @@ -2303,7 +2307,6 @@ thread spawned will spawn another until the application crashes. [`trace_events`]: tracing.md [`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshotoptions [`v8.getHeapStatistics()`]: v8.md#v8getheapstatistics -[`v8.heapProfilerConstants`]: v8.md#v8heapprofilerconstants [`vm`]: vm.md [`worker.SHARE_ENV`]: #worker_threadsshare_env [`worker.on('message')`]: #event-message_1 diff --git a/lib/internal/v8/heap_profile.js b/lib/internal/v8/heap_profile.js index db3eb2f02a044d..ea4679b6c05704 100644 --- a/lib/internal/v8/heap_profile.js +++ b/lib/internal/v8/heap_profile.js @@ -1,12 +1,10 @@ 'use strict'; const { - ObjectFreeze, -} = primordials; - -const { + validateBoolean, validateInteger, validateInt32, + validateObject, } = require('internal/validators'); const { @@ -16,34 +14,36 @@ const { kSamplingIncludeObjectsCollectedByMinorGC, } = internalBinding('v8'); -const heapProfilerConstants = { - __proto__: null, - SAMPLING_NO_FLAGS: kSamplingNoFlags, - SAMPLING_FORCE_GC: kSamplingForceGC, - SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC: - kSamplingIncludeObjectsCollectedByMajorGC, - SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC: - kSamplingIncludeObjectsCollectedByMinorGC, -}; -ObjectFreeze(heapProfilerConstants); +function normalizeHeapProfileOptions(options = {}) { + validateObject(options, 'options'); + const { + sampleInterval = 512 * 1024, + stackDepth = 16, + forceGC = false, + includeObjectsCollectedByMajorGC = false, + includeObjectsCollectedByMinorGC = false, + } = options; + + validateInteger(sampleInterval, 'options.sampleInterval', 1); + validateInt32(stackDepth, 'options.stackDepth', 0); + validateBoolean(forceGC, 'options.forceGC'); + validateBoolean(includeObjectsCollectedByMajorGC, + 'options.includeObjectsCollectedByMajorGC'); + validateBoolean(includeObjectsCollectedByMinorGC, + 'options.includeObjectsCollectedByMinorGC'); -const kMaxSamplingFlags = - heapProfilerConstants.SAMPLING_FORCE_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC; + let flags = kSamplingNoFlags; + if (forceGC) flags |= kSamplingForceGC; + if (includeObjectsCollectedByMajorGC) { + flags |= kSamplingIncludeObjectsCollectedByMajorGC; + } + if (includeObjectsCollectedByMinorGC) { + flags |= kSamplingIncludeObjectsCollectedByMinorGC; + } -function normalizeHeapProfileOptions( - sampleInterval = 512 * 1024, - stackDepth = 16, - flags = heapProfilerConstants.SAMPLING_NO_FLAGS, -) { - validateInteger(sampleInterval, 'sampleInterval', 1); - validateInt32(stackDepth, 'stackDepth', 0); - validateInt32(flags, 'flags', 0, kMaxSamplingFlags); return { sampleInterval, stackDepth, flags }; } module.exports = { - heapProfilerConstants, normalizeHeapProfileOptions, }; diff --git a/lib/internal/worker.js b/lib/internal/worker.js index 3904a01a3b5b1b..ac5f33b44c2e5c 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -590,9 +590,18 @@ class Worker extends EventEmitter { }); } - startHeapProfile(sampleInterval, stackDepth, flags) { - ({ sampleInterval, stackDepth, flags } = - normalizeHeapProfileOptions(sampleInterval, stackDepth, flags)); + /** + * @param {object} [options] + * @param {number} [options.sampleInterval] + * @param {number} [options.stackDepth] + * @param {boolean} [options.forceGC] + * @param {boolean} [options.includeObjectsCollectedByMajorGC] + * @param {boolean} [options.includeObjectsCollectedByMinorGC] + * @returns {Promise} + */ + startHeapProfile(options) { + const { sampleInterval, stackDepth, flags } = + normalizeHeapProfileOptions(options); const startTaker = this[kHandle]?.startHeapProfile( sampleInterval, stackDepth, flags); return new Promise((resolve, reject) => { diff --git a/lib/v8.js b/lib/v8.js index 3d5005bd80fc18..4bea3a9f1d3fbb 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -26,7 +26,6 @@ const { Int32Array, Int8Array, JSONParse, - ObjectDefineProperty, ObjectPrototypeToString, SymbolDispose, Uint16Array, @@ -52,7 +51,6 @@ const { namespace: startupSnapshot, } = require('internal/v8/startup_snapshot'); const { - heapProfilerConstants, normalizeHeapProfileOptions, } = require('internal/v8/heap_profile'); @@ -225,14 +223,17 @@ function startCpuProfile() { /** * Starting Heap Profile. - * @param {number} [sampleInterval] - * @param {number} [stackDepth] - * @param {number} [flags] + * @param {object} [options] + * @param {number} [options.sampleInterval] + * @param {number} [options.stackDepth] + * @param {boolean} [options.forceGC] + * @param {boolean} [options.includeObjectsCollectedByMajorGC] + * @param {boolean} [options.includeObjectsCollectedByMinorGC] * @returns {SyncHeapProfileHandle} */ -function startHeapProfile(sampleInterval, stackDepth, flags) { - ({ sampleInterval, stackDepth, flags } = - normalizeHeapProfileOptions(sampleInterval, stackDepth, flags)); +function startHeapProfile(options) { + const { sampleInterval, stackDepth, flags } = + normalizeHeapProfileOptions(options); _startHeapProfile(sampleInterval, stackDepth, flags); return new SyncHeapProfileHandle(); } @@ -557,10 +558,3 @@ module.exports = { startCpuProfile, startHeapProfile, }; - -ObjectDefineProperty(module.exports, 'heapProfilerConstants', { - __proto__: null, - configurable: false, - enumerable: true, - value: heapProfilerConstants, -}); diff --git a/src/util.cc b/src/util.cc index 9adca48e53af6e..6d08d8e71145a1 100644 --- a/src/util.cc +++ b/src/util.cc @@ -816,7 +816,7 @@ v8::Maybe GetValidFileMode(Environment* env, return v8::Just(mode); } -static void buildHeapProfileNode(Isolate* isolate, +static void BuildHeapProfileNode(Isolate* isolate, const AllocationProfile::Node* node, JSONWriter* writer) { size_t selfSize = 0; @@ -838,7 +838,7 @@ static void buildHeapProfileNode(Isolate* isolate, writer->json_arraystart("children"); for (const auto* child : node->children) { writer->json_start(); - buildHeapProfileNode(isolate, child, writer); + BuildHeapProfileNode(isolate, child, writer); writer->json_end(); } writer->json_arrayend(); @@ -866,7 +866,7 @@ bool SerializeHeapProfile(Isolate* isolate, std::ostringstream& out_stream) { writer.json_arrayend(); writer.json_objectstart("head"); - buildHeapProfileNode(isolate, profile->GetRootNode(), &writer); + BuildHeapProfileNode(isolate, profile->GetRootNode(), &writer); writer.json_objectend(); writer.json_end(); diff --git a/test/parallel/test-v8-heap-profile.js b/test/parallel/test-v8-heap-profile.js index 0445036ebba0f4..d1c611212509d4 100644 --- a/test/parallel/test-v8-heap-profile.js +++ b/test/parallel/test-v8-heap-profile.js @@ -3,41 +3,46 @@ require('../common'); const assert = require('assert'); const v8 = require('v8'); -const { heapProfilerConstants } = v8; -assert.ok(Object.isFrozen(heapProfilerConstants)); -assert.strictEqual(typeof heapProfilerConstants.SAMPLING_NO_FLAGS, 'number'); -assert.strictEqual(typeof heapProfilerConstants.SAMPLING_FORCE_GC, 'number'); -assert.strictEqual( - typeof heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC, - 'number'); -assert.strictEqual( - typeof heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC, - 'number'); +assert.throws(() => v8.startHeapProfile('bad'), { + code: 'ERR_INVALID_ARG_TYPE', +}); -function assertInvalidStartHeapProfile(args, code) { - assert.throws(() => Reflect.apply(v8.startHeapProfile, undefined, args), { - code, - }); - // Verify the invalid call above did not accidentally start profiling. - const handle = v8.startHeapProfile(); - const profile = handle.stop(); - assert.ok(typeof profile === 'string'); - assert.ok(profile.length > 0); -} +assert.throws(() => v8.startHeapProfile({ sampleInterval: '1024' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => v8.startHeapProfile({ sampleInterval: 1.1 }), { + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => v8.startHeapProfile({ sampleInterval: 0 }), { + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => v8.startHeapProfile({ sampleInterval: -1 }), { + code: 'ERR_OUT_OF_RANGE', +}); -assertInvalidStartHeapProfile(['1024'], 'ERR_INVALID_ARG_TYPE'); -assertInvalidStartHeapProfile([1.1], 'ERR_OUT_OF_RANGE'); -assertInvalidStartHeapProfile([0], 'ERR_OUT_OF_RANGE'); -assertInvalidStartHeapProfile([-1], 'ERR_OUT_OF_RANGE'); -assertInvalidStartHeapProfile([1024, '16'], 'ERR_INVALID_ARG_TYPE'); -assertInvalidStartHeapProfile([1024, 1.1], 'ERR_OUT_OF_RANGE'); -assertInvalidStartHeapProfile([1024, -1], 'ERR_OUT_OF_RANGE'); +assert.throws(() => v8.startHeapProfile({ stackDepth: '16' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => v8.startHeapProfile({ stackDepth: 1.1 }), { + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => v8.startHeapProfile({ stackDepth: -1 }), { + code: 'ERR_OUT_OF_RANGE', +}); -assertInvalidStartHeapProfile([1024, 16, '0'], 'ERR_INVALID_ARG_TYPE'); -assertInvalidStartHeapProfile([1024, 16, -1], 'ERR_OUT_OF_RANGE'); -assertInvalidStartHeapProfile([1024, 16, 8], 'ERR_OUT_OF_RANGE'); +assert.throws(() => v8.startHeapProfile({ forceGC: 'true' }), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws( + () => v8.startHeapProfile({ includeObjectsCollectedByMajorGC: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', + }); +assert.throws( + () => v8.startHeapProfile({ includeObjectsCollectedByMinorGC: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', + }); // Default params. { @@ -46,15 +51,15 @@ assertInvalidStartHeapProfile([1024, 16, 8], 'ERR_OUT_OF_RANGE'); JSON.parse(profile); } -// Custom params. +// Custom params with all flags. { - const handle = v8.startHeapProfile( - 1024, - 8, - heapProfilerConstants.SAMPLING_FORCE_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC, - ); + const handle = v8.startHeapProfile({ + sampleInterval: 1024, + stackDepth: 8, + forceGC: true, + includeObjectsCollectedByMajorGC: true, + includeObjectsCollectedByMinorGC: true, + }); assert.throws(() => v8.startHeapProfile(), { code: 'ERR_HEAP_PROFILE_HAVE_BEEN_STARTED', }); diff --git a/test/parallel/test-worker-heap-profile.js b/test/parallel/test-worker-heap-profile.js index 15440f052006f5..2a466dc2f186e1 100644 --- a/test/parallel/test-worker-heap-profile.js +++ b/test/parallel/test-worker-heap-profile.js @@ -1,9 +1,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const v8 = require('v8'); const { Worker } = require('worker_threads'); -const { heapProfilerConstants } = v8; const worker = new Worker(` const { parentPort } = require('worker_threads'); @@ -11,50 +9,55 @@ const worker = new Worker(` `, { eval: true }); worker.on('online', common.mustCall(async () => { - assert.throws(() => worker.startHeapProfile('1024'), { + assert.throws(() => worker.startHeapProfile('bad'), { code: 'ERR_INVALID_ARG_TYPE', }); - assert.throws(() => worker.startHeapProfile(1.1), { + + assert.throws(() => worker.startHeapProfile({ sampleInterval: '1024' }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + assert.throws(() => worker.startHeapProfile({ sampleInterval: 1.1 }), { code: 'ERR_OUT_OF_RANGE', }); - assert.throws(() => worker.startHeapProfile(0), { + assert.throws(() => worker.startHeapProfile({ sampleInterval: 0 }), { code: 'ERR_OUT_OF_RANGE', }); - assert.throws(() => worker.startHeapProfile(-1), { + assert.throws(() => worker.startHeapProfile({ sampleInterval: -1 }), { code: 'ERR_OUT_OF_RANGE', }); - assert.throws(() => worker.startHeapProfile(1024, '16'), { + assert.throws(() => worker.startHeapProfile({ stackDepth: '16' }), { code: 'ERR_INVALID_ARG_TYPE', }); - assert.throws(() => worker.startHeapProfile(1024, 1.1), { + assert.throws(() => worker.startHeapProfile({ stackDepth: 1.1 }), { code: 'ERR_OUT_OF_RANGE', }); - assert.throws(() => worker.startHeapProfile(1024, -1), { + assert.throws(() => worker.startHeapProfile({ stackDepth: -1 }), { code: 'ERR_OUT_OF_RANGE', }); - - assert.throws(() => worker.startHeapProfile(1024, 16, '0'), { + assert.throws(() => worker.startHeapProfile({ forceGC: 'true' }), { code: 'ERR_INVALID_ARG_TYPE', }); - assert.throws(() => worker.startHeapProfile(1024, 16, -1), { - code: 'ERR_OUT_OF_RANGE', - }); - assert.throws(() => worker.startHeapProfile(1024, 16, 8), { - code: 'ERR_OUT_OF_RANGE', - }); + assert.throws( + () => worker.startHeapProfile({ includeObjectsCollectedByMajorGC: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', + }); + assert.throws( + () => worker.startHeapProfile({ includeObjectsCollectedByMinorGC: 1 }), { + code: 'ERR_INVALID_ARG_TYPE', + }); { - const handle = await worker.startHeapProfile( - 1024, - 8, - heapProfilerConstants.SAMPLING_FORCE_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MAJOR_GC | - heapProfilerConstants.SAMPLING_INCLUDE_OBJECTS_COLLECTED_BY_MINOR_GC, - ); + const handle = await worker.startHeapProfile({ + sampleInterval: 1024, + stackDepth: 8, + forceGC: true, + includeObjectsCollectedByMajorGC: true, + includeObjectsCollectedByMinorGC: true, + }); JSON.parse(await handle.stop()); - // Stop again + // Stop again returns cached result. JSON.parse(await handle.stop()); } From 4cc95a5b9961acc7e2c531dea3c160552e8ca725 Mon Sep 17 00:00:00 2001 From: ishabi Date: Tue, 17 Mar 2026 15:09:00 +0100 Subject: [PATCH 4/4] load heap_profile module in worker --- lib/internal/worker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/worker.js b/lib/internal/worker.js index ac5f33b44c2e5c..0b0d91171f0ba5 100644 --- a/lib/internal/worker.js +++ b/lib/internal/worker.js @@ -71,9 +71,7 @@ const { validateObject, validateNumber, } = require('internal/validators'); -const { - normalizeHeapProfileOptions, -} = require('internal/v8/heap_profile'); +let normalizeHeapProfileOptions; const { throwIfBuildingSnapshot, } = require('internal/v8/startup_snapshot'); @@ -600,6 +598,8 @@ class Worker extends EventEmitter { * @returns {Promise} */ startHeapProfile(options) { + normalizeHeapProfileOptions ??= + require('internal/v8/heap_profile').normalizeHeapProfileOptions; const { sampleInterval, stackDepth, flags } = normalizeHeapProfileOptions(options); const startTaker = this[kHandle]?.startHeapProfile(