perf(window): re-enable macOS main-window background throttling (#9395)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
0d97653e39
commit
74e2cb702c
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue