diff --git a/doc/api/v8.md b/doc/api/v8.md index 371d5348f65bbc..b926e84e188e34 100644 --- a/doc/api/v8.md +++ b/doc/api/v8.md @@ -1853,6 +1853,56 @@ const profile = handle.stop(); console.log(profile); ``` +## `v8.setHeapProfileNearHeapLimit(onProfile, options)` + + + +> 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 +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)); +}, { + 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 @@ -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 diff --git a/lib/v8.js b/lib/v8.js index bb174f8d524305..b16bce465b0391 100644 --- a/lib/v8.js +++ b/lib/v8.js @@ -38,7 +38,10 @@ const { } = primordials; const { Buffer } = require('buffer'); +const { kEmptyObject } = require('internal/util'); const { + validateFunction, + validateObject, validateString, validateOneOf, validateUint32, @@ -128,6 +131,7 @@ const { updateHeapSpaceStatisticsBuffer, updateHeapCodeStatisticsBuffer, setHeapSnapshotNearHeapLimit: _setHeapSnapshotNearHeapLimit, + setHeapProfileNearHeapLimit: _setHeapProfileNearHeapLimit, // Properties for heap statistics buffer extraction. kTotalHeapSizeIndex, @@ -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, @@ -563,6 +596,7 @@ module.exports = { queryObjects, startupSnapshot, setHeapSnapshotNearHeapLimit, + setHeapProfileNearHeapLimit, GCProfiler, isStringOneByteRepresentation, startCpuProfile, diff --git a/src/env-inl.h b/src/env-inl.h index 761a7bfc995528..37219989d4215d 100644 --- a/src/env-inl.h +++ b/src/env-inl.h @@ -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 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 diff --git a/src/env.cc b/src/env.cc index d6a859e7a35452..782e74c0e0a343 100644 --- a/src/env.cc +++ b/src/env.cc @@ -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" @@ -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); @@ -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(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(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 +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(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(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 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 callback = + env->heap_profile_near_heap_limit_callback_.Get(isolate); + v8::Local arg; + if (!ToV8Value(context, payload, isolate).ToLocal(&arg)) return; + v8::Local 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() diff --git a/src/env.h b/src/env.h index c2caf979023811..6c9f3228f139be 100644 --- a/src/env.h +++ b/src/env.h @@ -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); @@ -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 callback); + inline void RemoveHeapProfileNearHeapLimitCallback(); + v8::CpuProfilingResult StartCpuProfile(const CpuProfileOptions& options); v8::CpuProfile* StopCpuProfile(v8::ProfilerId profile_id); @@ -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 heap_profile_near_heap_limit_callback_; + 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..43237edeb7f1ea 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -204,6 +204,23 @@ void SetHeapSnapshotNearHeapLimit(const FunctionCallbackInfo& args) { env->set_heap_snapshot_near_heap_limit(limit); } +void SetHeapProfileNearHeapLimit(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + CHECK_EQ(args.Length(), 3); + CHECK(args[0]->IsUint32()); + CHECK(args[1]->IsNumber()); + CHECK(args[2]->IsFunction()); + + const uint32_t max_extensions = args[0].As()->Value(); + const double extension_size = args[1].As()->Value(); + CHECK_GT(max_extensions, 0); + CHECK_GT(extension_size, 0); + + env->AddHeapProfileNearHeapLimitCallback(max_extensions, + static_cast(extension_size), + args[2].As()); +} + void UpdateHeapStatisticsBuffer(const FunctionCallbackInfo& args) { BindingData* data = Realm::GetBindingData(args); HeapStatistics s; @@ -300,6 +317,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 +735,10 @@ void Initialize(Local target, target, "setHeapSnapshotNearHeapLimit", SetHeapSnapshotNearHeapLimit); + SetMethod(context, + target, + "setHeapProfileNearHeapLimit", + SetHeapProfileNearHeapLimit); SetMethod(context, target, "updateHeapStatisticsBuffer", @@ -828,6 +850,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..72097561f57a1e 100644 --- a/src/node_worker.cc +++ b/src/node_worker.cc @@ -1109,6 +1109,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..394fd0d12f6963 --- /dev/null +++ b/test/fixtures/workload/heap-profile-and-snapshot-near-heap-limit.js @@ -0,0 +1,12 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const v8 = require('v8'); + +v8.setHeapSnapshotNearHeapLimit(1); +v8.startHeapProfile(); +v8.setHeapProfileNearHeapLimit((profile) => { + fs.writeFileSync('oom.heapprofile', profile); +}); + +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..970c3c3387d4fa --- /dev/null +++ b/test/fixtures/workload/heap-profile-near-heap-limit.js @@ -0,0 +1,12 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const v8 = require('v8'); + +v8.startHeapProfile(); +v8.setHeapProfileNearHeapLimit((profile) => { + fs.writeFileSync('oom.heapprofile', profile); + process.exit(0); +}); + +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..3b701b1b0aeed0 100644 --- a/test/parallel/test-v8-heap-profile.js +++ b/test/parallel/test-v8-heap-profile.js @@ -44,6 +44,20 @@ assert.throws( code: 'ERR_INVALID_ARG_TYPE', }); +assert.throws(() => v8.setHeapProfileNearHeapLimit(), { + code: 'ERR_INVALID_ARG_TYPE', +}); +assert.throws(() => v8.setHeapProfileNearHeapLimit(() => {}, { + maxExtensions: 0, +}), { + code: 'ERR_OUT_OF_RANGE', +}); +assert.throws(() => v8.setHeapProfileNearHeapLimit(() => {}, { + extensionSize: 0, +}), { + code: 'ERR_OUT_OF_RANGE', +}); + // Default params. { const handle = v8.startHeapProfile(); @@ -73,3 +87,11 @@ assert.throws( JSON.parse(handle.stop()); assert.strictEqual(handle.stop(), undefined); } + +// Profile and snapshot near-heap-limit callbacks coexist. +{ + v8.setHeapProfileNearHeapLimit(() => {}); + v8.setHeapProfileNearHeapLimit(() => {}); // 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..5231c7a672c27b --- /dev/null +++ b/test/pummel/test-heap-profile-and-snapshot-near-heap-limit.js @@ -0,0 +1,44 @@ +'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'); +const path = require('path'); +const env = { + ...process.env, + NODE_DEBUG_NATIVE: 'diagnostics', +}; + +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, + env, +}); +console.log(child.stdout.toString()); +const stderr = child.stderr.toString(); +console.log(stderr); +assert(common.nodeProcessAborted(child.status, child.signal), + 'process should have aborted, but did not'); + +const snapshots = fs.readdirSync(tmpdir.path) + .filter((file) => file.endsWith('.heapsnapshot')); +const risky = [...stderr.matchAll( + /Not generating snapshots because it's too risky/g)].length; +assert(snapshots.length + risky > 0 && snapshots.length <= 1, + `Generated ${snapshots.length} snapshots and ${risky} was too risky`); + +const profile = JSON.parse( + fs.readFileSync(path.join(tmpdir.path, 'oom.heapprofile'), 'utf8')); +assert(profile.head); +assert(profile.samples.length > 0); 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..76e0cd16d8af8a --- /dev/null +++ b/test/pummel/test-heap-profile-near-heap-limit.js @@ -0,0 +1,36 @@ +'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'); +const path = require('path'); +const env = { + ...process.env, + NODE_DEBUG_NATIVE: 'diagnostics', +}; + +tmpdir.refresh(); +const child = spawnSync(process.execPath, [ + '--max-old-space-size=50', + fixtures.path('workload', 'heap-profile-near-heap-limit.js'), +], { + cwd: tmpdir.path, + env, +}); +// Surface the V8 abort trace when the assertions below fail. +console.log(child.stdout.toString()); +console.log(child.stderr.toString()); +assert.strictEqual(child.status, 0); + +const profile = JSON.parse( + fs.readFileSync(path.join(tmpdir.path, 'oom.heapprofile'), 'utf8')); +assert(profile.head); +assert(profile.samples.length > 0);