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
35 changes: 35 additions & 0 deletions doc/api/v8.md
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,39 @@ const profile = handle.stop();
console.log(profile);
```

## `v8.setHeapProfileNearHeapLimit(limit)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

* `limit` {integer}

Writes the active sampling heap profile as a `.heapprofile` file when V8 is
near the heap limit, up to `limit` times. Files are written to the directory
configured by `--diagnostic-dir` (or to the current working directory if the
option is unset), using the
`Heap.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.heapprofile` naming
convention.

[`v8.startHeapProfile()`][] must be called before this API can capture a
Comment thread
jasnell marked this conversation as resolved.
profile. If no sampling heap profile is active when V8 reaches the heap
limit, the callback is uninstalled and V8 continues its normal
out-of-memory behavior.

This API can be used together with [`v8.setHeapSnapshotNearHeapLimit()`][].
Each API maintains its own `limit`.

Writing is best-effort. Materializing the sampled call tree can allocate V8
and native memory, so the profile might not be completed if the process has
insufficient memory available. The serialized profile is written by native code
and is not exposed to JavaScript.

Calling `setHeapProfileNearHeapLimit()` more than once is a no-op. `limit`
must be a positive integer.

[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 Expand Up @@ -1887,6 +1920,8 @@ console.log(profile);
[`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer
[`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer
[`settled` callback]: #settledpromise
[`v8.setHeapSnapshotNearHeapLimit()`]: #v8setheapsnapshotnearheaplimitlimit
[`v8.startHeapProfile()`]: #v8startheapprofileoptions
[`v8.stopCoverage()`]: #v8stopcoverage
[`v8.takeCoverage()`]: #v8takecoverage
[`vm.Script`]: vm.md#new-vmscriptcode-options
Expand Down
12 changes: 12 additions & 0 deletions lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const {
updateHeapSpaceStatisticsBuffer,
updateHeapCodeStatisticsBuffer,
setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit,
setHeapProfileNearHeapLimit: _setHeapProfileNearHeapLimit,

// Properties for heap statistics buffer extraction.
kTotalHeapSizeIndex,
Expand Down Expand Up @@ -360,6 +361,16 @@ function setHeapSnapshotNearHeapLimit(limit) {
_setHeapSnapshotNearHeapLimit(limit);
}

let heapProfileNearHeapLimitCallbackAdded = false;
function setHeapProfileNearHeapLimit(limit) {
validateUint32(limit, 'limit', true);
if (heapProfileNearHeapLimitCallbackAdded) {
return;
}
heapProfileNearHeapLimitCallbackAdded = true;
_setHeapProfileNearHeapLimit(limit);
}

const detailLevelDict = {
__proto__: null,
detailed: detailLevel.DETAILED,
Expand Down Expand Up @@ -563,6 +574,7 @@ module.exports = {
queryObjects,
startupSnapshot,
setHeapSnapshotNearHeapLimit,
setHeapProfileNearHeapLimit,
GCProfiler,
isStringOneByteRepresentation,
startCpuProfile,
Expand Down
40 changes: 37 additions & 3 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -905,26 +905,60 @@ inline void Environment::set_heap_snapshot_near_heap_limit(uint32_t limit) {
heap_snapshot_near_heap_limit_ = limit;
}

inline void Environment::set_heap_profile_near_heap_limit(uint32_t limit) {
heap_profile_near_heap_limit_ = limit;
}

inline bool Environment::is_in_heapsnapshot_heap_limit_callback() const {
return is_in_heapsnapshot_heap_limit_callback_;
}

inline bool Environment::is_in_heap_profile_near_heap_limit_callback() const {
return is_in_heap_profile_near_heap_limit_callback_;
}

inline bool Environment::report_exclude_env() const {
return options_->report_exclude_env;
}

inline void Environment::AddHeapSnapshotNearHeapLimitCallback() {
DCHECK(!heapsnapshot_near_heap_limit_callback_added_);
const bool was_registered = heap_profile_near_heap_limit_callback_added_;
heapsnapshot_near_heap_limit_callback_added_ = true;
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback, this);
if (!was_registered) {
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
this);
}
}

inline void Environment::RemoveHeapSnapshotNearHeapLimitCallback(
size_t heap_limit) {
DCHECK(heapsnapshot_near_heap_limit_callback_added_);
heapsnapshot_near_heap_limit_callback_added_ = false;
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
heap_limit);
if (!heap_profile_near_heap_limit_callback_added_) {
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
heap_limit);
}
}

inline void Environment::AddHeapProfileNearHeapLimitCallback() {
DCHECK(!heap_profile_near_heap_limit_callback_added_);
const bool was_registered = heapsnapshot_near_heap_limit_callback_added_;
heap_profile_near_heap_limit_callback_added_ = true;
if (!was_registered) {
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
this);
}
}

