Repository navigation
perf(window): re-enable macOS main-window background throttling - #9395
Conversation
Browser-guest surfaces stay alive via their own per-guest unthrottle at webview attach, so the main window no longer needs throttling disabled to avoid black webview surfaces. Keep the black-surface hardening as repaint on show/restore/focus plus one debounced 250ms late repaint, and add a regression test that fails if setBackgroundThrottling(false) returns. Co-authored-by: Orca <help@stably.ai>
With background throttling re-enabled, Chromium's intensive mode clamps hidden-page timers to one per minute after 5 minutes, delaying agent-done and bell notifications by up to 60s. Disable the IntensiveWakeUpThrottling feature so hidden timers keep the normal 1s clamp while rAF and rendering still stop. Verified live: 1.5s timer chain fired on schedule through a 6.5-minute hidden soak (was 60s gaps). Co-authored-by: Orca <help@stably.ai>
webContents can be destroyed a beat before its BrowserWindow during close, and forceRepaint runs from timers and focus events that can land in that gap. Co-authored-by: Orca <help@stably.ai>
… opt-out Co-authored-by: Orca <help@stably.ai>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe startup GPU configuration now appends 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 611bf6d9-8dcb-48f5-8f0a-8d93c2bd2dae
📒 Files selected for processing (4)
src/main/startup/configure-process.test.tssrc/main/startup/configure-process.tssrc/main/window/createMainWindow.test.tssrc/main/window/createMainWindow.ts
Latent-risk closure: every open risk from the adversarial review empirically verifiedFollow-up pass closing the residual risks flagged in the review section, each with a live test on this branch's dev build (macOS, OS-level 1. Occlusion-uncover WITHOUT focus (the top residual risk) — ✅ no stale/black surfaceThe one path with no Electron event coverage: another app's window fully covers Orca, then moves away without Orca ever being clicked — no Test: positioned Orca at a fixed frame, fully covered it with a focused TextEdit window for 90 seconds, then moved TextEdit away by repositioning TextEdit's own window (Orca never focused), and captured Orca's window at the OS level 2s later: Fully rendered — sidebar, browser pane, file panel, status bar. Chromium's own occlusion→repaint handled the uncover with zero help from our event handlers. (Consistent with the earlier probe finding that macOS occlusion detection is conservative: a 20s full cover never even stopped the renderer's frame production.) 2. Maximized windows get invalidate-only repaint (no setSize jiggle) — ✅ clean restore after eviction-window soak
Correct pixels everywhere, webview included — 3.
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f604f9cd-f3ad-4688-82c6-f80a89ee7ba8
📒 Files selected for processing (2)
src/main/window/createMainWindow.test.tssrc/main/window/createMainWindow.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/window/createMainWindow.ts
Per-element infinite CSS spin animations kept Chromium's frame pipeline awake for the whole agent run — measured live (interleaved A/B/A/B, renderer+GPU): 86.0/76.0 ms CPU/s with the CSS animation vs 61.0/61.0 with the clock, one working agent, and the CSS cost scales per element while the clock is one flat timer for N spinners. The clock ticks 12 steps/s at 30° — frame-for-frame identical to the retired animation — writes style.transform on registered elements, and stops on document hidden (native visibilitychange post-#9395), under prefers-reduced-motion (static ring keeps the filled top border from #9515), and when the last spinner unmounts. The stale-visibility latch keeps it spinning when proven user input contradicts the occlusion tracker. Co-authored-by: Orca <help@stably.ai>
…wake (one shared clock, hidden-window stop) (#9380) * perf(renderer): drive agent working-spinners from one shared clock Per-element infinite CSS spin animations kept Chromium's frame pipeline awake for the whole agent run — measured live (interleaved A/B/A/B, renderer+GPU): 86.0/76.0 ms CPU/s with the CSS animation vs 61.0/61.0 with the clock, one working agent, and the CSS cost scales per element while the clock is one flat timer for N spinners. The clock ticks 12 steps/s at 30° — frame-for-frame identical to the retired animation — writes style.transform on registered elements, and stops on document hidden (native visibilitychange post-#9395), under prefers-reduced-motion (static ring keeps the filled top border from #9515), and when the last spinner unmounts. The stale-visibility latch keeps it spinning when proven user input contradicts the occlusion tracker. Co-authored-by: Orca <help@stably.ai> * fix(github): restore explicit space after Filter fallback in PRFilterSections Pre-existing failure on main: i18n-jsx-spacing-guard requires {' '} after the 'Filter' translate fallback, but the file had a bare JSX space (which oxfmt collapses). Wrap the subject in a span so the explicit {' '} survives formatting, matching the pattern in the other guarded files. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
The iOS MJPEG and Android scrcpy device streams are gated only on the pane being the active tab (isActive, PR #7382). When the emulator tab is frontmost but the whole Orca window is hidden/minimized/occluded/display-asleep, the full-fps pipeline keeps running: main-process socket read + JPEG/H.264 decode + IPC + renderer decode. Renderer background-throttling (#9395) cannot stop it because the pipeline is IPC-push driven from main. Gate showStream additionally on window visibility via a new occlusion-safe hook that honors the terminal stale-visibility latch (so a display-sleep occlusion wedge can't freeze the emulator on a black frame) and delays the visible->hidden park by 500ms so a quick Cmd+Tab round-trip doesn't renegotiate the device stream. Co-authored-by: Orca <help@stably.ai>
The iOS MJPEG and Android scrcpy device streams are gated only on the pane being the active tab (isActive, PR #7382). When the emulator tab is frontmost but the whole Orca window is hidden/minimized/occluded/display-asleep, the full-fps pipeline keeps running: main-process socket read + JPEG/H.264 decode + IPC + renderer decode. Renderer background-throttling (#9395) cannot stop it because the pipeline is IPC-push driven from main. Gate showStream additionally on window visibility via a new occlusion-safe hook that honors the terminal stale-visibility latch (so a display-sleep occlusion wedge can't freeze the emulator on a black frame) and delays the visible->hidden park by 500ms so a quick Cmd+Tab round-trip doesn't renegotiate the device stream. Co-authored-by: Orca <help@stably.ai>
…lyai#9395) Co-authored-by: Orca <help@stably.ai>
…wake (one shared clock, hidden-window stop) (stablyai#9380) * perf(renderer): drive agent working-spinners from one shared clock Per-element infinite CSS spin animations kept Chromium's frame pipeline awake for the whole agent run — measured live (interleaved A/B/A/B, renderer+GPU): 86.0/76.0 ms CPU/s with the CSS animation vs 61.0/61.0 with the clock, one working agent, and the CSS cost scales per element while the clock is one flat timer for N spinners. The clock ticks 12 steps/s at 30° — frame-for-frame identical to the retired animation — writes style.transform on registered elements, and stops on document hidden (native visibilitychange post-stablyai#9395), under prefers-reduced-motion (static ring keeps the filled top border from stablyai#9515), and when the last spinner unmounts. The stale-visibility latch keeps it spinning when proven user input contradicts the occlusion tracker. Co-authored-by: Orca <help@stably.ai> * fix(github): restore explicit space after Filter fallback in PRFilterSections Pre-existing failure on main: i18n-jsx-spacing-guard requires {' '} after the 'Filter' translate fallback, but the file had a bare JSX space (which oxfmt collapses). Wrap the subject in a span so the explicit {' '} survives formatting, matching the pattern in the other guarded files. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
…i#9842) The iOS MJPEG and Android scrcpy device streams are gated only on the pane being the active tab (isActive, PR stablyai#7382). When the emulator tab is frontmost but the whole Orca window is hidden/minimized/occluded/display-asleep, the full-fps pipeline keeps running: main-process socket read + JPEG/H.264 decode + IPC + renderer decode. Renderer background-throttling (stablyai#9395) cannot stop it because the pipeline is IPC-push driven from main. Gate showStream additionally on window visibility via a new occlusion-safe hook that honors the terminal stale-visibility latch (so a display-sleep occlusion wedge can't freeze the emulator on a black frame) and delays the visible->hidden park by 500ms so a quick Cmd+Tab round-trip doesn't renegotiate the device stream. Co-authored-by: Orca <help@stably.ai>






TL;DR
Re-enables macOS main-window background throttling — removing the
setBackgroundThrottling(false)workaround that has kept every hidden/occluded Orca window rendering at ~60fps since the in-app browser shipped (#430). Browser webviews stay protected by their own per-guest unthrottle, and the black-surface hardening is kept as repaint-on-transition. Adds a Chromium feature opt-out so hidden-window timers keep 1s granularity instead of degrading to 1/min after 5 minutes.Measured: hidden-window renderer drops from continuous 60fps compositing to 0fps / ~0.0% CPU, with zero timer degradation and no webview black-surface regression through 6.5-minute eviction soaks.
Why the workaround is no longer needed
Deep-dive into Electron/Chromium internals (
disable_hidden.patch,frame_eviction_manager.cc, electron#42378):setBackgroundThrottling(false)works by makingRenderWidgetHostImpl::WasHidden()a no-op — a per-RenderWidgetHost flag. The browser guests get their ownsetBackgroundThrottling(false)at webview attach (browser-manager.ts, needed anyway for CDP screenshots while backgrounded), so guest webview surfaces are never enrolled for frame eviction regardless of the main window's setting. The main-window call was redundant for its stated purpose.setBackgroundThrottlingwhile a window is hidden. We never toggle: the value is set once at window creation, while visible. The new code comment documents this trap.Costs of the old workaround (both verified in the July 2026 perf audit): occluded-but-visible windows render at full frame rate (the biggest remaining battery cost), and
document.visibilityStateis pinned'visible'forever, suppressing every renderer visibility gate.What changed
createMainWindow.ts:setBackgroundThrottling(true)on darwin. Black-surface hardening kept and extended: repaint onrestore/showplus a debounced 250ms late repaint (macOS can finish restoring webview layers after Electron's event), andinvalidate()-only onfocusto cover occlusion-uncover (which fires neitherrestorenorshow). Focus deliberately does not run thesetSizejiggle — that would SIGWINCH every terminal on each Cmd+Tab.configure-process.ts:--disable-features=IntensiveWakeUpThrottling. With throttling back on, Chromium's intensive mode clamps hidden-page timers to 1/min after 5 minutes hidden, which would delay agent-done/bell notifications by up to 60s. This keeps the normal 1s hidden clamp (rAF and rendering still stop — the perf win is untouched) while timers stay usable.setBackgroundThrottling(false)now fails a unit test; focus-repaint asserts nosetSizecalls;IntensiveWakeUpThrottlingopt-out asserted in configure-process tests.Verification (live dev app on macOS, CDP-driven)
Instrumented the real renderer with rAF/timer/visibilitychange probes, drove hide/occlude/restore cycles via System Events, captured OS-level (
screencapture -l) window pixels:visibilitychangeeventsFrameEvictorculling delay), OS-level pixel captureSuites: main 12,826 ✅ · renderer 14,642 ✅ · preload+shared 2,571 ✅ · typecheck ✅ ·
git diff --check✅. (Repo-widepnpm linthas a pre-existing failure on main inskill-freshness-group.tsx, unrelated.)Accepted behavior changes (deliberate, degrade-not-break)
pane-terminal-output-scheduler.tswas built for exactly this; the shell cannot wedge — credits fire before dropping).setTimeouts) can slip to ~2s while hidden — covered by the intensive-throttling opt-out; verified max 2s.Adversarial review findings (subagent, addressed or accepted)
forceRepaint(incl. the ±1pxsetSizejiggle) on every window focus, which would have twitched the window edge and poked terminal ResizeObservers on each Cmd+Tab. Final code invalidates only on focus; a test asserts nosetSize.forceRepaint/focus-invalidate now also checkwebContents.isDestroyed()(webContents can die a beat before its BrowserWindow during close, and these run from timers/focus events).visibilitychangehandlers that were dead on macOS now run on real hide/show flips:refreshAllGitHub()on become-visible (has its own throttle; macOS only flips visibility on ~full occlusion or hide, so frequency is bounded) and the heavy wake-recovery path (glyph-atlas clear + refit) on visibility flips. Both are the intended design finally engaging on macOS, same as they already do on Windows/Linux.restore/show/focusnor (in my live test, where full occlusion by another app never even flipped visibility) avisibilitychange. The window then relies on Chromium's own occlusion→repaint. Mitigations: full-occlusion flips that do change visibility trigger the in-tree wake-recovery self-heal, and my 20s full-occlusion test showed frames never stopped in practice. This narrow case is the main thing perf-rc soaking should watch.History
Supersedes stalled PR #5486 (same core shape; this adds the focus/occlusion-uncover path, the no-jiggle-on-focus fix, and the
IntensiveWakeUpThrottlingopt-out). The original workaround shipped inside #430 with no linked issue; the "floating Claude stuck" concern that motivated keeping renderers unthrottled (#5829) was resolved by #7214's hidden-delivery pipeline, and #5829 was closed as obsolete.Recommend soaking on the perf-rc channel (#7336) before stable, since the original black-surface reports were never captured as issues.