feat: capture JS stack trace on renderer OOM#50911
Merged
jkleinsc merged 17 commits intoApr 27, 2026
Merged
Conversation
When a renderer process approaches its V8 heap limit, capture the
JavaScript stack trace and write it to both a Crashpad crash key
("js-oom-stack") and stderr.
The stack trace is captured via RequestInterrupt rather than directly
inside the NearHeapLimitCallback because CurrentStackTrace is unsafe
to call during OOM — V8 FATALs on optimized (TurboFan) frames that
have had their deoptimization data garbage-collected. RequestInterrupt
defers the capture to the next V8 safe point, where all frames are
guaranteed to have deopt data available. This matches Node.js's
approach of never capturing JS stacks inside the heap limit callback.
The callback is registered once per isolate via an atomic guard in
RendererClientBase::DidCreateScriptContext, preventing the CHECK
failure V8 raises on duplicate AddNearHeapLimitCallback registrations
(which would otherwise occur on page navigations or multiple contexts).
Refs: #46078
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Co-authored-by: Niklas Wenzel <dev@nikwen.de> Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Co-authored-by: Niklas Wenzel <dev@nikwen.de> Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Add a test that verifies the `electron.v8-oom.stack` crash key contains the JS stack trace (including function names) when a renderer process runs out of memory. Also deduplicate the heap info formatting in oom_stack_trace.cc. Refs: #46078 Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
deepak1556: "Should there be check for available heap size [for] CurrentStackTrace and formatting" CurrentStackTrace allocates StackTraceInfo + StackFrameInfo on the V8 heap. If the 20 MB bump is partially consumed by the time the interrupt fires, these allocations trigger a secondary OOM. Guard with a 2 MB headroom check. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
deepak1556: "Does this bumping work when we are at the cage limit of 4GB" V8's pointer compression cage caps the heap at ~4 GB. When current_heap_limit is already near the ceiling, our 20 MB bump gets clamped to zero and the interrupt never fires. Detect this and record heap info as the final crash key instead of waiting for a stack trace that won't arrive. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
deepak1556: "V8 seems to capture heap stats as crash keys but it gets missed today due to the OOM callback override... wonder if we can include that to get some more heuristics in the dump." Record heap used/total/limit/available, per-space stats for old_space and large_object_space, native/detached context counts, and utilization percentage as crash keys. Also add heap stats in the V8OOMErrorCallback in node_bindings.cc for the final OOM crash report. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
deepak1556: "You need a separate registration for worker threads via WorkerScriptReadyForEvaluationOnWorkerThread but that also means the process global g_registered_isolate would break." Chromium has one V8 isolate per thread (main + one per web worker), so thread_local is equivalent to per-isolate storage. Replace the global atomic + mutex/set with a constinit thread_local OomState* that holds the isolate pointer and per-isolate is_in_oom flag. The void* data parameter on AddNearHeapLimitCallback delivers OomState* directly into callbacks, so the hot path needs no TLS lookup. Add WorkerScriptReadyForEvaluationOnWorkerThread and WillDestroyWorkerContextOnWorkerThread overrides to RendererClientBase so both ElectronRendererClient and ElectronSandboxedRendererClient get worker OOM registration. Update ElectronRendererClient to call the base class in both worker lifecycle methods. Add a web worker OOM test that spawns a dedicated Worker with a memory leak and verifies the stack trace captures the worker function name. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
When context isolation is enabled, ShouldNotifyClient skips DidCreateScriptContext for the main world, but user JS still runs there and can OOM. Register in DidInstallConditionalFeatures which fires for every script context. The TLS dedup guard prevents double-registration on the same isolate. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Add a zero-guard on heap_size_limit before computing utilization percentage — maximizes robustness in an OOM code path. Add static_assert on kPtrComprCageReservationSize to catch any upstream V8 change to the cage size at compile time. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
- Remove redundant RegisterOomStackTraceCallback from electron_render_frame_observer.cc; DidCreateScriptContext is sufficient since main world and isolated world share the same isolate - Replace thread_local OomState* with base::ThreadLocalOwnedPointer wrapped in base::NoDestructor per Chromium style for non-trivially destructible types - Change heap-headroom and cage-limit logs from ERROR to INFO since users cannot act on these diagnostics - Add comment explaining why base class is called last in WillDestroyWorkerContextOnWorkerThread (OOM deregistration ordering) Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Worklets can share a thread and isolate via WorkletThreadHolder's per-process singleton pattern. With per-thread OOM state, the first worklet to be destroyed would prematurely remove the callback for any remaining worklets on the same thread. Skip worklets entirely to avoid this; can be revisited with ref-counting if needed. Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
The OomState held a raw_ptr<v8::Isolate> that outlived the isolate on the main thread: gin::IsolateHolder destroyed the isolate during shutdown, but the OomState (stored in thread-local storage) was only released later in JavascriptEnvironment::~JavascriptEnvironment. This triggers a dangling pointer check when building with enable_dangling_raw_ptr_checks. Register OomState as a gin::PerIsolateData::DisposeObserver so it clears the raw_ptr and removes the NearHeapLimitCallback before the isolate is destroyed, regardless of destructor ordering. Suggested-by: Deepak Mohan Made-with: Cursor Co-authored-by: Alexey Kozy <alexey@anysphere.co>
Replace stderr-based OOM tests with end-to-end crash dump validation.
Instead of parsing log output, start a crash reporter server, trigger
renderer OOM, and verify the uploaded crash dump contains the expected
`electron.v8-oom.*` annotations — the same code path production crash
reports take.
Consolidate all OOM test scenarios (basic heap leak, JSON.stringify,
web worker) into a single `describe('OOM crash keys')` block inside
api-crash-reporter-spec using the existing crash fixture app with new
renderer-oom-json and renderer-oom-worker crash types.
The web worker test verifies that OOM crash keys are present but does
not assert on the JS function name: the 20 MB heap bump may be
exhausted before V8 reaches a safe point to fire the stack-capture
interrupt, leaving the crash key at "(stack pending)". Increasing the
bump or switching to a synchronous capture strategy would fix this but
is left for a follow-up.
Remove the standalone oom-stack-trace-spec.ts and its fixture app.
Made-with: Cursor
Co-authored-by: Alexey Kozy <alexey@anysphere.co>
4 tasks
VerteDinde
approved these changes
Apr 22, 2026
Member
VerteDinde
left a comment
There was a problem hiding this comment.
Approved on behalf of @electron/wg-releases
|
Release Notes Persisted
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Backport of #50043
See that PR for details.
Notes: Added JS stack trace to crash reports on renderer OOM.