Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,30 @@ added: REPLACEME

Stopping collecting the profile and the profile will be discarded.

## Class: `SyncHeapProfileHandle`

<!-- YAML
added: REPLACEME
-->

### `syncHeapProfileHandle.stop()`

<!-- YAML
added: REPLACEME
-->

* Returns: {string}

Stopping collecting the profile and return the profile data.

### `syncHeapProfileHandle[Symbol.dispose]()`

<!-- YAML
added: REPLACEME
-->

Stopping collecting the profile and the profile will be discarded.

## Class: `CPUProfileHandle`

<!-- YAML
Expand Down Expand Up @@ -1534,6 +1558,23 @@ const profile = handle.stop();
console.log(profile);
```

## `v8.startHeapProfile()`

<!-- YAML
added: REPLACEME
-->

* 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
Expand Down
28 changes: 28 additions & 0 deletions lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ const {
setFlagsFromString: _setFlagsFromString,
startCpuProfile: _startCpuProfile,
stopCpuProfile: _stopCpuProfile,
startHeapProfile: _startHeapProfile,
stopHeapProfile: _stopHeapProfile,
isStringOneByteRepresentation: _isStringOneByteRepresentation,
updateHeapStatisticsBuffer,
updateHeapSpaceStatisticsBuffer,
Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down Expand Up @@ -512,4 +539,5 @@ module.exports = {
GCProfiler,
isStringOneByteRepresentation,
startCpuProfile,
startHeapProfile,
};
28 changes: 28 additions & 0 deletions src/node_v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,30 @@ void StopCpuProfile(const FunctionCallbackInfo<Value>& args) {
}
}

void StartHeapProfile(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
if (isolate->GetHeapProfiler()->StartSamplingHeapProfiler()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

StartSamplingHeapProfiler returns false if a sampling heap profiler is already running, we should check the value if false we stop this operation.
https://v8docs.nodesource.com/node-6.17/d7/d76/classv8_1_1_heap_profiler.html#ada00bb7ec0b7827ce97280a1fb6e1f64

Copy link
Contributor Author

@theanarkh theanarkh Oct 13, 2025

Choose a reason for hiding this comment

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

Thanks for reviewing ! An Error will be thrown in the following code when false is returned.

return;
}
THROW_ERR_HEAP_PROFILE_HAVE_BEEN_STARTED(isolate,
"Heap profile has been started");
}

void StopHeapProfile(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();
std::ostringstream out_stream;
bool success = node::SerializeHeapProfile(isolate, out_stream);
if (success) {
Local<Value> 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<Value>& args) {
CHECK_EQ(args.Length(), 1);
Expand Down Expand Up @@ -739,6 +763,8 @@ void Initialize(Local<Object> 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,
Expand Down Expand Up @@ -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
Expand Down
62 changes: 2 additions & 60 deletions src/node_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

using node::kAllowedInEnvvar;
using node::kDisallowedInEnvvar;
using v8::AllocationProfile;
using v8::Array;
using v8::ArrayBuffer;
using v8::Boolean;
Expand All @@ -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;
Expand Down Expand Up @@ -1087,63 +1085,6 @@ void Worker::StartHeapProfile(const FunctionCallbackInfo<Value>& 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<AllocationProfile> 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<double>(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<Value>& args) {
Worker* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.This());
Expand All @@ -1163,7 +1104,8 @@ void Worker::StopHeapProfile(const FunctionCallbackInfo<Value>& 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),
Expand Down
1 change: 1 addition & 0 deletions src/node_worker.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
61 changes: 61 additions & 0 deletions src/util.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -902,4 +906,61 @@ v8::Maybe<int> 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<AllocationProfile> 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<double>(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
2 changes: 2 additions & 0 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -1055,6 +1055,8 @@ inline v8::MaybeLocal<v8::Object> NewDictionaryInstanceNullProto(
v8::Local<v8::DictionaryTemplate> tmpl,
v8::MemorySpan<v8::MaybeLocal<v8::Value>> property_values);

bool SerializeHeapProfile(v8::Isolate* isolate, std::ostringstream& out_stream);

} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
Expand Down
15 changes: 15 additions & 0 deletions test/parallel/test-v8-heap-profile.js
Original file line number Diff line number Diff line change
@@ -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);
1 change: 1 addition & 0 deletions tools/doc/type-parser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /(?:\[])+$/;
Expand Down
Loading