diff --git a/src/main/crash-reporting/gpu-crash-fallback-decision.ts b/src/main/crash-reporting/gpu-crash-fallback-decision.ts index edf65e4bd71..3741881b51e 100644 --- a/src/main/crash-reporting/gpu-crash-fallback-decision.ts +++ b/src/main/crash-reporting/gpu-crash-fallback-decision.ts @@ -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({ diff --git a/src/main/crash-reporting/process-gone-classification.ts b/src/main/crash-reporting/process-gone-classification.ts index afd4915bf70..ed5d0e02f7b 100644 --- a/src/main/crash-reporting/process-gone-classification.ts +++ b/src/main/crash-reporting/process-gone-classification.ts @@ -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' ) } diff --git a/src/main/crash-reporting/process-gone-recorder.ts b/src/main/crash-reporting/process-gone-recorder.ts index c03050574b8..e9fd5aee98a 100644 --- a/src/main/crash-reporting/process-gone-recorder.ts +++ b/src/main/crash-reporting/process-gone-recorder.ts @@ -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({ diff --git a/src/main/index.ts b/src/main/index.ts index 5746110cb7d..6e735b1ce68 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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. diff --git a/src/main/startup/gpu-fallback-marker.ts b/src/main/startup/gpu-fallback-marker.ts index 79733fc92f2..92840b9f98d 100644 --- a/src/main/startup/gpu-fallback-marker.ts +++ b/src/main/startup/gpu-fallback-marker.ts @@ -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 { diff --git a/src/main/startup/gpu-fallback-tiers.ts b/src/main/startup/gpu-fallback-tiers.ts index e7ac46213bc..ebb8614bd21 100644 --- a/src/main/startup/gpu-fallback-tiers.ts +++ b/src/main/startup/gpu-fallback-tiers.ts @@ -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 diff --git a/src/shared/crash-reporting.ts b/src/shared/crash-reporting.ts index ff0b856fc8a..19db8c3abfe 100644 --- a/src/shared/crash-reporting.ts +++ b/src/shared/crash-reporting.ts @@ -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 )