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

## `v8.setHeapProfileNearHeapLimit(onProfile, options)`

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

* `onProfile` {Function} Callback invoked with the captured heap profile.
* `profile` {string} A JSON heap profile string with the same shape as
[`syncHeapProfileHandle.stop()`][].
* `options` {Object}
* `maxExtensions` {integer} Maximum number of times V8 is allowed to extend
the heap limit for this registration before letting OOM proceed. After the
budget is exhausted the callback uninstalls itself after the pending
profile is delivered. **Default:** `1`.
* `extensionSize` {integer} Number of bytes to add to the heap limit for
each extension. **Default:** `10 * 1024 * 1024` (10 MiB).

Installs a callback that captures the active sampling heap profile when V8 is
near the heap limit. The callback extends the heap limit up to `maxExtensions`
times by `extensionSize` bytes so the profile can be delivered to JavaScript.

[`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.

The callback is best-effort. If the process cannot continue after the heap
extension, `onProfile` might not run.

Calling `setHeapProfileNearHeapLimit()` more than once is a no-op; the first
registration wins and subsequent calls are silently ignored.

This API is independent of [`v8.setHeapSnapshotNearHeapLimit()`][] and
`--heapsnapshot-near-heap-limit`. Both can be active on the same isolate; on a
near-heap-limit event Node.js invokes every registered handler and uses the
largest returned heap limit. Each handler maintains its own budget.

```cjs
const v8 = require('node:v8');

v8.startHeapProfile();
v8.setHeapProfileNearHeapLimit((profile) => {
console.log(JSON.parse(profile));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This example asks users to exacerbating the heap usages when the heap is near limit...

}, {
maxExtensions: 2,
extensionSize: 64 * 1024 * 1024,
});
```

[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 All @@ -1870,6 +1920,10 @@ console.log(profile);
[`GetHeapSpaceStatistics`]: https://v8docs.nodesource.com/node-13.2/d5/dda/classv8_1_1_isolate.html#ac673576f24fdc7a33378f8f57e1d13a4
[`NODE_V8_COVERAGE`]: cli.md#node_v8_coveragedir
[`Serializer`]: #class-v8serializer
[`syncHeapProfileHandle.stop()`]: #syncheapprofilehandlestop
[`v8.setHeapProfileNearHeapLimit()`]: #v8setheapprofilenearheaplimitonprofile-options
[`v8.setHeapSnapshotNearHeapLimit()`]: #v8setheapsnapshotnearheaplimitlimit
[`v8.startHeapProfile()`]: #v8startheapprofileoptions
[`after` callback]: #afterpromise
[`async_hooks`]: async_hooks.md
[`before` callback]: #beforepromise
Expand Down
34 changes: 34 additions & 0 deletions lib/v8.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@ const {
} = primordials;

const { Buffer } = require('buffer');
const { kEmptyObject } = require('internal/util');
const {
validateFunction,
validateObject,
validateString,
validateOneOf,
validateUint32,
Expand Down Expand Up @@ -128,6 +131,7 @@ const {
updateHeapSpaceStatisticsBuffer,
updateHeapCodeStatisticsBuffer,
setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit,
setHeapProfileNearHeapLimit: _setHeapProfileNearHeapLimit,

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

/**
* Install a callback that captures a sampling allocation profile when V8 is
* near the heap limit, buying the process up to `maxExtensions` additional
* chunks of `extensionSize` bytes to give the callback time to run.
*
* Requires v8.startHeapProfile() to have been called; if the sampler is not
* running when V8 fires the near-heap-limit event, the callback is uninstalled
* and V8 aborts on OOM as usual.
* @param {(profileJson: string) => void} onProfile
* @param {{ maxExtensions?: number, extensionSize?: number }} [options]
* @returns {void}
*/
const kDefaultMaxExtensions = 1;
const kDefaultExtensionSize = 10 * 1024 * 1024;
let heapProfileNearHeapLimitCallbackAdded = false;
function setHeapProfileNearHeapLimit(onProfile, options = kEmptyObject) {
validateFunction(onProfile, 'onProfile');
validateObject(options, 'options');
const {
maxExtensions = kDefaultMaxExtensions,
extensionSize = kDefaultExtensionSize,
} = options;
validateUint32(maxExtensions, 'options.maxExtensions', true);
validateUint32(extensionSize, 'options.extensionSize', true);
if (heapProfileNearHeapLimitCallbackAdded) return;
heapProfileNearHeapLimitCallbackAdded = true;
_setHeapProfileNearHeapLimit(maxExtensions, extensionSize, onProfile);
}

const detailLevelDict = {
__proto__: null,
detailed: detailLevel.DETAILED,
Expand Down Expand Up @@ -563,6 +596,7 @@ module.exports = {
queryObjects,
startupSnapshot,
setHeapSnapshotNearHeapLimit,
setHeapProfileNearHeapLimit,
GCProfiler,
isStringOneByteRepresentation,
startCpuProfile,
Expand Down
39 changes: 36 additions & 3 deletions src/env-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -915,16 +915,49 @@ inline bool Environment::report_exclude_env() const {

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(
uint32_t max_extensions,
size_t extension_size,
v8::Local<v8::Function> callback) {
DCHECK(!heap_profile_near_heap_limit_callback_added_);
const bool was_registered = heapsnapshot_near_heap_limit_callback_added_;
heap_profile_near_heap_limit_max_extensions_ = max_extensions;
heap_profile_near_heap_limit_extension_size_ = extension_size;
heap_profile_near_heap_limit_extensions_used_ = 0;
heap_profile_near_heap_limit_callback_.Reset(isolate_, callback);
heap_profile_near_heap_limit_callback_added_ = true;
if (!was_registered) {
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
this);
}
}

inline void Environment::RemoveHeapProfileNearHeapLimitCallback() {
DCHECK(heap_profile_near_heap_limit_callback_added_);
heap_profile_near_heap_limit_callback_added_ = false;
heap_profile_near_heap_limit_callback_.Reset();
if (!heapsnapshot_near_heap_limit_callback_added_) {
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
0);
}
}

} // namespace node
Expand Down
105 changes: 104 additions & 1 deletion src/env.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#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 Down Expand Up @@ -1047,6 +1048,9 @@ Environment::~Environment() {
if (heapsnapshot_near_heap_limit_callback_added_) {
RemoveHeapSnapshotNearHeapLimitCallback(0);
}
if (heap_profile_near_heap_limit_callback_added_) {
RemoveHeapProfileNearHeapLimitCallback();
}

isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback(
BuildEmbedderGraph, this);
Expand Down Expand Up @@ -2098,14 +2102,36 @@ 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);
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 +2253,83 @@ 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);
const size_t extension = env->heap_profile_near_heap_limit_extension_size_;
const uint32_t max = env->heap_profile_near_heap_limit_max_extensions_;

// Capture is already in progress.
if (env->is_in_heap_profile_near_heap_limit_callback_) {
return current_heap_limit + extension;
}

// Budget spent. Refuse further extensions; DeliverHeapProfileNearHeapLimit
// uninstalls the callback once the last pending payload has been flushed.
if (env->heap_profile_near_heap_limit_extensions_used_ >= max) {
return current_heap_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; });

// If the sampler is not running there is nothing to deliver; let V8 abort.
std::ostringstream out_stream;
if (!node::SerializeHeapProfile(env->isolate(), out_stream)) {
env->RemoveHeapProfileNearHeapLimitCallback();
return current_heap_limit;
}

// Keep only the newest profile if V8 reaches the limit again. Delivery
// runs at the next V8 safe point via RequestInterrupt — more prompt than
// SetImmediate under memory pressure, since a tight allocating loop may
// not tick the event loop before V8 aborts.
const bool should_schedule_delivery =
env->heap_profile_near_heap_limit_pending_.empty();
env->heap_profile_near_heap_limit_pending_ = out_stream.str();
if (should_schedule_delivery) {
env->isolate()->RequestInterrupt(
[](v8::Isolate*, void* data) {
Environment::DeliverHeapProfileNearHeapLimit(
static_cast<Environment*>(data));
},
env);
}

env->heap_profile_near_heap_limit_extensions_used_ += 1;

return current_heap_limit + extension;
}

void Environment::DeliverHeapProfileNearHeapLimit(Environment* env) {
Isolate* isolate = env->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> context = env->context();
v8::Context::Scope context_scope(context);

std::string payload = std::move(env->heap_profile_near_heap_limit_pending_);
auto uninstall_if_spent = OnScopeLeave([env]() {
if (!env->heap_profile_near_heap_limit_callback_added_) return;
if (env->heap_profile_near_heap_limit_extensions_used_ <
env->heap_profile_near_heap_limit_max_extensions_) {
return;
}
env->RemoveHeapProfileNearHeapLimitCallback();
});

if (payload.empty()) return;
if (env->heap_profile_near_heap_limit_callback_.IsEmpty()) return;

v8::Local<v8::Function> callback =
env->heap_profile_near_heap_limit_callback_.Get(isolate);
v8::Local<v8::Value> arg;
if (!ToV8Value(context, payload, isolate).ToLocal(&arg)) return;
v8::Local<v8::Value> argv[] = {arg};
USE(node::MakeCallback(
isolate, context->Global(), callback, arraysize(argv), argv, {0, 0}));
}

inline size_t Environment::SelfSize() const {
size_t size = sizeof(*this);
// Remove non pointer fields that will be tracked in MemoryInfo()
Expand Down
21 changes: 21 additions & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,13 @@ 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 DeliverHeapProfileNearHeapLimit(Environment* env);
static void BuildEmbedderGraph(v8::Isolate* isolate,
v8::EmbedderGraph* graph,
void* data);
Expand Down Expand Up @@ -1057,6 +1064,12 @@ class Environment final : public MemoryRetainer {

inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);

inline void AddHeapProfileNearHeapLimitCallback(
uint32_t max_extensions,
size_t extension_size,
v8::Local<v8::Function> callback);
inline void RemoveHeapProfileNearHeapLimitCallback();

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

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

bool heap_profile_near_heap_limit_callback_added_ = false;
bool is_in_heap_profile_near_heap_limit_callback_ = false;
uint32_t heap_profile_near_heap_limit_max_extensions_ = 0;
uint32_t heap_profile_near_heap_limit_extensions_used_ = 0;
size_t heap_profile_near_heap_limit_extension_size_ = 0;
std::string heap_profile_near_heap_limit_pending_;
v8::Global<v8::Function> heap_profile_near_heap_limit_callback_;

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