Skip to content

Commit 5ed55ba

Browse files
authored
v8: add setHeapProfileNearHeapLimit
Signed-off-by: ishabi <ilyasshabi94@gmail.com> PR-URL: #64676 Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 0375d09 commit 5ed55ba

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"
@@ -36,6 +38,7 @@
3638
#include <limits>
3739
#include <memory>
3840
#include <optional>
41+
#include <sstream>
3942
#include <unordered_map>
4043

4144
namespace node {
@@ -1112,6 +1115,9 @@ Environment::~Environment() {
11121115
if (heapsnapshot_near_heap_limit_callback_added_) {
11131116
RemoveHeapSnapshotNearHeapLimitCallback(0);
11141117
}
1118+
if (heap_profile_near_heap_limit_callback_added_) {
1119+
RemoveHeapProfileNearHeapLimitCallback(0);
1120+
}
11151121

11161122
isolate()->GetHeapProfiler()->RemoveBuildEmbedderGraphCallback(
11171123
BuildEmbedderGraph, this);
@@ -2179,14 +2185,49 @@ void Environment::TracePromises(PromiseHookType type,
21792185
PrintCurrentStackTrace(isolate);
21802186
}
21812187

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

2198+
// Profile and snapshot generation can allocate and invoke this callback
2199+
// recursively. Only extend the heap for the operation already in progress;
2200+
// do not start the other diagnostic from the nested invocation.
2201+
if (env->is_in_heapsnapshot_heap_limit_callback_) {
2202+
return HeapSnapshotNearHeapLimitCallback(
2203+
data, current_heap_limit, initial_heap_limit);
2204+
}
2205+
if (env->is_in_heap_profile_near_heap_limit_callback_) {
2206+
return HeapProfileNearHeapLimitCallback(
2207+
data, current_heap_limit, initial_heap_limit);
2208+
}
2209+
2210+
size_t new_limit = current_heap_limit;
2211+
if (env->heapsnapshot_near_heap_limit_callback_added_) {
2212+
new_limit = std::max(new_limit,
2213+
HeapSnapshotNearHeapLimitCallback(
2214+
data, current_heap_limit, initial_heap_limit));
2215+
}
2216+
if (env->heap_profile_near_heap_limit_callback_added_) {
2217+
new_limit = std::max(new_limit,
2218+
HeapProfileNearHeapLimitCallback(
2219+
data, current_heap_limit, initial_heap_limit));
2220+
}
2221+
return new_limit;
2222+
}
2223+
2224+
size_t Environment::HeapSnapshotNearHeapLimitCallback(
2225+
void* data, size_t current_heap_limit, size_t initial_heap_limit) {
2226+
auto* env = static_cast<Environment*>(data);
2227+
21872228
Debug(env,
21882229
DebugCategory::DIAGNOSTICS,
2189-
"Invoked NearHeapLimitCallback, processing=%d, "
2230+
"Invoked HeapSnapshotNearHeapLimitCallback, processing=%d, "
21902231
"current_limit=%" PRIu64 ", "
21912232
"initial_limit=%" PRIu64 "\n",
21922233
env->is_in_heapsnapshot_heap_limit_callback_,
@@ -2308,6 +2349,85 @@ size_t Environment::NearHeapLimitCallback(void* data,
23082349
return new_limit;
23092350
}
23102351

2352+
size_t Environment::HeapProfileNearHeapLimitCallback(
2353+
void* data, size_t current_heap_limit, size_t initial_heap_limit) {
2354+
auto* env = static_cast<Environment*>(data);
2355+
2356+
Debug(env,
2357+
DebugCategory::DIAGNOSTICS,
2358+
"Invoked HeapProfileNearHeapLimitCallback, processing=%d, "
2359+
"current_limit=%" PRIu64 ", "
2360+
"initial_limit=%" PRIu64 "\n",
2361+
env->is_in_heap_profile_near_heap_limit_callback_,
2362+
static_cast<uint64_t>(current_heap_limit),
2363+
static_cast<uint64_t>(initial_heap_limit));
2364+
2365+
const size_t max_young_gen_size = env->isolate_data()->max_young_gen_size;
2366+
const size_t new_limit = current_heap_limit + max_young_gen_size;
2367+
2368+
if (env->is_in_heap_profile_near_heap_limit_callback_) {
2369+
return new_limit;
2370+
}
2371+
2372+
env->is_in_heap_profile_near_heap_limit_callback_ = true;
2373+
auto reset_in_callback = OnScopeLeave(
2374+
[env]() { env->is_in_heap_profile_near_heap_limit_callback_ = false; });
2375+
auto restore_initial_limit = OnScopeLeave(
2376+
[env]() { env->isolate()->AutomaticallyRestoreInitialHeapLimit(0.95); });
2377+
auto uninstall_callback = [env]() {
2378+
env->RemoveHeapProfileNearHeapLimitCallback(0);
2379+
};
2380+
2381+
std::string dir = env->options()->diagnostic_dir;
2382+
if (dir.empty()) {
2383+
dir = Environment::GetCwd(env->exec_path_);
2384+
}
2385+
DiagnosticFilename name(env, "Heap", "heapprofile");
2386+
std::string filename = dir + kPathSeparator + (*name);
2387+
2388+
Debug(env, DebugCategory::DIAGNOSTICS, "Writing %s...\n", *name);
2389+
2390+
std::ostringstream out;
2391+
if (!node::SerializeHeapProfile(env->isolate(), out)) {
2392+
Debug(env,
2393+
DebugCategory::DIAGNOSTICS,
2394+
"No sampling heap profile active; uninstalling callback.\n");
2395+
uninstall_callback();
2396+
return new_limit;
2397+
}
2398+
2399+
std::string profile = std::move(out).str();
2400+
uv_buf_t buffer = uv_buf_init(profile.data(), profile.size());
2401+
const int err = WriteFileSync(filename.c_str(), buffer);
2402+
if (err != 0) {
2403+
FPrintF(stderr,
2404+
"Failed to write heap profile %s: %s\n",
2405+
filename,
2406+
uv_strerror(err));
2407+
uninstall_callback();
2408+
return new_limit;
2409+
}
2410+
2411+
env->heap_limit_profile_taken_ += 1;
2412+
FPrintF(stderr, "Wrote heap profile to %s\n", filename);
2413+
2414+
Debug(env,
2415+
DebugCategory::DIAGNOSTICS,
2416+
"%" PRIu32 "/%" PRIu32 " heap profiles written.\n",
2417+
env->heap_limit_profile_taken_,
2418+
env->heap_profile_near_heap_limit_);
2419+
2420+
if (env->heap_limit_profile_taken_ == env->heap_profile_near_heap_limit_) {
2421+
Debug(env,
2422+
DebugCategory::DIAGNOSTICS,
2423+
"Removing the near heap limit callback");
2424+
uninstall_callback();
2425+
}
2426+
2427+
// Returning a larger value is required by V8 even after the final write.
2428+
return new_limit;
2429+
}
2430+
23112431
inline size_t Environment::SelfSize() const {
23122432
size_t size = sizeof(*this);
23132433
// 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
@@ -999,6 +999,12 @@ class Environment final : public MemoryRetainer {
999999
static size_t NearHeapLimitCallback(void* data,
10001000
size_t current_heap_limit,
10011001
size_t initial_heap_limit);
1002+
static size_t HeapSnapshotNearHeapLimitCallback(void* data,
1003+
size_t current_heap_limit,
1004+
size_t initial_heap_limit);
1005+
static size_t HeapProfileNearHeapLimitCallback(void* data,
1006+
size_t current_heap_limit,
1007+
size_t initial_heap_limit);
10021008
static void BuildEmbedderGraph(v8::Isolate* isolate,
10031009
v8::EmbedderGraph* graph,
10041010
void* data);
@@ -1076,13 +1082,17 @@ class Environment final : public MemoryRetainer {
10761082

10771083
inline void set_heap_snapshot_near_heap_limit(uint32_t limit);
10781084
inline bool is_in_heapsnapshot_heap_limit_callback() const;
1085+
inline void set_heap_profile_near_heap_limit(uint32_t limit);
1086+
inline bool is_in_heap_profile_near_heap_limit_callback() const;
10791087

10801088
inline bool report_exclude_env() const;
10811089

10821090
inline void AddHeapSnapshotNearHeapLimitCallback();
1083-
10841091
inline void RemoveHeapSnapshotNearHeapLimitCallback(size_t heap_limit);
10851092

1093+
inline void AddHeapProfileNearHeapLimitCallback();
1094+
inline void RemoveHeapProfileNearHeapLimitCallback(size_t heap_limit);
1095+
10861096
v8::CpuProfilingResult StartCpuProfile(const CpuProfileOptions& options);
10871097
v8::CpuProfile* StopCpuProfile(v8::ProfilerId profile_id);
10881098

@@ -1182,6 +1192,11 @@ class Environment final : public MemoryRetainer {
11821192
uint32_t heap_snapshot_near_heap_limit_ = 0;
11831193
bool heapsnapshot_near_heap_limit_callback_added_ = false;
11841194

1195+
bool is_in_heap_profile_near_heap_limit_callback_ = false;
1196+
uint32_t heap_limit_profile_taken_ = 0;
1197+
uint32_t heap_profile_near_heap_limit_ = 0;
1198+
bool heap_profile_near_heap_limit_callback_added_ = false;
1199+
11851200
uint32_t module_id_counter_ = 0;
11861201
uint32_t script_id_counter_ = 0;
11871202
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)