diff --git a/doc/api/v8.md b/doc/api/v8.md index 371d5348f65bbc..a4373d1ac72d4a 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1853,6 +1853,39 @@ const profile = handle.stop(); console.log(profile); ``` +## `v8.setHeapProfileNearHeapLimit(limit)` + + + +> 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 +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 @@ -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 diff --git a/lib/v8.js b/lib/v8.js index bb174f8d524305..1dd0553e8c6b14 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -128,6 +128,7 @@ const { updateHeapSpaceStatisticsBuffer, updateHeapCodeStatisticsBuffer, setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit, + setHeapProfileNearHeapLimit: _setHeapProfileNearHeapLimit, // Properties for heap statistics buffer extraction. kTotalHeapSizeIndex, @@ -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, @@ -563,6 +574,7 @@ module.exports = { queryObjects, startupSnapshot, setHeapSnapshotNearHeapLimit, + setHeapProfileNearHeapLimit, GCProfiler, isStringOneByteRepresentation, startCpuProfile, diff --git a/src/env-inl.h b/src/env-inl.h index 761a7bfc995528..4af0cafad53676 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -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 diff --git a/src/env.cc b/src/env.cc index d6a859e7a35452..550d1190d754fc 100644 --- a/src/env.cc +++ b/src/env.cc @@ -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" @@ -33,6 +35,7 @@ #include #include #include +#include #include namespace node { @@ -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); @@ -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(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(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_, @@ -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(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(current_heap_limit), + static_cast(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() diff --git a/src/env.h b/src/env.h index c2caf979023811..59e5d5222af1e8 100644 --- a/src/env.h +++ b/src/env.h @@ -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); @@ -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); @@ -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; diff --git a/src/node_profiling.cc b/src/node_profiling.cc index b6c42b286908e0..c4725d16e8160e 100644 --- a/src/node_profiling.cc +++ b/src/node_profiling.cc @@ -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(); diff --git a/src/node_v8.cc b/src/node_v8.cc index b49c29443a4287..b18201ebfccfb0 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -204,6 +204,15 @@ void SetHeapSnapshotNearHeapLimit(const FunctionCallbackInfo& args) { env->set_heap_snapshot_near_heap_limit(limit); } +void SetHeapProfileNearHeapLimit(const FunctionCallbackInfo& args) { + CHECK(args[0]->IsUint32()); + Environment* env = Environment::GetCurrent(args); + uint32_t limit = args[0].As()->Value(); + CHECK_GT(limit, 0); + env->AddHeapProfileNearHeapLimitCallback(); + env->set_heap_profile_near_heap_limit(limit); +} + void UpdateHeapStatisticsBuffer(const FunctionCallbackInfo& args) { BindingData* data = Realm::GetBindingData(args); HeapStatistics s; @@ -300,6 +309,7 @@ void StopHeapProfile(const FunctionCallbackInfo& args) { std::ostringstream out_stream; bool success = node::SerializeHeapProfile(isolate, out_stream); if (success) { + isolate->GetHeapProfiler()->StopSamplingHeapProfiler(); Local result; if (ToV8Value(env->context(), out_stream.str(), isolate).ToLocal(&result)) { args.GetReturnValue().Set(result); @@ -717,6 +727,10 @@ void Initialize(Local target, target, "setHeapSnapshotNearHeapLimit", SetHeapSnapshotNearHeapLimit); + SetMethod(context, + target, + "setHeapProfileNearHeapLimit", + SetHeapProfileNearHeapLimit); SetMethod(context, target, "updateHeapStatisticsBuffer", @@ -828,6 +842,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(SetFlagsFromString); registry->Register(GetHashSeed); registry->Register(SetHeapSnapshotNearHeapLimit); + registry->Register(SetHeapProfileNearHeapLimit); registry->Register(GCProfiler::New); registry->Register(GCProfiler::Start); registry->Register(GCProfiler::Stop); diff --git a/src/node_worker.cc b/src/node_worker.cc index edc21e7e556157..5a50d5dcab4222 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -280,6 +280,7 @@ size_t Worker::NearHeapLimit(void* data, size_t current_heap_limit, Environment* env = worker->env(); if (env != nullptr) { DCHECK(!env->is_in_heapsnapshot_heap_limit_callback()); + DCHECK(!env->is_in_heap_profile_near_heap_limit_callback()); Debug(env, DebugCategory::DIAGNOSTICS, "Throwing ERR_WORKER_OUT_OF_MEMORY, " @@ -1109,6 +1110,9 @@ void Worker::StopHeapProfile(const FunctionCallbackInfo& args) { std::ostringstream out_stream; bool success = node::SerializeHeapProfile(worker_env->isolate(), out_stream); + if (success) { + worker_env->isolate()->GetHeapProfiler()->StopSamplingHeapProfiler(); + } env->SetImmediateThreadsafe( [taker = std::move(taker), out_stream = std::move(out_stream), diff --git a/test/fixtures/workload/heap-profile-and-snapshot-near-heap-limit.js b/test/fixtures/workload/heap-profile-and-snapshot-near-heap-limit.js new file mode 100644 index 00000000000000..2f9ad332aa201d --- /dev/null +++ b/test/fixtures/workload/heap-profile-and-snapshot-near-heap-limit.js @@ -0,0 +1,9 @@ +'use strict'; +const path = require('path'); +const v8 = require('v8'); + +v8.setHeapSnapshotNearHeapLimit(1); +v8.startHeapProfile(); +v8.setHeapProfileNearHeapLimit(1); + +require(path.resolve(__dirname, 'grow.js')); diff --git a/test/fixtures/workload/heap-profile-near-heap-limit-with-heap-prof.js b/test/fixtures/workload/heap-profile-near-heap-limit-with-heap-prof.js new file mode 100644 index 00000000000000..1e39cf5df8963b --- /dev/null +++ b/test/fixtures/workload/heap-profile-near-heap-limit-with-heap-prof.js @@ -0,0 +1,7 @@ +'use strict'; +const path = require('path'); +const v8 = require('v8'); + +v8.setHeapProfileNearHeapLimit(1); + +require(path.resolve(__dirname, 'grow.js')); diff --git a/test/fixtures/workload/heap-profile-near-heap-limit.js b/test/fixtures/workload/heap-profile-near-heap-limit.js new file mode 100644 index 00000000000000..303cd71ad12c74 --- /dev/null +++ b/test/fixtures/workload/heap-profile-near-heap-limit.js @@ -0,0 +1,8 @@ +'use strict'; +const path = require('path'); +const v8 = require('v8'); + +v8.startHeapProfile(); +v8.setHeapProfileNearHeapLimit(1); + +require(path.resolve(__dirname, 'grow.js')); diff --git a/test/parallel/test-v8-heap-profile.js b/test/parallel/test-v8-heap-profile.js index d1c611212509d4..a8068b1a59ce22 100644 --- a/test/parallel/test-v8-heap-profile.js +++ b/test/parallel/test-v8-heap-profile.js @@ -44,6 +44,12 @@ assert.throws( code: 'ERR_INVALID_ARG_TYPE', }); +const invalidLimits = [-1, 0, '', {}, NaN, undefined]; +for (const value of invalidLimits) { + assert.throws(() => v8.setHeapProfileNearHeapLimit(value), + /ERR_INVALID_ARG_TYPE|ERR_OUT_OF_RANGE/); +} + // Default params. { const handle = v8.startHeapProfile(); @@ -73,3 +79,11 @@ assert.throws( JSON.parse(handle.stop()); assert.strictEqual(handle.stop(), undefined); } + +// Profile and snapshot near-heap-limit callbacks coexist. +{ + v8.setHeapProfileNearHeapLimit(1); + v8.setHeapProfileNearHeapLimit(1); // no-op + v8.setHeapSnapshotNearHeapLimit(1); + v8.setHeapSnapshotNearHeapLimit(1); // no-op +} diff --git a/test/pummel/test-heap-profile-and-snapshot-near-heap-limit.js b/test/pummel/test-heap-profile-and-snapshot-near-heap-limit.js new file mode 100644 index 00000000000000..f7dec66d90605a --- /dev/null +++ b/test/pummel/test-heap-profile-and-snapshot-near-heap-limit.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); + +if (common.isPi()) { + common.skip('Too slow for Raspberry Pi devices'); +} + +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); + +tmpdir.refresh(); +const child = spawnSync(process.execPath, [ + '--max-old-space-size=50', + fixtures.path('workload', 'heap-profile-and-snapshot-near-heap-limit.js'), +], { + cwd: tmpdir.path, +}); + +const stderr = child.stderr.toString(); +assert(common.nodeProcessAborted(child.status, child.signal), + 'process should have aborted, but did not'); + +const files = fs.readdirSync(tmpdir.path); +const snapshots = files.filter((f) => f.endsWith('.heapsnapshot')); +const profiles = files.filter((f) => f.endsWith('.heapprofile')); + +assert(snapshots.length === 1 || + stderr.includes('Not generating snapshots because it\'s too risky')); +assert.strictEqual(profiles.length, 1); diff --git a/test/pummel/test-heap-profile-near-heap-limit.js b/test/pummel/test-heap-profile-near-heap-limit.js new file mode 100644 index 00000000000000..522b01c54c7ca1 --- /dev/null +++ b/test/pummel/test-heap-profile-near-heap-limit.js @@ -0,0 +1,33 @@ +'use strict'; + +const common = require('../common'); + +if (common.isPi()) { + common.skip('Too slow for Raspberry Pi devices'); +} + +const tmpdir = require('../common/tmpdir'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fixtures = require('../common/fixtures'); +const fs = require('fs'); + +tmpdir.refresh(); +const child = spawnSync(process.execPath, [ + '--max-old-space-size=50', + fixtures.path('workload', 'heap-profile-near-heap-limit.js'), +], { + cwd: tmpdir.path, +}); + +assert(common.nodeProcessAborted(child.status, child.signal), + 'process should have aborted, but did not'); + +const profiles = fs.readdirSync(tmpdir.path) + .filter((file) => file.endsWith('.heapprofile')); +assert.strictEqual(profiles.length, 1); + +const profile = JSON.parse( + fs.readFileSync(tmpdir.resolve(profiles[0]), 'utf8')); +assert(profile.head); +assert(profile.samples.length > 0);