fix(gpu): centralize process type detection and harden marker durability

- Extract isGpuProcessType to shared module to prevent classification drift across crash reporting, fallback budget, and dialog logic
- Add fsync() before marker rename to survive power loss during persistence
- Resolve GPU fallback environment before recording breadcrumb, so triage doesn't see an engaged fallback that never ran
- Derive MIN/MAX tier constants from GPU_FALLBACK_TIERS array to keep clamping in sync if the ladder changes
This commit is contained in:
Jinjing
2026-08-01 12:17:51 -07:00
parent 8843199afb
commit a1ed1e8fe6
7 changed files with 51 additions and 19 deletions
@@ -1,3 +1,5 @@
import { isGpuProcessType } from '../../shared/crash-reporting'
export type GpuCrashFallbackOptions = {
/** Rolling span over which clustered GPU crashes indicate a broken driver. */
windowMs: number
@@ -86,7 +88,7 @@ export class GpuCrashFallbackTracker {
/** True for the Chromium child process types whose crashes should count here. */
export function isGpuChildProcessType(processType: string | undefined): boolean {
return (processType ?? '').toLowerCase() === 'gpu'
return isGpuProcessType(processType)
}
export function isGpuFallbackCrashCandidate({
@@ -1,3 +1,5 @@
import { isGpuProcessType } from '../../shared/crash-reporting'
export type ProcessGoneSource = 'renderer' | 'child'
export type ExpectedTeardownScope = 'none' | 'renderer-reload' | 'app-shutdown'
@@ -48,7 +50,7 @@ function isRecoverableChromiumChildProcess({
// still tears the GPU child down noisily, so app-shutdown stays suppressed.
return !(
gpuFallbackActive === true &&
normalizedProcessType === 'gpu' &&
isGpuProcessType(processType) &&
expectedTeardown !== 'app-shutdown'
)
}
@@ -2,6 +2,7 @@ import os from 'node:os'
import { app } from 'electron'
import {
isCrashReportReason,
isGpuProcessType,
sanitizeCrashReportString,
type CrashReportBreadcrumbData
} from '../../shared/crash-reporting'
@@ -60,7 +61,7 @@ function countsAgainstGpuFallbackReportBudget(event: ProcessGoneCrashEvent): boo
return (
event.gpuFallbackActive === true &&
event.source === 'child' &&
event.processType.toLowerCase() === 'gpu'
isGpuProcessType(event.processType)
)
}
@@ -161,7 +162,7 @@ export function recordProcessGoneCrash(
// Why: GPU and renderer deaths are the ones triage needs driver identity for;
// every other child type would just pad the report.
const gpuIdentity =
event.source === 'renderer' || event.processType.toLowerCase() === 'gpu'
event.source === 'renderer' || isGpuProcessType(event.processType)
? (getGpuInfoSnapshot() ?? {})
: {}
const crashDetails = buildProcessGoneCrashDetails({
+13 -10
View File
@@ -1651,6 +1651,12 @@ async function handleGpuChildCrash(reason: string, exitCode: number | null): Pro
})
return
}
// Why: resolve first — a breadcrumb claiming the fallback engaged, written ahead of
// the guard that abandons it, reads in triage as a remediation that never ran.
const environment = getWindowsGpuFallbackEnvironment()
if (!environment) {
return
}
recordCrashBreadcrumb('gpu_fallback_engaged', {
reason,
exitCode,
@@ -1682,10 +1688,6 @@ async function handleGpuChildCrash(reason: string, exitCode: number | null): Pro
recordDurableCrashBreadcrumb('gpu_fallback_restart_deferred', fallbackData)
return
}
const environment = getWindowsGpuFallbackEnvironment()
if (!environment) {
return
}
try {
writeGpuFallbackMarker(
userDataPath,
@@ -2147,12 +2149,13 @@ void app.whenReady().then(async () => {
// Skipped in headless serve: no crash dialog to feed, and 'complete' can provoke a
// GPU child that Xvfb environments have no reason to spawn.
if (!isServeMode) {
void captureGpuIdentity(
(infoType) => app.getGPUInfo(infoType),
GPU_INFO_CAPTURE_TIMEOUT_MS
).then((snapshot) => {
recordCrashBreadcrumb('gpu_info_captured', snapshot)
})
void captureGpuIdentity((infoType) => app.getGPUInfo(infoType), GPU_INFO_CAPTURE_TIMEOUT_MS)
.then((snapshot) => {
recordCrashBreadcrumb('gpu_info_captured', snapshot)
})
.catch(() => {
// best effort; GPU identity is a diagnostic, never a startup dependency
})
}
scheduleGpuFallbackHistoryReset()
// Why: install certificate decisions before any webview or headless window issues its first TLS request.
+18 -2
View File
@@ -1,4 +1,12 @@
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
import {
closeSync,
fsyncSync,
openSync,
readFileSync,
renameSync,
rmSync,
writeFileSync
} from 'node:fs'
import { join } from 'node:path'
import { clampGpuFallbackTier, type GpuFallbackTier } from './gpu-fallback-tiers'
@@ -137,7 +145,15 @@ export function writeGpuFallbackMarker(
// reads as invalid and silently re-arms hardware acceleration next launch.
const tempPath = join(userDataPath, GPU_FALLBACK_MARKER_TEMP_FILE)
try {
writeFileSync(tempPath, JSON.stringify(marker))
// Why: a durable rename over non-durable data leaves an empty marker after power
// loss, which reads as invalid and re-arms hardware acceleration.
const handle = openSync(tempPath, 'w')
try {
writeFileSync(handle, JSON.stringify(marker))
fsyncSync(handle)
} finally {
closeSync(handle)
}
renameSync(tempPath, markerPath(userDataPath))
} catch (error) {
try {
+5 -2
View File
@@ -19,8 +19,11 @@ 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 = 2
// Why: derived, so adding a rung to the ladder cannot leave clamping and escalation pinned to the old ceiling.
export const MIN_GPU_FALLBACK_TIER: GpuFallbackTier = GPU_FALLBACK_TIERS[0]
export const MAX_GPU_FALLBACK_TIER: GpuFallbackTier = GPU_FALLBACK_TIERS.reduce((highest, tier) =>
tier > highest ? tier : highest
)
/** Tier 0 means "no fallback applied to this launch". */
export const NO_GPU_FALLBACK_TIER = 0
+6 -1
View File
@@ -163,6 +163,11 @@ export function isReactErrorBoundaryReport(report: CrashReportRecord): boolean {
)
}
/** Single definition of "this is the GPU child"; crash classification, budget accounting and the dialog must not drift apart. */
export function isGpuProcessType(processType: string | undefined): boolean {
return processType?.toLowerCase() === 'gpu'
}
/**
* A GPU child crash that happened while a GPU fallback tier was already applied.
*
@@ -172,7 +177,7 @@ export function isReactErrorBoundaryReport(report: CrashReportRecord): boolean {
export function isGpuFallbackCrashReport(report: CrashReportRecord): boolean {
return (
report.source === 'child' &&
report.processType.toLowerCase() === 'gpu' &&
isGpuProcessType(report.processType) &&
typeof report.details.gpuFallbackTier === 'number' &&
report.details.gpuFallbackTier > 0
)