From 0c767a8bebe63a932c12ef0d4e7035cc5f021932 Mon Sep 17 00:00:00 2001 From: Neil Date: Fri, 26 Jun 2026 13:41:13 -0700 Subject: [PATCH] Add renderer surface crash breadcrumbs --- .../src/lib/crash-diagnostics.test.ts | 59 +++++++++++++++++-- src/renderer/src/lib/crash-diagnostics.ts | 45 ++++++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/lib/crash-diagnostics.test.ts b/src/renderer/src/lib/crash-diagnostics.test.ts index 8abe9aaf9ea..670eb5d2bdf 100644 --- a/src/renderer/src/lib/crash-diagnostics.test.ts +++ b/src/renderer/src/lib/crash-diagnostics.test.ts @@ -11,13 +11,27 @@ describe('renderer crash diagnostics', () => { let setIntervalMock: ReturnType let clearIntervalMock: ReturnType let removeEventListenerMock: ReturnType + let rootElement: { + childElementCount: number + getBoundingClientRect: ReturnType + } + let bodyElement: { + getBoundingClientRect: ReturnType + } beforeEach(async () => { vi.resetModules() listeners = new Map() recordBreadcrumbMock = vi.fn() - setIntervalMock = vi.fn(() => 1) + setIntervalMock = vi.fn(() => setIntervalMock.mock.calls.length + 1) clearIntervalMock = vi.fn() + rootElement = { + childElementCount: 1, + getBoundingClientRect: vi.fn(() => ({ width: 1200, height: 800 })) + } + bodyElement = { + getBoundingClientRect: vi.fn(() => ({ width: 1200, height: 800 })) + } removeEventListenerMock = vi.fn((type: string, listener: Listener) => { listeners.set( type, @@ -38,6 +52,14 @@ describe('renderer crash diagnostics', () => { removeEventListener: removeEventListenerMock, setInterval: setIntervalMock, clearInterval: clearIntervalMock, + innerWidth: 1200, + innerHeight: 800, + devicePixelRatio: 1.5, + getComputedStyle: vi.fn((element: unknown) => + element === rootElement + ? { display: 'block', visibility: 'visible', backgroundColor: 'rgba(0, 0, 0, 0)' } + : { display: 'block', visibility: 'visible', backgroundColor: 'rgb(10, 10, 10)' } + ), performance: { memory: { usedJSHeapSize: 32 * 1024 * 1024, @@ -46,6 +68,12 @@ describe('renderer crash diagnostics', () => { } } }) + vi.stubGlobal('document', { + visibilityState: 'visible', + body: bodyElement, + hasFocus: vi.fn(() => true), + getElementById: vi.fn((id: string) => (id === 'root' ? rootElement : null)) + }) vi.doMock('../components/browser-pane/webview-registry', () => ({ getBrowserWebviewMemoryProfile: () => ({ browserWebviewCount: 4, @@ -68,12 +96,12 @@ describe('renderer crash diagnostics', () => { }) }) - it('installs startup, error, rejection, and memory breadcrumbs once', () => { + it('installs startup, error, rejection, memory, and surface breadcrumbs once', () => { diagnostics.installRendererCrashDiagnostics() diagnostics.installRendererCrashDiagnostics() expect(window.addEventListener).toHaveBeenCalledTimes(2) - expect(setIntervalMock).toHaveBeenCalledTimes(1) + expect(setIntervalMock).toHaveBeenCalledTimes(2) expect(recordBreadcrumbMock).toHaveBeenCalledWith({ name: 'renderer_memory', data: { @@ -85,6 +113,27 @@ describe('renderer crash diagnostics', () => { registeredBrowserGuests: 3 } }) + expect(recordBreadcrumbMock).toHaveBeenCalledWith({ + name: 'renderer_surface', + data: { + reason: 'startup', + visibilityState: 'visible', + hasFocus: true, + innerWidth: 1200, + innerHeight: 800, + devicePixelRatio: 1.5, + rootPresent: true, + rootChildElementCount: 1, + rootWidth: 1200, + rootHeight: 800, + bodyWidth: 1200, + bodyHeight: 800, + rootDisplay: 'block', + rootVisibility: 'visible', + rootBackgroundColor: 'rgba(0, 0, 0, 0)', + bodyBackgroundColor: 'rgb(10, 10, 10)' + } + }) listeners.get('error')?.[0]?.({ message: 'boom', @@ -116,14 +165,14 @@ describe('renderer crash diagnostics', () => { }) }) - it('disposes global listeners and the memory interval', () => { + it('disposes global listeners and diagnostic intervals', () => { diagnostics.installRendererCrashDiagnostics() diagnostics._disposeRendererCrashDiagnosticsForTests() expect(removeEventListenerMock).toHaveBeenCalledWith('error', expect.any(Function)) expect(removeEventListenerMock).toHaveBeenCalledWith('unhandledrejection', expect.any(Function)) - expect(clearIntervalMock).toHaveBeenCalledWith(1) + expect(clearIntervalMock).toHaveBeenCalledTimes(2) expect(listeners.get('error')).toHaveLength(0) expect(listeners.get('unhandledrejection')).toHaveLength(0) }) diff --git a/src/renderer/src/lib/crash-diagnostics.ts b/src/renderer/src/lib/crash-diagnostics.ts index aa7ec860ef6..a61312e9575 100644 --- a/src/renderer/src/lib/crash-diagnostics.ts +++ b/src/renderer/src/lib/crash-diagnostics.ts @@ -5,6 +5,7 @@ import type { import { getBrowserWebviewMemoryProfile } from '../components/browser-pane/webview-registry' const RENDERER_MEMORY_SAMPLE_INTERVAL_MS = 60_000 +const RENDERER_SURFACE_SAMPLE_INTERVAL_MS = 60_000 const BYTES_PER_MEGABYTE = 1024 * 1024 type BrowserPerformanceMemory = { @@ -15,6 +16,7 @@ type BrowserPerformanceMemory = { let rendererCrashDiagnosticsInstalled = false let rendererMemoryInterval: number | null = null +let rendererSurfaceInterval: number | null = null export function recordRendererCrashBreadcrumb( name: string, @@ -49,6 +51,11 @@ export function installRendererCrashDiagnostics(): void { RENDERER_MEMORY_SAMPLE_INTERVAL_MS ) } + recordRendererSurface('startup') + rendererSurfaceInterval = window.setInterval( + () => recordRendererSurface('interval'), + RENDERER_SURFACE_SAMPLE_INTERVAL_MS + ) } export function _disposeRendererCrashDiagnosticsForTests(): void { @@ -66,6 +73,10 @@ function disposeRendererCrashDiagnostics(): void { window.clearInterval(rendererMemoryInterval) rendererMemoryInterval = null } + if (rendererSurfaceInterval !== null) { + window.clearInterval(rendererSurfaceInterval) + rendererSurfaceInterval = null + } } if (typeof import.meta !== 'undefined' && import.meta.hot) { @@ -114,6 +125,40 @@ function recordRendererMemory(reason: string): void { ) } +function recordRendererSurface(reason: string): void { + if (typeof document === 'undefined' || typeof window === 'undefined') { + return + } + + const root = document.getElementById('root') + const rootRect = root?.getBoundingClientRect() + const bodyRect = document.body?.getBoundingClientRect() + const rootStyle = root ? window.getComputedStyle(root) : undefined + const bodyStyle = document.body ? window.getComputedStyle(document.body) : undefined + + recordRendererCrashBreadcrumb( + 'renderer_surface', + compactBreadcrumbData({ + reason, + visibilityState: document.visibilityState, + hasFocus: typeof document.hasFocus === 'function' ? document.hasFocus() : undefined, + innerWidth: window.innerWidth, + innerHeight: window.innerHeight, + devicePixelRatio: window.devicePixelRatio, + rootPresent: Boolean(root), + rootChildElementCount: root?.childElementCount, + rootWidth: rootRect ? Math.round(rootRect.width) : undefined, + rootHeight: rootRect ? Math.round(rootRect.height) : undefined, + bodyWidth: bodyRect ? Math.round(bodyRect.width) : undefined, + bodyHeight: bodyRect ? Math.round(bodyRect.height) : undefined, + rootDisplay: rootStyle?.display, + rootVisibility: rootStyle?.visibility, + rootBackgroundColor: rootStyle?.backgroundColor, + bodyBackgroundColor: bodyStyle?.backgroundColor + }) + ) +} + function getPerformanceMemory(): BrowserPerformanceMemory | undefined { if (typeof window === 'undefined') { return undefined