fix(gpu): drop the in-process-gpu tier from the fallback ladder

--in-process-gpu removes the GPU child entirely, but it also turns a
recoverable child fault into a main-process kill. Nothing in the crash
evidence says tier 2 is insufficient, so the strongest rung was risk
without support. Ladder is now 1 -> 2.
This commit is contained in:
Jinjing
2026-08-01 12:17:50 -07:00
parent 1e96c29d59
commit 4d39763c8f
3 changed files with 29 additions and 31 deletions
+3 -3
View File
@@ -51,8 +51,8 @@ describe('gpu-fallback-marker', () => {
it('overwrites an existing marker when escalating tiers', () => {
writeGpuFallbackMarker(userDataPath, { engagedAt: 1, crashesInWindow: 3, tier: 1 }, environment)
writeGpuFallbackMarker(userDataPath, { engagedAt: 2, crashesInWindow: 4, tier: 3 }, environment)
expect(readGpuFallbackMarker(userDataPath)?.tier).toBe(3)
writeGpuFallbackMarker(userDataPath, { engagedAt: 2, crashesInWindow: 4, tier: 2 }, environment)
expect(readGpuFallbackMarker(userDataPath)?.tier).toBe(2)
})
it('returns null when no marker exists', () => {
@@ -90,7 +90,7 @@ describe('gpu-fallback-marker', () => {
platform: 'win32'
})
)
expect(readGpuFallbackMarker(userDataPath)?.tier).toBe(3)
expect(readGpuFallbackMarker(userDataPath)?.tier).toBe(2)
})
it('defaults a marker with no tier to the first rung', () => {
+16 -15
View File
@@ -11,8 +11,7 @@ describe('gpu-fallback-tiers', () => {
it('escalates one rung at a time and stops at the top', () => {
expect(getNextGpuFallbackTier(0)).toBe(1)
expect(getNextGpuFallbackTier(1)).toBe(2)
expect(getNextGpuFallbackTier(2)).toBe(3)
expect(getNextGpuFallbackTier(3)).toBeNull()
expect(getNextGpuFallbackTier(2)).toBeNull()
})
// Why: null is what bounds relaunches per build; a non-null answer here is a crash loop.
@@ -35,30 +34,32 @@ describe('gpu-fallback-tiers', () => {
it('identifies ladder members', () => {
expect(isGpuFallbackTier(1)).toBe(true)
expect(isGpuFallbackTier(0)).toBe(false)
expect(isGpuFallbackTier(4)).toBe(false)
expect(isGpuFallbackTier(3)).toBe(false)
})
// Why: escalating must never drop a switch a lower tier already needed.
it('is additive across tiers', () => {
const names = ([1, 2, 3] as const).map(
(tier) => new Set(getGpuFallbackTierSwitches(tier).map((entry) => entry.name))
)
for (const name of names[0]) {
expect(names[1].has(name)).toBe(true)
}
for (const name of names[1]) {
expect(names[2].has(name)).toBe(true)
const tier1 = new Set(getGpuFallbackTierSwitches(1).map((entry) => entry.name))
const tier2 = new Set(getGpuFallbackTierSwitches(2).map((entry) => entry.name))
for (const name of tier1) {
expect(tier2.has(name)).toBe(true)
}
})
it('removes the GPU child process at the top tier', () => {
const tier1 = getGpuFallbackTierSwitches(1).map((entry) => entry.name)
expect(tier1).toEqual(['disable-gpu'])
it('takes the compositor off the GPU child and ANGLE off the vendor driver at tier 2', () => {
expect(getGpuFallbackTierSwitches(1).map((entry) => entry.name)).toEqual(['disable-gpu'])
const tier2 = getGpuFallbackTierSwitches(2)
expect(tier2.map((entry) => entry.name)).toContain('disable-gpu-compositing')
expect(tier2.find((entry) => entry.name === 'use-angle')?.value).toBe('swiftshader')
})
expect(getGpuFallbackTierSwitches(3).map((entry) => entry.name)).toContain('in-process-gpu')
// Why: in-process-gpu turns a recoverable child fault into a main-process kill.
it('never offers in-process-gpu on any rung', () => {
for (const tier of [1, 2] as const) {
expect(getGpuFallbackTierSwitches(tier).map((entry) => entry.name)).not.toContain(
'in-process-gpu'
)
}
})
})
+10 -13
View File
@@ -3,20 +3,24 @@
*
* `--disable-gpu` alone does not stop Chromium from spawning a GPU child for the
* Viz display compositor, so a driver that CHECK-crashes at GPU init keeps
* killing every launch even with the fallback applied. Each tier removes
* strictly more of the GPU process from the path; tier 3 removes the child
* process entirely, which is the only shape a GPU-init crash cannot survive.
* killing every launch even with the fallback applied (20 of 21 crashed launches
* in the reported bundle already carried tier 1). Tier 2 takes the compositor off
* the GPU child and forces ANGLE onto SwiftShader so the vendor DLL is never loaded.
*
* The ladder deliberately stops there. `--in-process-gpu` would remove the child
* entirely, but it also turns a recoverable child fault into a main-process kill,
* and nothing in the evidence says tier 2 is insufficient.
*
* Tiers are additive on purpose: escalating never drops a switch that a lower
* tier already needed.
*/
export const GPU_FALLBACK_TIERS = [1, 2, 3] as const
export const GPU_FALLBACK_TIERS = [1, 2] as const
export type GpuFallbackTier = (typeof GPU_FALLBACK_TIERS)[number]
export const MIN_GPU_FALLBACK_TIER: GpuFallbackTier = 1
export const MAX_GPU_FALLBACK_TIER: GpuFallbackTier = 3
export const MAX_GPU_FALLBACK_TIER: GpuFallbackTier = 2
/** Tier 0 means "no fallback applied to this launch". */
export const NO_GPU_FALLBACK_TIER = 0
@@ -31,13 +35,6 @@ const TIER_SWITCHES: Record<GpuFallbackTier, readonly GpuFallbackSwitch[]> = {
{ name: 'disable-gpu-compositing' },
// Why: keeps a broken vendor D3D11 driver DLL out of the process even when Chromium still initializes ANGLE.
{ name: 'use-angle', value: 'swiftshader' }
],
3: [
{ name: 'disable-gpu' },
{ name: 'disable-gpu-compositing' },
{ name: 'use-angle', value: 'swiftshader' },
// Why: last resort — no GPU child exists, so a GPU-init CHECK crash can no longer loop the app.
{ name: 'in-process-gpu' }
]
}
@@ -63,7 +60,7 @@ export function getGpuFallbackTierSwitches(tier: GpuFallbackTier): readonly GpuF
/**
* Next rung above `currentTier` (0 = nothing applied yet), or null once the
* ladder is exhausted. Null is what bounds relaunches to 3 per build.
* ladder is exhausted. Null is what bounds relaunches per build.
*/
export function getNextGpuFallbackTier(currentTier: number): GpuFallbackTier | null {
if (Number.isNaN(currentTier) || currentTier < MIN_GPU_FALLBACK_TIER) {