diff --git a/src/renderer/src/lib/crash-diagnostics.test.ts b/src/renderer/src/lib/crash-diagnostics.test.ts index d72649d3e40..24c4ded56c9 100644 --- a/src/renderer/src/lib/crash-diagnostics.test.ts +++ b/src/renderer/src/lib/crash-diagnostics.test.ts @@ -9,12 +9,21 @@ describe('renderer crash diagnostics', () => { let listeners: Map let recordBreadcrumbMock: ReturnType let setIntervalMock: ReturnType + let clearIntervalMock: ReturnType + let removeEventListenerMock: ReturnType beforeEach(async () => { vi.resetModules() listeners = new Map() recordBreadcrumbMock = vi.fn() setIntervalMock = vi.fn(() => 1) + clearIntervalMock = vi.fn() + removeEventListenerMock = vi.fn((type: string, listener: Listener) => { + listeners.set( + type, + (listeners.get(type) ?? []).filter((candidate) => candidate !== listener) + ) + }) vi.stubGlobal('window', { api: { crashReports: { @@ -26,7 +35,9 @@ describe('renderer crash diagnostics', () => { current.push(listener) listeners.set(type, current) }), + removeEventListener: removeEventListenerMock, setInterval: setIntervalMock, + clearInterval: clearIntervalMock, performance: { memory: { usedJSHeapSize: 32 * 1024 * 1024, @@ -97,6 +108,18 @@ describe('renderer crash diagnostics', () => { }) }) + it('disposes global listeners and the memory interval', () => { + diagnostics.installRendererCrashDiagnostics() + + diagnostics._disposeRendererCrashDiagnosticsForTests() + + expect(removeEventListenerMock).toHaveBeenCalledWith('error', expect.any(Function)) + expect(removeEventListenerMock).toHaveBeenCalledWith('unhandledrejection', expect.any(Function)) + expect(clearIntervalMock).toHaveBeenCalledWith(1) + expect(listeners.get('error')).toHaveLength(0) + expect(listeners.get('unhandledrejection')).toHaveLength(0) + }) + it('does not throw when preload is unavailable', () => { vi.stubGlobal('window', {}) diff --git a/src/renderer/src/lib/crash-diagnostics.ts b/src/renderer/src/lib/crash-diagnostics.ts index 8cf76a86927..9068c49b42b 100644 --- a/src/renderer/src/lib/crash-diagnostics.ts +++ b/src/renderer/src/lib/crash-diagnostics.ts @@ -13,6 +13,7 @@ type BrowserPerformanceMemory = { } let rendererCrashDiagnosticsInstalled = false +let rendererMemoryInterval: number | null = null export function recordRendererCrashBreadcrumb( name: string, @@ -42,10 +43,36 @@ export function installRendererCrashDiagnostics(): void { if (getPerformanceMemory()) { recordRendererMemory('startup') - window.setInterval(() => recordRendererMemory('interval'), RENDERER_MEMORY_SAMPLE_INTERVAL_MS) + rendererMemoryInterval = window.setInterval( + () => recordRendererMemory('interval'), + RENDERER_MEMORY_SAMPLE_INTERVAL_MS + ) } } +export function _disposeRendererCrashDiagnosticsForTests(): void { + disposeRendererCrashDiagnostics() +} + +function disposeRendererCrashDiagnostics(): void { + if (!rendererCrashDiagnosticsInstalled || typeof window === 'undefined') { + return + } + rendererCrashDiagnosticsInstalled = false + window.removeEventListener('error', recordRendererError) + window.removeEventListener('unhandledrejection', recordRendererUnhandledRejection) + if (rendererMemoryInterval !== null) { + window.clearInterval(rendererMemoryInterval) + rendererMemoryInterval = null + } +} + +if (typeof import.meta !== 'undefined' && import.meta.hot) { + // Why: Vite can replace this module without a full renderer reload. Remove + // global diagnostics hooks so dev sessions do not accumulate listeners. + import.meta.hot.dispose(disposeRendererCrashDiagnostics) +} + function recordRendererError(event: ErrorEvent): void { recordRendererCrashBreadcrumb( 'renderer_error',