diff --git a/doc/api/v8.md b/doc/api/v8.md index ff4b5614f5c173..0800b8b08df316 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1422,6 +1422,30 @@ added: REPLACEME 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 47f694103719aa..c2e029e744042b 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, @@ -190,6 +192,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} @@ -199,6 +217,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 @@ -512,4 +539,5 @@ module.exports = { GCProfiler, isStringOneByteRepresentation, startCpuProfile, + startHeapProfile, }; diff --git a/src/node_v8.cc b/src/node_v8.cc index ce253d7fa0a8b1..0681cfb018146d 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -279,6 +279,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); @@ -739,6 +763,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, @@ -786,6 +812,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 8d878855706ac1..f455b856a9cffa 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; @@ -1087,63 +1085,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; - } - 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; -} - void Worker::StopHeapProfile(const FunctionCallbackInfo& args) { Worker* w; ASSIGN_OR_RETURN_UNWRAP(&w, args.This()); @@ -1163,7 +1104,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 873029096c465e..7fb09010324d16 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 c4b39450c5b7f9..19834bfb54793d 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; @@ -902,4 +906,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 6dfccf3a342da3..d2d78640c70035 100644 --- a/src/util.h +++ b/src/util.h @@ -1055,6 +1055,8 @@ inline v8::MaybeLocal NewDictionaryInstanceNullProto( v8::Local tmpl, v8::MemorySpan> property_values); +bool SerializeHeapProfile(v8::Isolate* isolate, std::ostringstream& out_stream); + } // namespace node #endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS 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); diff --git a/tools/doc/type-parser.mjs b/tools/doc/type-parser.mjs index 599de750d6b129..642ab581a264b1 100644 --- a/tools/doc/type-parser.mjs +++ b/tools/doc/type-parser.mjs @@ -353,6 +353,7 @@ const customTypesMap = { 'CPUProfileHandle': 'v8.html#class-cpuprofilehandle', 'HeapProfileHandle': 'v8.html#class-heapprofilehandle', 'SyncCPUProfileHandle': 'v8.html#class-synccpuprofilehandle', + 'SyncHeapProfileHandle': 'v8.html#class-syncheapprofilehandle', }; const arrayPart = /(?:\[])+$/;