diff --git a/filenames.gni b/filenames.gni index 8522de131b..c4bff0487a 100644 --- a/filenames.gni +++ b/filenames.gni @@ -742,6 +742,8 @@ filenames = { "shell/renderer/electron_sandboxed_renderer_client.h", "shell/renderer/electron_smooth_round_rect.cc", "shell/renderer/electron_smooth_round_rect.h", + "shell/renderer/oom_stack_trace.cc", + "shell/renderer/oom_stack_trace.h", "shell/renderer/preload_realm_context.cc", "shell/renderer/preload_realm_context.h", "shell/renderer/preload_utils.cc", diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index 16f6b2fc5f..8fe41237bd 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -51,6 +51,7 @@ #include "third_party/blink/renderer/bindings/core/v8/v8_initializer.h" // nogncheck #include "third_party/electron_node/src/debug_utils.h" #include "third_party/electron_node/src/module_wrap.h" +#include "v8/include/v8-statistics.h" #if !IS_MAS_BUILD() #include "shell/common/crash_keys.h" @@ -200,6 +201,25 @@ void V8OOMErrorCallback(const char* location, const v8::OOMDetails& details) { if (details.detail) { electron::crash_keys::SetCrashKey("electron.v8-oom.detail", details.detail); } + + // TryGetCurrent() instead of GetCurrent() to avoid FATAL if no isolate. + v8::Isolate* isolate = v8::Isolate::TryGetCurrent(); + if (isolate) { + v8::HeapStatistics stats; + isolate->GetHeapStatistics(&stats); + electron::crash_keys::SetCrashKey( + "electron.v8-oom.heap.used", + base::NumberToString(stats.used_heap_size())); + electron::crash_keys::SetCrashKey( + "electron.v8-oom.heap.total", + base::NumberToString(stats.total_heap_size())); + electron::crash_keys::SetCrashKey( + "electron.v8-oom.heap.limit", + base::NumberToString(stats.heap_size_limit())); + electron::crash_keys::SetCrashKey( + "electron.v8-oom.heap.total_available", + base::NumberToString(stats.total_available_size())); + } #endif OOM_CRASH(0); diff --git a/shell/renderer/electron_renderer_client.cc b/shell/renderer/electron_renderer_client.cc index 77f1244a11..e438cdddca 100644 --- a/shell/renderer/electron_renderer_client.cc +++ b/shell/renderer/electron_renderer_client.cc @@ -86,6 +86,9 @@ void ElectronRendererClient::DidCreateScriptContext( v8::Isolate* const isolate, v8::Local renderer_context, content::RenderFrame* render_frame) { + RendererClientBase::DidCreateScriptContext(isolate, renderer_context, + render_frame); + // TODO(zcbenz): Do not create Node environment if node integration is not // enabled. @@ -238,6 +241,8 @@ bool WorkerHasNodeIntegration(blink::ExecutionContext* ec) { void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread( v8::Local context) { + RendererClientBase::WorkerScriptReadyForEvaluationOnWorkerThread(context); + auto* ec = blink::ExecutionContext::From(context); if (!WorkerHasNodeIntegration(ec)) return; @@ -251,12 +256,15 @@ void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread( void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread( v8::Local context) { auto* ec = blink::ExecutionContext::From(context); - if (!WorkerHasNodeIntegration(ec)) - return; + if (WorkerHasNodeIntegration(ec)) { + auto* current = WebWorkerObserver::GetCurrent(); + if (current) + current->ContextWillDestroy(context); + } - auto* current = WebWorkerObserver::GetCurrent(); - if (current) - current->ContextWillDestroy(context); + // Call base class last: OOM callback deregistration must happen after + // all other cleanup that might still trigger V8 heap operations. + RendererClientBase::WillDestroyWorkerContextOnWorkerThread(context); } void ElectronRendererClient::SetUpWebAssemblyTrapHandler() { diff --git a/shell/renderer/electron_sandboxed_renderer_client.cc b/shell/renderer/electron_sandboxed_renderer_client.cc index 3eb0810d7f..57f3f5f8cc 100644 --- a/shell/renderer/electron_sandboxed_renderer_client.cc +++ b/shell/renderer/electron_sandboxed_renderer_client.cc @@ -111,6 +111,8 @@ void ElectronSandboxedRendererClient::DidCreateScriptContext( v8::Isolate* const isolate, v8::Local context, content::RenderFrame* render_frame) { + RendererClientBase::DidCreateScriptContext(isolate, context, render_frame); + // Only allow preload for the main frame or // For devtools we still want to run the preload_bundle script // Or when nodeSupport is explicitly enabled in sub frames diff --git a/shell/renderer/oom_stack_trace.cc b/shell/renderer/oom_stack_trace.cc new file mode 100644 index 0000000000..bcd386c008 --- /dev/null +++ b/shell/renderer/oom_stack_trace.cc @@ -0,0 +1,265 @@ +// Copyright (c) 2026 Anysphere, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/renderer/oom_stack_trace.h" + +#include +#include +#include +#include + +#include "base/logging.h" +#include "base/memory/raw_ptr.h" +#include "base/no_destructor.h" +#include "base/strings/string_number_conversions.h" +#include "base/threading/thread_local.h" +#include "electron/mas.h" +#include "gin/per_isolate_data.h" +#include "shell/common/crash_keys.h" +#include "third_party/abseil-cpp/absl/strings/str_format.h" +#include "v8/include/v8-exception.h" +#include "v8/include/v8-internal.h" +#include "v8/include/v8-isolate.h" +#include "v8/include/v8-local-handle.h" +#include "v8/include/v8-primitive.h" +#include "v8/include/v8-statistics.h" + +namespace electron { + +namespace { + +// Forward-declare so OomState can reference it. +size_t NearHeapLimitCallback(void* data, + size_t current_heap_limit, + size_t initial_heap_limit); + +struct OomState : public gin::PerIsolateData::DisposeObserver { + raw_ptr isolate; + std::atomic is_in_oom{false}; + + // gin::PerIsolateData::DisposeObserver: + void OnBeforeDispose(v8::Isolate* disposing_isolate) override { + if (!is_in_oom.load()) { + disposing_isolate->RemoveNearHeapLimitCallback(NearHeapLimitCallback, 0); + } + isolate = nullptr; + } + void OnBeforeMicrotasksRunnerDispose(v8::Isolate* /*disposing*/) override {} + void OnDisposed() override {} +}; + +base::ThreadLocalOwnedPointer& GetOomState() { + static base::NoDestructor> instance; + return *instance; +} + +std::string FormatStackTrace(v8::Isolate* isolate, + v8::Local stack) { + std::string result; + int frame_count = stack->GetFrameCount(); + for (int i = 0; i < frame_count; i++) { + v8::Local frame = stack->GetFrame(isolate, i); + + v8::Local function_name = frame->GetFunctionName(); + v8::Local script_name = frame->GetScriptName(); + int line = frame->GetLineNumber(); + int col = frame->GetColumn(); + + std::string func_str = "(anonymous)"; + if (!function_name.IsEmpty()) { + v8::String::Utf8Value utf8(isolate, function_name); + if (*utf8) { + func_str = *utf8; + } + } + + std::string script_str = ""; + if (!script_name.IsEmpty()) { + v8::String::Utf8Value utf8(isolate, script_name); + if (*utf8) { + script_str = *utf8; + } + } + + absl::StrAppendFormat(&result, " #%d %s (%s:%d:%d)\n", i, func_str, + script_str, line, col); + } + return result; +} + +// Runs at the next V8 safe point after the heap limit was hit. +// At a safe point, all frames have deoptimization data available, +// so CurrentStackTrace won't FATAL on optimized frames. +void CaptureStackOnInterrupt(v8::Isolate* isolate, void* data) { + if (!isolate->InContext()) { + return; + } + + v8::HeapStatistics stats; + isolate->GetHeapStatistics(&stats); + // CurrentStackTrace allocates StackTraceInfo + StackFrameInfo objects on the + // V8 managed heap. If the 20 MB bump from NearHeapLimitCallback has been + // partially consumed by the time this interrupt fires, these allocations + // could trigger a secondary OOM. Check headroom before proceeding. + // Note: use addition form to avoid unsigned underflow -- in OOM scenarios, + // used_heap_size can transiently exceed heap_size_limit. + constexpr size_t kMinHeadroom = 2 * 1024 * 1024; // 2 MB + if (stats.used_heap_size() + kMinHeadroom > stats.heap_size_limit()) { + LOG(INFO) << "Skipping JS stack capture: insufficient heap headroom"; + return; + } + + v8::HandleScope handle_scope(isolate); + v8::Local stack = + v8::StackTrace::CurrentStackTrace(isolate, 10); + if (stack.IsEmpty() || stack->GetFrameCount() == 0) { + return; + } + + std::string js_stack = FormatStackTrace(isolate, stack); + if (!js_stack.empty()) { +#if !IS_MAS_BUILD() + crash_keys::SetCrashKey("electron.v8-oom.stack", js_stack); +#endif + LOG(ERROR) << "\n<--- JS stacktrace (captured at safe point) --->\n" + << js_stack; + } +} + +// V8's pointer compression cage limits the heap to kPtrComprCageReservationSize +// (~4 GB). After this callback returns a new limit, V8 clamps it via +// Heap::AllocatorLimitOnMaxOldGenerationSize() to at most that cage size. +// If current_heap_limit is already near the ceiling the bump is effectively +// zero, the interrupt never gets enough headroom to fire, and we never capture +// a stack trace. When that happens we fall back to recording heap info only. +size_t NearHeapLimitCallback(void* data, + size_t current_heap_limit, + size_t initial_heap_limit) { + auto* state = static_cast(data); + v8::Isolate* isolate = state->isolate; + + if (state->is_in_oom.exchange(true)) { + return current_heap_limit; + } + + v8::HeapStatistics stats; + isolate->GetHeapStatistics(&stats); + std::string heap_info = absl::StrFormat("Heap: used=%.1fMB limit=%.1fMB", + stats.used_heap_size() / 1048576.0, + stats.heap_size_limit() / 1048576.0); + LOG(ERROR) << "\n<--- Near heap limit --->\n" << heap_info; + +#if !IS_MAS_BUILD() + crash_keys::SetCrashKey("electron.v8-oom.stack", + heap_info + " (stack pending)"); + + crash_keys::SetCrashKey("electron.v8-oom.heap.used", + base::NumberToString(stats.used_heap_size())); + crash_keys::SetCrashKey("electron.v8-oom.heap.total", + base::NumberToString(stats.total_heap_size())); + crash_keys::SetCrashKey("electron.v8-oom.heap.limit", + base::NumberToString(stats.heap_size_limit())); + crash_keys::SetCrashKey("electron.v8-oom.heap.total_available", + base::NumberToString(stats.total_available_size())); + crash_keys::SetCrashKey("electron.v8-oom.heap.total_physical", + base::NumberToString(stats.total_physical_size())); + crash_keys::SetCrashKey("electron.v8-oom.heap.malloced_memory", + base::NumberToString(stats.malloced_memory())); + crash_keys::SetCrashKey("electron.v8-oom.heap.external_memory", + base::NumberToString(stats.external_memory())); + crash_keys::SetCrashKey( + "electron.v8-oom.heap.native_contexts", + base::NumberToString(stats.number_of_native_contexts())); + crash_keys::SetCrashKey( + "electron.v8-oom.heap.detached_contexts", + base::NumberToString(stats.number_of_detached_contexts())); + + double utilization = stats.heap_size_limit() > 0 + ? static_cast(stats.used_heap_size()) / + stats.heap_size_limit() * 100.0 + : 100.0; + crash_keys::SetCrashKey("electron.v8-oom.heap.utilization_pct", + absl::StrFormat("%.1f", utilization)); + + v8::HeapSpaceStatistics space_stats; + for (size_t i = 0; i < isolate->NumberOfHeapSpaces(); i++) { + isolate->GetHeapSpaceStatistics(&space_stats, i); + if (std::string_view(space_stats.space_name()) == "old_space") { + crash_keys::SetCrashKey( + "electron.v8-oom.old_space.used", + base::NumberToString(space_stats.space_used_size())); + crash_keys::SetCrashKey("electron.v8-oom.old_space.size", + base::NumberToString(space_stats.space_size())); + } else if (std::string_view(space_stats.space_name()) == + "large_object_space") { + crash_keys::SetCrashKey( + "electron.v8-oom.lo_space.used", + base::NumberToString(space_stats.space_used_size())); + crash_keys::SetCrashKey("electron.v8-oom.lo_space.size", + base::NumberToString(space_stats.space_size())); + } + } +#endif + + // Request an interrupt to capture the JS stack at the next safe point, + // where optimized frames have deoptimization data available. + // CurrentStackTrace is unsafe to call directly here because V8 may + // FATAL on optimized frames missing deopt info during OOM. + isolate->RequestInterrupt(CaptureStackOnInterrupt, state); + + // Remove ourselves and bump the limit to give V8 room to reach a safe + // point where the interrupt can fire and capture the stack trace. + isolate->RemoveNearHeapLimitCallback(NearHeapLimitCallback, 0); + + constexpr size_t kHeapBump = 20 * 1024 * 1024; + size_t new_limit = current_heap_limit + kHeapBump; + +#ifdef V8_COMPRESS_POINTERS + constexpr size_t kCageLimit = v8::internal::kPtrComprCageReservationSize; + static_assert(kCageLimit == size_t{1} << 32, + "Cage size changed; review heap bump logic"); +#else + constexpr size_t kCageLimit = std::numeric_limits::max(); +#endif + + if (current_heap_limit >= kCageLimit - kHeapBump) { + // The bump will be clamped by V8 to the cage ceiling, leaving no + // headroom for the interrupt to fire. Record what we can now. +#if !IS_MAS_BUILD() + crash_keys::SetCrashKey("electron.v8-oom.stack", + heap_info + " (at cage limit, stack unavailable)"); +#endif + LOG(INFO) << "Near V8 cage limit; stack trace capture may not succeed"; + } + + return new_limit; +} + +} // namespace + +void RegisterOomStackTraceCallback(v8::Isolate* isolate) { + auto& tls = GetOomState(); + if (tls.Get()) + return; + auto state = std::make_unique(); + state->isolate = isolate; + OomState* raw = state.get(); + gin::PerIsolateData::From(isolate)->AddDisposeObserver(raw); + tls.Set(std::move(state)); + isolate->AddNearHeapLimitCallback(NearHeapLimitCallback, raw); +} + +void UnregisterOomStackTraceCallback(v8::Isolate* isolate) { + auto& tls = GetOomState(); + OomState* state = tls.Get(); + if (!state || state->isolate != isolate) + return; + if (!state->is_in_oom.load()) { + isolate->RemoveNearHeapLimitCallback(NearHeapLimitCallback, 0); + } + gin::PerIsolateData::From(isolate)->RemoveDisposeObserver(state); + tls.Set(nullptr); +} + +} // namespace electron diff --git a/shell/renderer/oom_stack_trace.h b/shell/renderer/oom_stack_trace.h new file mode 100644 index 0000000000..12a5575d4b --- /dev/null +++ b/shell/renderer/oom_stack_trace.h @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Anysphere, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_RENDERER_OOM_STACK_TRACE_H_ +#define ELECTRON_SHELL_RENDERER_OOM_STACK_TRACE_H_ + +#include "v8/include/v8-isolate.h" + +namespace electron { + +void RegisterOomStackTraceCallback(v8::Isolate* isolate); +void UnregisterOomStackTraceCallback(v8::Isolate* isolate); + +} // namespace electron + +#endif // ELECTRON_SHELL_RENDERER_OOM_STACK_TRACE_H_ diff --git a/shell/renderer/renderer_client_base.cc b/shell/renderer/renderer_client_base.cc index 5f4dad2cb1..c300f7fd22 100644 --- a/shell/renderer/renderer_client_base.cc +++ b/shell/renderer/renderer_client_base.cc @@ -36,6 +36,7 @@ #include "shell/renderer/content_settings_observer.h" #include "shell/renderer/electron_api_service_impl.h" #include "shell/renderer/electron_autofill_agent.h" +#include "shell/renderer/oom_stack_trace.h" #include "third_party/abseil-cpp/absl/strings/str_format.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h" #include "third_party/blink/public/common/associated_interfaces/associated_interface_registry.h" @@ -49,6 +50,7 @@ #include "third_party/blink/public/web/web_script_source.h" #include "third_party/blink/public/web/web_security_policy.h" #include "third_party/blink/public/web/web_view.h" +#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck #include "third_party/blink/renderer/platform/media/multi_buffer_data_source.h" // nogncheck #include "third_party/blink/renderer/platform/weborigin/scheme_registry.h" // nogncheck #include "third_party/widevine/cdm/buildflags.h" @@ -361,6 +363,13 @@ void RendererClientBase::GetInterface( } #endif +void RendererClientBase::DidCreateScriptContext( + v8::Isolate* isolate, + v8::Local context, + content::RenderFrame* render_frame) { + RegisterOomStackTraceCallback(isolate); +} + void RendererClientBase::DidClearWindowObject( content::RenderFrame* render_frame) { // Make sure every page will get a script context created. @@ -536,6 +545,23 @@ void RendererClientBase::WillDestroyServiceWorkerContextOnWorkerThread( #endif } +void RendererClientBase::WorkerScriptReadyForEvaluationOnWorkerThread( + v8::Local context) { + // Worklets can share a thread and isolate (via WorkletThreadHolder), so the + // per-thread OOM state would be prematurely removed when the first worklet + // is destroyed. Skip worklets for now; can be revisited with ref-counting. + if (blink::ExecutionContext::From(context)->IsWorkletGlobalScope()) + return; + RegisterOomStackTraceCallback(v8::Isolate::GetCurrent()); +} + +void RendererClientBase::WillDestroyWorkerContextOnWorkerThread( + v8::Local context) { + if (blink::ExecutionContext::From(context)->IsWorkletGlobalScope()) + return; + UnregisterOomStackTraceCallback(v8::Isolate::GetCurrent()); +} + void RendererClientBase::WebViewCreated(blink::WebView* web_view, bool was_created_by_renderer, const url::Origin* outermost_origin) { diff --git a/shell/renderer/renderer_client_base.h b/shell/renderer/renderer_client_base.h index dd8fa7cd3b..2731aca9ea 100644 --- a/shell/renderer/renderer_client_base.h +++ b/shell/renderer/renderer_client_base.h @@ -58,7 +58,7 @@ class RendererClientBase : public content::ContentRendererClient virtual void DidCreateScriptContext(v8::Isolate* isolate, v8::Local context, - content::RenderFrame* render_frame) = 0; + content::RenderFrame* render_frame); virtual void WillReleaseScriptContext(v8::Isolate* isolate, v8::Local context, content::RenderFrame* render_frame) = 0; @@ -141,6 +141,10 @@ class RendererClientBase : public content::ContentRendererClient const GURL& service_worker_scope, const GURL& script_url, const blink::ServiceWorkerToken& service_worker_token) override; + void WorkerScriptReadyForEvaluationOnWorkerThread( + v8::Local context) override; + void WillDestroyWorkerContextOnWorkerThread( + v8::Local context) override; void WebViewCreated(blink::WebView* web_view, bool was_created_by_renderer, const url::Origin* outermost_origin) override; diff --git a/spec/api-crash-reporter-spec.ts b/spec/api-crash-reporter-spec.ts index 683acf1b4c..63201a5bcf 100644 --- a/spec/api-crash-reporter-spec.ts +++ b/spec/api-crash-reporter-spec.ts @@ -33,6 +33,13 @@ type CrashInfo = { longParam: string | undefined 'electron.v8-fatal.location': string | undefined 'electron.v8-fatal.message': string | undefined + 'electron.v8-oom.stack': string | undefined + 'electron.v8-oom.heap.used': string | undefined + 'electron.v8-oom.heap.total': string | undefined + 'electron.v8-oom.heap.limit': string | undefined + 'electron.v8-oom.heap.utilization_pct': string | undefined + 'electron.v8-oom.heap.native_contexts': string | undefined + 'electron.v8-oom.heap.detached_contexts': string | undefined } function checkCrash (expectedProcessType: string, fields: CrashInfo) { @@ -305,6 +312,43 @@ ifdescribe(!isLinuxOnArm && !process.mas && !process.env.DISABLE_CRASH_REPORTER_ expect(crash['electron.v8-fatal.message']).to.equal('Circular extension dependency'); }); }); + + describe('OOM crash keys', () => { + it('reports OOM stack trace and heap statistics when renderer runs out of memory', async function () { + this.timeout(120000); + const { port, waitForCrash } = await startServer(); + runCrashApp('renderer-oom', port, ['--js-flags=--max-old-space-size=128']); + const crash = await waitForCrash(); + expect(crash.process_type).to.equal('renderer'); + expect(crash['electron.v8-oom.stack']).to.be.a('string'); + expect(crash['electron.v8-oom.stack']).to.include('oomTrigger'); + expect(crash['electron.v8-oom.heap.used']).to.be.a('string'); + expect(crash['electron.v8-oom.heap.limit']).to.be.a('string'); + }); + + it('captures the calling function on JSON.stringify OOM', async function () { + this.timeout(120000); + const { port, waitForCrash } = await startServer(); + runCrashApp('renderer-oom-json', port, ['--js-flags=--max-old-space-size=128']); + const crash = await waitForCrash(); + expect(crash.process_type).to.equal('renderer'); + expect(crash['electron.v8-oom.stack']).to.be.a('string'); + expect(crash['electron.v8-oom.stack']).to.include('serializeData'); + }); + + it('captures OOM crash keys inside a web worker', async function () { + this.timeout(120000); + const { port, waitForCrash } = await startServer(); + runCrashApp('renderer-oom-worker', port, ['--js-flags=--max-old-space-size=128']); + const crash = await waitForCrash(); + expect(crash.process_type).to.equal('renderer'); + const oomStack = crash['electron.v8-oom.stack']; + expect(oomStack).to.be.a('string'); + expect(oomStack!.length).to.be.greaterThan(0); + expect(crash['electron.v8-oom.heap.used']).to.be.a('string'); + expect(crash['electron.v8-oom.heap.limit']).to.be.a('string'); + }); + }); }); ifdescribe(!isLinuxOnArm)('extra parameter limits', () => { diff --git a/spec/fixtures/apps/crash/main.js b/spec/fixtures/apps/crash/main.js index bcb1bea2bb..0329e1319e 100644 --- a/spec/fixtures/apps/crash/main.js +++ b/spec/fixtures/apps/crash/main.js @@ -64,6 +64,53 @@ app.whenReady().then(() => { `); }); w.loadURL('about:blank'); + } else if (crashType === 'renderer-oom') { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.webContents.on('render-process-gone', () => process.exit(0)); + w.webContents.on('did-finish-load', () => { + w.webContents.executeJavaScript(` + function oomTrigger() { + const arr = []; + while (true) arr.push(new Array(10000).fill('x'.repeat(100))); + } + oomTrigger(); + `); + }); + w.loadURL('about:blank'); + } else if (crashType === 'renderer-oom-json') { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.webContents.on('render-process-gone', () => process.exit(0)); + w.webContents.on('did-finish-load', () => { + w.webContents.executeJavaScript(` + function serializeData() { + const results = []; + while (true) { + const chunk = {}; + for (let i = 0; i < 1000; i++) chunk['k' + i] = 'x'.repeat(500); + results.push(JSON.stringify(chunk)); + } + } + serializeData(); + `); + }); + w.loadURL('about:blank'); + } else if (crashType === 'renderer-oom-worker') { + const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); + w.webContents.on('render-process-gone', () => process.exit(0)); + w.webContents.on('did-finish-load', () => { + w.webContents.executeJavaScript(` + const blob = new Blob([\` + function workerLeakMemory() { + const arr = []; + while (true) { arr.push(new Array(1000).fill("x".repeat(1000))); } + } + function triggerWorkerOom() { workerLeakMemory(); } + triggerWorkerOom(); + \`], { type: 'application/javascript' }); + const worker = new Worker(URL.createObjectURL(blob)); + `); + }); + w.loadURL('about:blank'); } else if (crashType === 'node') { const crashPath = path.join(__dirname, 'node-crash.js'); const child = childProcess.fork(crashPath, { silent: true });