mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
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:
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user