inline void Environment::RemoveHeapProfileNearHeapLimitCallback(
size_t heap_limit) {
DCHECK(heap_profile_near_heap_limit_callback_added_);
heap_profile_near_heap_limit_callback_added_ = false;
if (!heapsnapshot_near_heap_limit_callback_added_) {
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
heap_limit);
}
}

} // namespace node
Expand Down
122 changes: 121 additions & 1 deletion src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
#include "node_context_data.h"
#include "node_contextify.h"
#include "node_errors.h"
#include "node_file_utils.h"
#include "node_internals.h"
#include "node_options-inl.h"
#include "node_process-inl.h"
#include "node_profiling.h"
#include "node_shadow_realm.h"
#include "node_snapshotable.h"
#include "node_v8_platform-inl.h"
Expand All @@ -33,6 +35,7 @@
#include <limits>
#include <memory>
#include <optional>
#include <sstream>
#include <unordered_map>

namespace node {
Expand Down Expand Up @@ -1047,6 +1050,9 @@ Environment::~Environment() {
if (heapsnapshot_near_heap_limit_callback_added_) {
RemoveHeapSnapshotNearHeapLimitCallback(0);
}
if (heap_profile_near_heap_limit_callback_added_) {
RemoveHeapProfileNearHeapLimitCallback(0);
}

isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback(
BuildEmbedderGraph, this);
Expand Down Expand Up @@ -2098,14 +2104,49 @@ void Environment::TracePromises(PromiseHookType type,
PrintCurrentStackTrace(isolate);
}

// V8 only invokes the most recently registered near-heap-limit callback.
// To allow the snapshot and profile handlers to coexist on the same
// isolate, both register through this single V8-facing entry point. It fans
// out to whichever sub-handlers are active and returns the largest requested
// heap limit.
size_t Environment::NearHeapLimitCallback(void* data,
size_t current_heap_limit,
size_t initial_heap_limit) {
auto* env = static_cast<Environment*>(data);

// Profile and snapshot generation can allocate and invoke this callback
// recursively. Only extend the heap for the operation already in progress;
// do not start the other diagnostic from the nested invocation.
if (env->is_in_heapsnapshot_heap_limit_callback_) {
return HeapSnapshotNearHeapLimitCallback(
data, current_heap_limit, initial_heap_limit);
}
if (env->is_in_heap_profile_near_heap_limit_callback_) {
return HeapProfileNearHeapLimitCallback(
data, current_heap_limit, initial_heap_limit);
}

size_t new_limit = current_heap_limit;
if (env->heapsnapshot_near_heap_limit_callback_added_) {
new_limit = std::max(new_limit,
HeapSnapshotNearHeapLimitCallback(
data, current_heap_limit, initial_heap_limit));
}
if (env->heap_profile_near_heap_limit_callback_added_) {
new_limit = std::max(new_limit,
HeapProfileNearHeapLimitCallback(
data, current_heap_limit, initial_heap_limit));
}
return new_limit;
}

size_t Environment::HeapSnapshotNearHeapLimitCallback(
void* data, size_t current_heap_limit, size_t initial_heap_limit) {
auto* env = static_cast<Environment*>(data);

Debug(env,
DebugCategory::DIAGNOSTICS,
"Invoked NearHeapLimitCallback, processing=%d, "
"Invoked HeapSnapshotNearHeapLimitCallback, processing=%d, "
"current_limit=%" PRIu64 ", "
"initial_limit=%" PRIu64 "\n",
env->is_in_heapsnapshot_heap_limit_callback_,
Expand Down Expand Up @@ -2227,6 +2268,85 @@ size_t Environment::NearHeapLimitCallback(void* data,
return new_limit;
}

size_t Environment::HeapProfileNearHeapLimitCallback(
void* data, size_t current_heap_limit, size_t initial_heap_limit) {
auto* env = static_cast<Environment*>(data);

Debug(env,
DebugCategory::DIAGNOSTICS,
"Invoked HeapProfileNearHeapLimitCallback, processing=%d, "
"current_limit=%" PRIu64 ", "
"initial_limit=%" PRIu64 "\n",
env->is_in_heap_profile_near_heap_limit_callback_,
static_cast<uint64_t>(current_heap_limit),
static_cast<uint64_t>(initial_heap_limit));

const size_t max_young_gen_size = env->isolate_data()->max_young_gen_size;
const size_t new_limit = current_heap_limit + max_young_gen_size;

if (env->is_in_heap_profile_near_heap_limit_callback_) {
return new_limit;
}

env->is_in_heap_profile_near_heap_limit_callback_ = true;
auto reset_in_callback = OnScopeLeave(
[env]() { env->is_in_heap_profile_near_heap_limit_callback_ = false; });
auto restore_initial_limit = OnScopeLeave(
[env]() { env->isolate()->AutomaticallyRestoreInitialHeapLimit(0.95); });
auto uninstall_callback = [env]() {
env->RemoveHeapProfileNearHeapLimitCallback(0);
};

std::string dir = env->options()->diagnostic_dir;
if (dir.empty()) {
dir = Environment::GetCwd(env->exec_path_);
}
DiagnosticFilename name(env, "Heap", "heapprofile");
std::string filename = dir + kPathSeparator + (*name);

Debug(env, DebugCategory::DIAGNOSTICS, "Writing %s...\n", *name);

std::ostringstream out;
if (!node::SerializeHeapProfile(env->isolate(), out)) {
Debug(env,
DebugCategory::DIAGNOSTICS,
"No sampling heap profile active; uninstalling callback.\n");
uninstall_callback();
return new_limit;
}

std::string profile = std::move(out).str();
uv_buf_t buffer = uv_buf_init(profile.data(), profile.size());
const int err = WriteFileSync(filename.c_str(), buffer);
if (err != 0) {
FPrintF(stderr,
"Failed to write heap profile %s: %s\n",
filename,
uv_strerror(err));
uninstall_callback();
return new_limit;
}

env->heap_limit_profile_taken_ += 1;
FPrintF(stderr, "Wrote heap profile to %s\n", filename);

Debug(env,
DebugCategory::DIAGNOSTICS,
"%" PRIu32 "/%" PRIu32 " heap profiles written.\n",
env->heap_limit_profile_taken_,
env->heap_profile_near_heap_limit_);

if (env->heap_limit_profile_taken_ == env->heap_profile_near_heap_limit_) {
Debug(env,
DebugCategory::DIAGNOSTICS,
"Removing the near heap limit callback");
uninstall_callback();
}

// Returning a larger value is required by V8 even after the final write.
return new_limit;
}

inline size_t Environment::SelfSize() const {
size_t size = sizeof(*this);
// Remove non pointer fields that will be tracked in MemoryInfo()
Expand Down
17 changes: 16 additions & 1 deletion src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,12 @@ class Environment final : public MemoryRetainer {
static size_t NearHeapLimitCallback(void* data,
size_t current_heap_limit,
size_t initial_heap_limit);
static size_t HeapSnapshotNearHeapLimitCallback(void* data,
size_t current_heap_limit,
size_t initial_heap_limit);
static size_t HeapProfileNearHeapLimitCallback(void* data,
size_t current_heap_limit,
size_t initial_heap_limit);
static void BuildEmbedderGraph(v8::Isolate* isolate,
v8::EmbedderGraph* graph,
void* data);
Expand Down Expand Up @@ -1050,13 +1056,17 @@ class Environment final : public MemoryRetainer {

inline void set_heap_snapshot_near_heap_limit(uint32_t limit);
inline bool is_in_heapsnapshot_heap_limit_callback() const;
inline void set_heap_profile_near_heap_limit(uint32_t limit);
inline bool is_in_heap_profile_near_heap_limit_callback() const;

inline bool report_exclude_env() const;

inline void AddHeapSnapshotNearHeapLimitCallback();

inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);

inline void AddHeapProfileNearHeapLimitCallback();
inline void RemoveHeapProfileNearHeapLimitCallback(size_t heap_limit);

v8::CpuProfilingResult StartCpuProfile(const CpuProfileOptions& options);
v8::CpuProfile* StopCpuProfile(v8::ProfilerId profile_id);

Expand Down Expand Up @@ -1155,6 +1165,11 @@ class Environment final : public MemoryRetainer {
uint32_t heap_snapshot_near_heap_limit_ = 0;
bool heapsnapshot_near_heap_limit_callback_added_ = false;

bool is_in_heap_profile_near_heap_limit_callback_ = false;
uint32_t heap_limit_profile_taken_ = 0;
uint32_t heap_profile_near_heap_limit_ = 0;
bool heap_profile_near_heap_limit_callback_added_ = false;

uint32_t module_id_counter_ = 0;
uint32_t script_id_counter_ = 0;
uint32_t function_id_counter_ = 0;
Expand Down
1 change: 0 additions & 1 deletion src/node_profiling.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ bool SerializeHeapProfile(Isolate* isolate, std::ostringstream& out_stream) {
if (!profile) {
return false;
}
profiler->StopSamplingHeapProfiler();
JSONWriter writer(out_stream, true);
writer.json_start();

Expand Down
Loading
Loading