perf(window): re-enable macOS main-window background throttling (#9395)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil 2026-07-19 21:30:01 -07:00 committed by GitHub
parent 0d97653e39
commit 74e2cb702c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 151 additions and 14 deletions

View File

@ -297,6 +297,20 @@ describe('enableMainProcessGpuFeatures', () => {
expect(app.commandLine.appendSwitch).not.toHaveBeenCalledWith('enable-unsafe-webgpu')
})
it('opts hidden pages out of intensive wake-up throttling', async () => {
const { app } = await import('electron')
const { enableMainProcessGpuFeatures } = await import('./configure-process')
delete process.env.ORCA_E2E_USER_DATA_DIR
vi.mocked(app.commandLine.appendSwitch).mockClear()
enableMainProcessGpuFeatures()
expect(app.commandLine.appendSwitch).toHaveBeenCalledWith(
'disable-features',
'IntensiveWakeUpThrottling'
)
})
it('raises the WebGL context budget above the 16-context Blink default', async () => {
const { app } = await import('electron')
const { enableMainProcessGpuFeatures } = await import('./configure-process')

View File

@ -344,4 +344,16 @@ export function enableMainProcessGpuFeatures(): void {
if (features) {
app.commandLine.appendSwitch('enable-features', features)
}
const existingDisabledFeatures = app.commandLine.getSwitchValue('disable-features')
// Why: with main-window background throttling on, Chromium's intensive mode
// clamps hidden-page timers to 1/min after 5 minutes, delaying agent-done and
// bell notifications by up to 60s. Keep the normal 1s hidden clamp (rAF and
// rendering still stop) but opt out of the 1/min tier. Callers skip this
// function under GPU fallback (win32-only today); if throttling ever extends
// to Windows, this opt-out must move out of the GPU-gated path.
const disabledFeatures = ['IntensiveWakeUpThrottling', existingDisabledFeatures]
.filter(Boolean)
.join(',')
app.commandLine.appendSwitch('disable-features', disabledFeatures)
}

View File

@ -299,6 +299,80 @@ describe('createMainWindow', () => {
}
})
it('keeps main-window background throttling enabled while repainting macOS visibility transitions', () => {
vi.useFakeTimers()
const windowHandlers = new Map<string, ((...args: any[]) => void)[]>()
let windowSize: [number, number] = [1200, 800]
const webContents = {
on: vi.fn(),
setZoomLevel: vi.fn(),
setBackgroundThrottling: vi.fn(),
invalidate: vi.fn(),
isDestroyed: vi.fn(() => false),
setWindowOpenHandler: vi.fn(),
send: vi.fn(),
isDevToolsOpened: vi.fn(),
openDevTools: vi.fn(),
closeDevTools: vi.fn()
}
const browserWindowInstance = {
webContents,
on: vi.fn((event: string, handler: (...args: any[]) => void) => {
const handlers = windowHandlers.get(event) ?? []
handlers.push(handler)
windowHandlers.set(event, handlers)
}),
isDestroyed: vi.fn(() => false),
isMaximized: vi.fn(() => false),
isFullScreen: vi.fn(() => false),
getSize: vi.fn(() => windowSize),
setSize: vi.fn((width: number, height: number) => {
windowSize = [width, height]
}),
maximize: vi.fn(),
show: vi.fn(),
loadFile: vi.fn(),
loadURL: vi.fn()
}
browserWindowMock.mockImplementation(function () {
return browserWindowInstance
})
withPlatform('darwin', () => createMainWindow(null))
// Why: throttling-off pins visibilityState 'visible' and renders occluded
// windows at full rate; this guards against reintroducing it.
expect(webContents.setBackgroundThrottling).not.toHaveBeenCalledWith(false)
expect(webContents.setBackgroundThrottling).toHaveBeenCalledWith(true)
expect(windowHandlers.get('restore')).toHaveLength(1)
expect(windowHandlers.get('show')).toHaveLength(1)
expect(windowHandlers.get('focus')).toHaveLength(1)
windowHandlers.get('show')?.[0]?.()
windowHandlers.get('restore')?.[0]?.()
expect(webContents.invalidate).toHaveBeenCalledTimes(2)
expect(browserWindowInstance.setSize).toHaveBeenNthCalledWith(1, 1201, 800)
expect(browserWindowInstance.setSize).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(32)
expect(browserWindowInstance.setSize).toHaveBeenNthCalledWith(2, 1200, 800)
vi.advanceTimersByTime(217)
expect(webContents.invalidate).toHaveBeenCalledTimes(2)
vi.advanceTimersByTime(1)
expect(webContents.invalidate).toHaveBeenCalledTimes(3)
// Why: focus covers occlusion-uncover with invalidate only — no setSize
// jiggle that would resize terminals on every window focus.
const setSizeCalls = browserWindowInstance.setSize.mock.calls.length
windowHandlers.get('focus')?.[0]?.()
expect(webContents.invalidate).toHaveBeenCalledTimes(4)
expect(browserWindowInstance.setSize).toHaveBeenCalledTimes(setSizeCalls)
})
it('supports all minus key variants for terminal zoom out', () => {
const windowHandlers: Record<string, (...args: any[]) => void> = {}
const webContents = {

View File

@ -50,23 +50,65 @@ import { buildEditableContextMenuTemplate } from './editable-context-menu'
import { clearTrustedUIRendererWebContentsId, setTrustedUIRendererWebContentsId } from '../ipc/ui'
import { resolveWindowCloseAction } from './window-close-decision'
// Why: show/restore/resume can overlap before the size nudge resets; never
// capture the temporary width as the next repaint's baseline.
const activeRepaintJiggles = new WeakSet<BrowserWindow>()
function forceRepaint(window: BrowserWindow): void {
if (window.isDestroyed()) {
// Why: webContents can be destroyed a beat before the BrowserWindow during
// close, and this runs from timers/focus events that can land in that gap.
if (window.isDestroyed() || window.webContents.isDestroyed()) {
return
}
window.webContents.invalidate()
if (window.isMaximized() || window.isFullScreen()) {
if (window.isMaximized() || window.isFullScreen() || activeRepaintJiggles.has(window)) {
return
}
activeRepaintJiggles.add(window)
const [width, height] = window.getSize()
window.setSize(width + 1, height)
setTimeout(() => {
if (!window.isDestroyed()) {
window.setSize(width, height)
}
activeRepaintJiggles.delete(window)
}, 32)
}
function installMacosVisibilityRepaint(window: BrowserWindow): void {
let delayedRepaintTimer: ReturnType<typeof setTimeout> | null = null
const repaintAfterVisibilityTransition = (): void => {
forceRepaint(window)
if (delayedRepaintTimer) {
clearTimeout(delayedRepaintTimer)
}
// Why: macOS can finish restoring webview compositor layers after Electron's
// show/restore event, so a second paint catches late black-surface recovery.
delayedRepaintTimer = setTimeout(() => {
delayedRepaintTimer = null
forceRepaint(window)
}, 250)
}
const clearDelayedRepaint = (): void => {
if (delayedRepaintTimer) {
clearTimeout(delayedRepaintTimer)
delayedRepaintTimer = null
}
}
window.on('restore', repaintAfterVisibilityTransition)
window.on('show', repaintAfterVisibilityTransition)
// Why: occlusion-uncover fires neither restore nor show; focus is the only
// signal. Invalidate only — the setSize jiggle would SIGWINCH every terminal
// on each Cmd+Tab.
window.on('focus', () => {
if (!window.isDestroyed() && !window.webContents.isDestroyed()) {
window.webContents.invalidate()
}
})
window.on('closed', clearDelayedRepaint)
}
function isMacAppPasteInput(input: Electron.Input): boolean {
return (
process.platform === 'darwin' &&
@ -293,18 +335,13 @@ export function createMainWindow(
setTrustedUIRendererWebContentsId(rendererWebContentsId)
if (process.platform === 'darwin') {
// Why: persistent browser webviews use separate compositor layers, and on
// recent macOS releases those layers can fail to repaint after occlusion or
// restore. Disabling main-window throttling and forcing a repaint on
// visibility transitions hardens Orca against black-surface failures during
// browser-tab restore and tab switching.
mainWindow.webContents.setBackgroundThrottling(false)
mainWindow.on('restore', () => {
forceRepaint(mainWindow)
})
mainWindow.on('show', () => {
forceRepaint(mainWindow)
})
// Why: browser-guest surfaces are kept alive by their own per-guest
// unthrottle (browser-manager attach), so the main window can throttle
// normally while hidden/occluded instead of rendering at full rate.
// Toggling must happen only while visible: flipping it on a hidden window
// desyncs Chromium's frame evictor and blanks the surface (electron#42378).
mainWindow.webContents.setBackgroundThrottling(true)
installMacosVisibilityRepaint(mainWindow)
}
// Why: a focus-preserving system/display wake fires no window focus or