import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'; import { dirname } from 'node:path'; export const GPU_CRASH_WINDOW_MS = 10 * 60 * 1000; export const GPU_CRASH_THRESHOLD = 2; export type GpuFallbackState = { crashCount: number; lastCrashAt: number; reason: string; }; function isState(value: unknown): value is GpuFallbackState { if (!value || typeof value !== 'object') return false; const record = value as Record; return Number.isFinite(record.crashCount) && Number.isFinite(record.lastCrashAt) && typeof record.reason === 'string'; } export function readGpuFallbackState( filePath: string, now = Date.now(), ): GpuFallbackState | null { try { const parsed: unknown = JSON.parse(readFileSync(filePath, 'utf8')); if (!isState(parsed)) return null; if (now - parsed.lastCrashAt > GPU_CRASH_WINDOW_MS) return null; return parsed; } catch { return null; } } export function shouldUseSoftwareRendering(input: { envDisabled?: boolean; cliDisabled?: boolean; state?: GpuFallbackState | null; now?: number; }): boolean { if (input.envDisabled || input.cliDisabled) return true; const state = input.state; if (!state) return false; const now = input.now ?? Date.now(); return now - state.lastCrashAt <= GPU_CRASH_WINDOW_MS && state.crashCount >= GPU_CRASH_THRESHOLD; } export function recordGpuCrash( filePath: string, reason: string, now = Date.now(), ): GpuFallbackState { const previous = readGpuFallbackState(filePath, now); const next: GpuFallbackState = { crashCount: previous ? previous.crashCount + 1 : 1, lastCrashAt: now, reason, }; mkdirSync(dirname(filePath), { recursive: true }); const temporaryPath = `${filePath}.${process.pid}.tmp`; writeFileSync(temporaryPath, `${JSON.stringify(next)}\n`, 'utf8'); renameSync(temporaryPath, filePath); return next; }