mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(terminal): retry bounded WebGL recovery on tab reveal
This commit is contained in:
@@ -114,6 +114,7 @@ export type PaneRenderingDiagnostics = {
|
||||
gpuRenderingEnabled: boolean
|
||||
webglAttachmentDeferred: boolean
|
||||
webglDisabledAfterContextLoss: boolean
|
||||
webglContextLossesInWindow?: number
|
||||
webglAttachFailedSinceRecovery: boolean
|
||||
hasComplexScriptOutput: boolean
|
||||
terminalWebglAutoDecision: TerminalWebglAutoDecision
|
||||
@@ -142,6 +143,8 @@ export type ManagedPaneInternal = {
|
||||
gpuRenderingEnabled: boolean
|
||||
webglAttachmentDeferred: boolean
|
||||
webglDisabledAfterContextLoss: boolean
|
||||
// Context losses are retained for a bounded reveal retry policy.
|
||||
webglContextLossTimestamps?: number[]
|
||||
// Hidden retained renderers rebuild at the resume boundary, never behind the hidden surface.
|
||||
webglRebuildDeferred?: boolean
|
||||
// Why per-pane: one pane's failed WebGL attach must not strand every other
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
presentPaneViewport,
|
||||
resetWebglTextureAtlas
|
||||
} from './pane-webgl-renderer'
|
||||
import { rebuildAttachedWebgl, reattachWebglIfNeeded } from './pane-webgl-reattach'
|
||||
import {
|
||||
clearPaneWebglContextLossForRetry,
|
||||
rebuildAttachedWebgl,
|
||||
reattachWebglIfNeeded
|
||||
} from './pane-webgl-reattach'
|
||||
import {
|
||||
releaseHiddenWebglRetention,
|
||||
tryRetainHiddenPanesWebgl
|
||||
@@ -85,7 +89,7 @@ export function resumePaneRendering(
|
||||
clearTerminalWebglAttachBackoff(pane)
|
||||
const rebuildDeferred = pane.webglRebuildDeferred === true
|
||||
pane.webglAttachmentDeferred = false
|
||||
pane.webglDisabledAfterContextLoss = false
|
||||
clearPaneWebglContextLossForRetry(pane)
|
||||
pane.webglRebuildDeferred = false
|
||||
if (pane.webglAddon && isPaneWebglContextLost(pane)) {
|
||||
disposeWebgl(pane)
|
||||
|
||||
@@ -10,6 +10,7 @@ export function collectPaneRenderingDiagnostics(
|
||||
gpuRenderingEnabled: pane.gpuRenderingEnabled,
|
||||
webglAttachmentDeferred: pane.webglAttachmentDeferred,
|
||||
webglDisabledAfterContextLoss: pane.webglDisabledAfterContextLoss,
|
||||
webglContextLossesInWindow: pane.webglContextLossTimestamps?.length ?? 0,
|
||||
webglAttachFailedSinceRecovery: pane.webglAttachFailedSinceRecovery === true,
|
||||
hasComplexScriptOutput: pane.hasComplexScriptOutput,
|
||||
terminalWebglAutoDecision: getTerminalWebglAutoDecision(),
|
||||
|
||||
@@ -235,5 +235,17 @@ describe('schedulePaneRevealRepaint', () => {
|
||||
expect(pane.webglAddon).not.toBeNull()
|
||||
expect(pane.terminal.refresh).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reattaches a pane that lost WebGL while hidden when its tab is revealed', () => {
|
||||
const pane = createPane()
|
||||
pane.webglDisabledAfterContextLoss = true
|
||||
|
||||
schedulePaneRevealPresent(() => [pane])
|
||||
flushFrame()
|
||||
flushFrame()
|
||||
|
||||
expect(pane.webglDisabledAfterContextLoss).toBe(false)
|
||||
expect(pane.webglAddon).not.toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
shouldUseTerminalWebgl
|
||||
} from './pane-webgl-renderer'
|
||||
import { safeFit } from './pane-tree-ops'
|
||||
import { resetPaneWebglContextLosses } from './pane-webgl-context-loss-policy'
|
||||
|
||||
export function applyTerminalGpuAcceleration(
|
||||
panes: Iterable<ManagedPaneInternal>,
|
||||
@@ -26,6 +27,7 @@ export function applyTerminalGpuAcceleration(
|
||||
// renderer; context-loss and attach-failure latches from the old mode
|
||||
// should not pin DOM.
|
||||
pane.webglDisabledAfterContextLoss = false
|
||||
resetPaneWebglContextLosses(pane)
|
||||
pane.webglAttachFailedSinceRecovery = false
|
||||
}
|
||||
if (!shouldUseTerminalWebgl(pane)) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
|
||||
/** Retry a pane after a few transient losses, but stop retrying a persistently unstable context. */
|
||||
export const WEBGL_CONTEXT_LOSS_RETRY_LIMIT = 3
|
||||
export const WEBGL_CONTEXT_LOSS_RETRY_WINDOW_MS = 60_000
|
||||
|
||||
function recentContextLosses(pane: ManagedPaneInternal, now: number): number[] {
|
||||
const cutoff = now - WEBGL_CONTEXT_LOSS_RETRY_WINDOW_MS
|
||||
return (pane.webglContextLossTimestamps ?? []).filter((timestamp) => timestamp > cutoff)
|
||||
}
|
||||
|
||||
export function recordPaneWebglContextLoss(pane: ManagedPaneInternal, now = Date.now()): number {
|
||||
const losses = recentContextLosses(pane, now)
|
||||
losses.push(now)
|
||||
pane.webglContextLossTimestamps = losses
|
||||
return losses.length
|
||||
}
|
||||
|
||||
export function canRetryPaneWebglAfterContextLoss(
|
||||
pane: ManagedPaneInternal,
|
||||
now = Date.now()
|
||||
): boolean {
|
||||
const losses = recentContextLosses(pane, now)
|
||||
pane.webglContextLossTimestamps = losses
|
||||
return losses.length < WEBGL_CONTEXT_LOSS_RETRY_LIMIT
|
||||
}
|
||||
|
||||
export function resetPaneWebglContextLosses(pane: ManagedPaneInternal): void {
|
||||
pane.webglContextLossTimestamps = undefined
|
||||
}
|
||||
@@ -168,6 +168,27 @@ describe('terminal WebGL context recovery', () => {
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
})
|
||||
|
||||
it('stops reveal and resume retries after repeated losses in the retry window', () => {
|
||||
const pane = createPane()
|
||||
|
||||
attachWebgl(pane)
|
||||
fireContextLoss(pane)
|
||||
resumePaneRendering([pane])
|
||||
fireContextLoss(pane)
|
||||
resumePaneRendering([pane])
|
||||
fireContextLoss(pane)
|
||||
|
||||
expect(pane.webglContextLossTimestamps).toHaveLength(3)
|
||||
expect(pane.webglDisabledAfterContextLoss).toBe(true)
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
|
||||
resumePaneRendering([pane])
|
||||
|
||||
expect(pane.webglDisabledAfterContextLoss).toBe(true)
|
||||
expect(pane.webglAddon).toBeNull()
|
||||
expect(pane.terminal.loadAddon).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
// Why exact keys: a GPU death loses every pane's context at once and the
|
||||
// crash ring coalesces the repeats, so the population has to survive on the
|
||||
// payload. It must use the same names the fit-retry crumb uses, or one ring
|
||||
@@ -186,7 +207,12 @@ describe('terminal WebGL context recovery', () => {
|
||||
expect(recorded).toEqual([
|
||||
{
|
||||
kind: 'webgl-context-loss',
|
||||
detail: { paneId: 1, livePanes: expect.any(Number), livePaneManagers: expect.any(Number) }
|
||||
detail: {
|
||||
paneId: 1,
|
||||
lossesInWindow: 1,
|
||||
livePanes: expect.any(Number),
|
||||
livePaneManagers: expect.any(Number)
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import type { ManagedPaneInternal } from './pane-manager-types'
|
||||
import { attachWebgl, clearTerminalWebglAttachBackoff, disposeWebgl } from './pane-webgl-renderer'
|
||||
import { canRetryPaneWebglAfterContextLoss } from './pane-webgl-context-loss-policy'
|
||||
|
||||
export function clearPaneWebglContextLossForRetry(pane: ManagedPaneInternal): boolean {
|
||||
if (!pane.webglDisabledAfterContextLoss) {
|
||||
return true
|
||||
}
|
||||
if (!canRetryPaneWebglAfterContextLoss(pane)) {
|
||||
return false
|
||||
}
|
||||
pane.webglDisabledAfterContextLoss = false
|
||||
return true
|
||||
}
|
||||
|
||||
export function reattachWebglIfNeeded(pane: ManagedPaneInternal): void {
|
||||
if (pane.gpuRenderingEnabled && !pane.webglAddon && !pane.webglDisabledAfterContextLoss) {
|
||||
if (pane.gpuRenderingEnabled && !pane.webglAddon && clearPaneWebglContextLossForRetry(pane)) {
|
||||
attachWebgl(pane)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { safeFit, safeFitAndThen } from './pane-fit'
|
||||
import { setPaneFitWebglAttachHook } from './pane-fit-webgl-attach-signal'
|
||||
import { repairPaneWebglCanvasDprMismatch } from './terminal-canvas-dpr-repair'
|
||||
import { recordPaneWebglContextLoss } from './pane-webgl-context-loss-policy'
|
||||
|
||||
export const ENABLE_WEBGL_RENDERER = true
|
||||
let suggestedRendererType: 'dom' | undefined
|
||||
@@ -340,15 +341,17 @@ export function attachWebgl(pane: ManagedPaneInternal): void {
|
||||
// once, and the crash-report ring coalesces repeats, so the count has to
|
||||
// be in the payload rather than in the number of crumbs.
|
||||
const census = getLivePaneCensus()
|
||||
recordTerminalWebglDiagnostic('webgl-context-loss', {
|
||||
paneId: pane.id,
|
||||
livePanes: census.panes,
|
||||
livePaneManagers: census.managers
|
||||
})
|
||||
// Why: Chromium starts reclaiming terminal contexts under pressure.
|
||||
// Recreating WebGL for this pane can loop context loss and leave xterm
|
||||
// visually blank, so keep the pane on the DOM renderer until the next
|
||||
// rendering resume (worktree foreground / window wake) retries it.
|
||||
const lossesInWindow = recordPaneWebglContextLoss(pane)
|
||||
recordTerminalWebglDiagnostic('webgl-context-loss', {
|
||||
paneId: pane.id,
|
||||
lossesInWindow,
|
||||
livePanes: census.panes,
|
||||
livePaneManagers: census.managers
|
||||
})
|
||||
pane.webglDisabledAfterContextLoss = true
|
||||
disposeWebgl(pane, { refreshDimensions: true })
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user