Skip to content

Commit 63db010

Browse files
committed
v8: add setHeapProfileNearHeapLimit
Signed-off-by: ishabi <ilyasshabi94@gmail.com>
1 parent 984e46f commit 63db010

15 files changed

Lines changed: 361 additions & 6 deletions

‎doc/api/v8.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,39 @@ const profile = handle.stop();
18531853
console.log(profile);
18541854
```
18551855
1856+
## `v8.setHeapProfileNearHeapLimit(limit)`
1857+
1858+
<!-- YAML
1859+
added: REPLACEME
1860+
-->
1861+
1862+
> Stability: 1 - Experimental
1863+
1864+
* `limit` {integer}
1865+
1866+
Writes the active sampling heap profile as a `.heapprofile` file when V8 is
1867+
near the heap limit, up to `limit` times. Files are written to the directory
1868+
configured by `--diagnostic-dir` (or to the current working directory if the
1869+
option is unset), using the
1870+
`Heap.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.heapprofile` naming
1871+
convention.
1872+
1873+
[`v8.startHeapProfile()`][] must be called before this API can capture a
1874+
profile. If no sampling heap profile is active when V8 reaches the heap
1875+
limit, the callback is uninstalled and V8 continues its normal
1876+
out-of-memory behavior.
1877+
1878+
This API can be used together with [`v8.setHeapSnapshotNearHeapLimit()`][].
1879+
Each API maintains its own `limit`.
1880+
1881+
Writing is best-effort. Materializing the sampled call tree can allocate V8
1882+
and native memory, so the profile might not be completed if the process has
1883+
insufficient memory available. The serialized profile is written by native code
1884+
and is not exposed to JavaScript.
1885+
1886+
Calling `setHeapProfileNearHeapLimit()` more than once is a no-op. `limit`
1887+
must be a positive integer.
1888+
18561889
[CppHeap]: https://v8docs.nodesource.com/node-22.4/d9/dc4/classv8_1_1_cpp_heap.html
18571890
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
18581891
[Hook Callbacks]: #hook-callbacks
@@ -1887,6 +1920,8 @@ console.log(profile);
18871920
[`serializer.transferArrayBuffer()`]: #serializertransferarraybufferid-arraybuffer
18881921
[`serializer.writeRawBytes()`]: #serializerwriterawbytesbuffer
18891922
[`settled` callback]: #settledpromise
1923+
[`v8.setHeapSnapshotNearHeapLimit()`]: #v8setheapsnapshotnearheaplimitlimit
1924+
[`v8.startHeapProfile()`]: #v8startheapprofileoptions
18901925
[`v8.stopCoverage()`]: #v8stopcoverage
18911926
[`v8.takeCoverage()`]: #v8takecoverage
18921927
[`vm.Script`]: vm.md#new-vmscriptcode-options

‎lib/v8.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const {
128128
updateHeapSpaceStatisticsBuffer,
129129
updateHeapCodeStatisticsBuffer,
130130
setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit,
131+
setHeapProfileNearHeapLimit: _setHeapProfileNearHeapLimit,
131132

132133
// Properties for heap statistics buffer extraction.
133134
kTotalHeapSizeIndex,
@@ -360,6 +361,16 @@ function setHeapSnapshotNearHeapLimit(limit) {
360361
_setHeapSnapshotNearHeapLimit(limit);
361362
}
362363

364+
let heapProfileNearHeapLimitAdded = false;
365+
function setHeapProfileNearHeapLimit(limit) {
366+
validateUint32(limit, 'limit', true);
367+
if (heapProfileNearHeapLimitAdded) {
368+
return;
369+
}
370+
_setHeapProfileNearHeapLimit(limit);
371+
heapProfileNearHeapLimitAdded = true;
372+
}
373+
363374
const detailLevelDict = {
364375
__proto__: null,
365376
detailed: detailLevel.DETAILED,
@@ -563,6 +574,7 @@ module.exports = {
563574
queryObjects,
564575
startupSnapshot,
565576
setHeapSnapshotNearHeapLimit,
577+
setHeapProfileNearHeapLimit,
566578
GCProfiler,
567579
isStringOneByteRepresentation,
568580
startCpuProfile,

‎src/env-inl.h‎

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -923,26 +923,60 @@ inline void Environment::set_heap_snapshot_near_heap_limit(uint32_t limit) {
923923
heap_snapshot_near_heap_limit_ = limit;
924924
}
925925

926+
inline void Environment::set_heap_profile_near_heap_limit(uint32_t limit) {
927+
heap_profile_near_heap_limit_ = limit;
928+
}
929+
926930
inline bool Environment::is_in_heapsnapshot_heap_limit_callback() const {
927931
return is_in_heapsnapshot_heap_limit_callback_;
928932
}
929933

934+
inline bool Environment::is_in_heap_profile_near_heap_limit_callback() const {
935+
return is_in_heap_profile_near_heap_limit_callback_;
936+
}
937+
930938
inline bool Environment::report_exclude_env() const {
931939
return options_->report_exclude_env;
932940
}
933941

934942
inline void Environment::AddHeapSnapshotNearHeapLimitCallback() {
935943
DCHECK(!heapsnapshot_near_heap_limit_callback_added_);
944+
const bool was_registered = heap_profile_near_heap_limit_callback_added_;
936945
heapsnapshot_near_heap_limit_callback_added_ = true;
937-
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback, this);
946+
if (!was_registered) {
947+
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
948+
this);
949+
}
938950
}
939951

940952
inline void Environment::RemoveHeapSnapshotNearHeapLimitCallback(
941953
size_t heap_limit) {
942954
DCHECK(heapsnapshot_near_heap_limit_callback_added_);
943955
heapsnapshot_near_heap_limit_callback_added_ = false;
944-
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
945-
heap_limit);
956+
if (!heap_profile_near_heap_limit_callback_added_) {
957+
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
958+
heap_limit);
959+
}
960+
}
961+
962+
inline void Environment::AddHeapProfileNearHeapLimitCallback() {
963+
DCHECK(!heap_profile_near_heap_limit_callback_added_);
964+
const bool was_registered = heapsnapshot_near_heap_limit_callback_added_;
965+
heap_profile_near_heap_limit_callback_added_ = true;
966+
if (!was_registered) {
967+
isolate_->AddNearHeapLimitCallback(Environment::NearHeapLimitCallback,
968+
this);
969+
}
970+
}
971+
972+
inline void Environment::RemoveHeapProfileNearHeapLimitCallback(
973+
size_t heap_limit) {
974+
DCHECK(heap_profile_near_heap_limit_callback_added_);
975+
heap_profile_near_heap_limit_callback_added_ = false;
976+
if (!heapsnapshot_near_heap_limit_callback_added_) {
977+
isolate_->RemoveNearHeapLimitCallback(Environment::NearHeapLimitCallback,
978+
heap_limit);
979+
}
946980
}
947981

948982
} // namespace node

‎src/env.cc‎

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
#include "node_context_data.h"
1010
#include "node_contextify.h"
1111
#include "node_errors.h"
12+
#include "node_file_utils.h"
1213
#include "node_internals.h"
1314
#include "node_options-inl.h"
1415
#include "node_process-inl.h"
16+
#include "node_profiling.h"
1517
#include "node_shadow_realm.h"
1618
#include "node_snapshotable.h"
1719
#include "node_v8_platform-inl.h"
@@ -33,6 +35,7 @@
3335
#include <limits>
3436
#include <memory>
3537
#include <optional>
38+
#include <sstream>
3639
#include <unordered_map>
3740

3841
namespace node {
@@ -1101,6 +1104,9 @@ Environment::~Environment() {
11011104
if (heapsnapshot_near_heap_limit_callback_added_) {
11021105
RemoveHeapSnapshotNearHeapLimitCallback(0);
11031106
}
1107+
if (heap_profile_near_heap_limit_callback_added_) {
1108+
RemoveHeapProfileNearHeapLimitCallback(0);
1109+
}
11041110

11051111
isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback(
11061112
BuildEmbedderGraph, this);
@@ -2159,14 +2165,49 @@ void Environment::TracePromises(PromiseHookType type,
21592165
PrintCurrentStackTrace(isolate);
21602166
}
21612167

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

2178+
// Profile and snapshot generation can allocate and invoke this callback
2179+
// recursively. Only extend the heap for the operation already in progress;
2180+
// do not start the other diagnostic from the nested invocation.
2181+
if (env->is_in_heapsnapshot_heap_limit_callback_) {
2182+
return HeapSnapshotNearHeapLimitCallback(
2183+
data, current_heap_limit, initial_heap_limit);
2184+
}
2185+
if (env->is_in_heap_profile_near_heap_limit_callback_) {
2186+
return HeapProfileNearHeapLimitCallback(
2187+
data, current_heap_limit, initial_heap_limit);
2188+
}
2189+
2190+
size_t new_limit = current_heap_limit;
2191+
if (env->heapsnapshot_near_heap_limit_callback_added_) {
2192+
new_limit = std::max(new_limit,
2193+
HeapSnapshotNearHeapLimitCallback(
2194+
data, current_heap_limit, initial_heap_limit));
2195+
}
2196+
if (env->heap_profile_near_heap_limit_callback_added_) {
2197+
new_limit = std::max(new_limit,
2198+
HeapProfileNearHeapLimitCallback(
2199+
data, current_heap_limit, initial_heap_limit));
2200+
}
2201+
return new_limit;
2202+
}
2203+
2204+
size_t Environment::HeapSnapshotNearHeapLimitCallback(
2205+
void* data, size_t current_heap_limit, size_t initial_heap_limit) {
2206+
auto* env = static_cast<Environment*>(data);
2207+
21672208
Debug(env,
21682209
DebugCategory::DIAGNOSTICS,
2169-
"Invoked NearHeapLimitCallback, processing=%d, "
2210+
"Invoked HeapSnapshotNearHeapLimitCallback, processing=%d, "
21702211
"current_limit=%" PRIu64 ", "
21712212
"initial_limit=%" PRIu64 "\n",
21722213
env->is_in_heapsnapshot_heap_limit_callback_,
@@ -2288,6 +2329,85 @@ size_t Environment::NearHeapLimitCallback(void* data,
22882329
return new_limit;
22892330
}
22902331

2332+
size_t Environment::HeapProfileNearHeapLimitCallback(
2333+
void* data, size_t current_heap_limit, size_t initial_heap_limit) {
2334+
auto* env = static_cast<Environment*>(data);
2335+
2336+
Debug(env,
2337+
DebugCategory::DIAGNOSTICS,
2338+
"Invoked HeapProfileNearHeapLimitCallback, processing=%d, "
2339+
"current_limit=%" PRIu64 ", "
2340+
"initial_limit=%" PRIu64 "\n",
2341+
env->is_in_heap_profile_near_heap_limit_callback_,
2342+
static_cast<uint64_t>(current_heap_limit),
2343+
static_cast<uint64_t>(initial_heap_limit));
2344+
2345+
const size_t max_young_gen_size = env->isolate_data()->max_young_gen_size;
2346+
const size_t new_limit = current_heap_limit + max_young_gen_size;
2347+
2348+
if (env->is_in_heap_profile_near_heap_limit_callback_) {
2349+
return new_limit;
2350+
}
2351+
2352+
env->is_in_heap_profile_near_heap_limit_callback_ = true;
2353+
auto reset_in_callback = OnScopeLeave(
2354+
[env]() { env->is_in_heap_profile_near_heap_limit_callback_ = false; });
2355+
auto restore_initial_limit = OnScopeLeave(
2356+
[env]() { env->isolate()->AutomaticallyRestoreInitialHeapLimit(0.95); });
2357+
auto uninstall_callback = [env]() {
2358+
env->RemoveHeapProfileNearHeapLimitCallback(0);
2359+
};
2360+
2361+
std::string dir = env->options()->diagnostic_dir;
2362+
if (dir.empty()) {
2363+
dir = Environment::GetCwd(env->exec_path_);
2364+
}
2365+
DiagnosticFilename name(env, "Heap", "heapprofile");
2366+
std::string filename = dir + kPathSeparator + (*name);
2367+
2368+
Debug(env, DebugCategory::DIAGNOSTICS, "Writing %s...\n", *name);
2369+
2370+
std::ostringstream out;
2371+
if (!node::SerializeHeapProfile(env->isolate(), out)) {
2372+
Debug(env,
2373+
DebugCategory::DIAGNOSTICS,
2374+
"No sampling heap profile active; uninstalling callback.\n");
2375+
uninstall_callback();
2376+
return new_limit;
2377+
}
2378+
2379+
std::string profile = std::move(out).str();
2380+
uv_buf_t buffer = uv_buf_init(profile.data(), profile.size());
2381+
const int err = WriteFileSync(filename.c_str(), buffer);
2382+
if (err != 0) {
2383+
FPrintF(stderr,
2384+
"Failed to write heap profile %s: %s\n",
2385+
filename,
2386+
uv_strerror(err));
2387+
uninstall_callback();
2388+
return new_limit;
2389+
}
2390+
2391+
env->heap_limit_profile_taken_ += 1;
2392+
FPrintF(stderr, "Wrote heap profile to %s\n", filename);
2393+
2394+
Debug(env,
2395+
DebugCategory::DIAGNOSTICS,
2396+
"%" PRIu32 "/%" PRIu32 " heap profiles written.\n",
2397+
env->heap_limit_profile_taken_,
2398+
env->heap_profile_near_heap_limit_);
2399+
2400+
if (env->heap_limit_profile_taken_ == env->heap_profile_near_heap_limit_) {
2401+
Debug(env,
2402+
DebugCategory::DIAGNOSTICS,
2403+
"Removing the near heap limit callback");
2404+
uninstall_callback();
2405+
}
2406+
2407+
// Returning a larger value is required by V8 even after the final write.
2408+
return new_limit;
2409+
}
2410+
22912411
inline size_t Environment::SelfSize() const {
22922412
size_t size = sizeof(*this);
22932413
// Remove non pointer fields that will be tracked in MemoryInfo()

‎src/env.h‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,12 @@ class Environment final : public MemoryRetainer {
997997
static size_t NearHeapLimitCallback(void* data,
998998
size_t current_heap_limit,
999999
size_t initial_heap_limit);
1000+
static size_t HeapSnapshotNearHeapLimitCallback(void* data,
1001+
size_t current_heap_limit,
1002+
size_t initial_heap_limit);
1003+
static size_t HeapProfileNearHeapLimitCallback(void* data,
1004+
size_t current_heap_limit,
1005+
size_t initial_heap_limit);
10001006
static void BuildEmbedderGraph(v8::Isolate* isolate,
10011007
v8::EmbedderGraph* graph,
10021008
void* data);
@@ -1074,13 +1080,17 @@ class Environment final : public MemoryRetainer {
10741080

10751081
inline void set_heap_snapshot_near_heap_limit(uint32_t limit);
10761082
inline bool is_in_heapsnapshot_heap_limit_callback() const;
1083+
inline void set_heap_profile_near_heap_limit(uint32_t limit);
1084+
inline bool is_in_heap_profile_near_heap_limit_callback() const;
10771085

10781086
inline bool report_exclude_env() const;
10791087

10801088
inline void AddHeapSnapshotNearHeapLimitCallback();
1081-
10821089
inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);
10831090

1091+
inline void AddHeapProfileNearHeapLimitCallback();
1092+
inline void RemoveHeapProfileNearHeapLimitCallback(size_t heap_limit);
1093+
10841094
v8::CpuProfilingResult StartCpuProfile(const CpuProfileOptions& options);
10851095
v8::CpuProfile* StopCpuProfile(v8::ProfilerId profile_id);
10861096

@@ -1179,6 +1189,11 @@ class Environment final : public MemoryRetainer {
11791189
uint32_t heap_snapshot_near_heap_limit_ = 0;
11801190
bool heapsnapshot_near_heap_limit_callback_added_ = false;
11811191

1192+
bool is_in_heap_profile_near_heap_limit_callback_ = false;
1193+
uint32_t heap_limit_profile_taken_ = 0;
1194+
uint32_t heap_profile_near_heap_limit_ = 0;
1195+
bool heap_profile_near_heap_limit_callback_added_ = false;
1196+
11821197
uint32_t module_id_counter_ = 0;
11831198
uint32_t script_id_counter_ = 0;
11841199
uint32_t function_id_counter_ = 0;

‎src/node_profiling.cc‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ bool SerializeHeapProfile(Isolate* isolate, std::ostringstream& out_stream) {
4848
if (!profile) {
4949
return false;
5050
}
51-
profiler->StopSamplingHeapProfiler();
5251
JSONWriter writer(out_stream, true);
5352
writer.json_start();
5453

0 commit comments

Comments
 (0)