Files
Toju/electron/diagnostics/diagnostics.lifecycle.ts

215 lines
4.8 KiB
TypeScript

import {
app,
BrowserWindow,
ipcMain
} from 'electron';
import { collectAppMetricsSnapshot } from '../app-metrics';
import { sumWorkingSetKb } from './process-metrics.rules';
import { isPerfDiagEnabled } from './diagnostics.flags';
import type { PerfDiagEntry } from './diagnostics.models';
import { PerfDiagWriter } from './diagnostics.writer';
const PROCESS_POLL_INTERVAL_MS = 5_000;
let activeWriter: PerfDiagWriter | null = null;
let processPollTimer: NodeJS.Timeout | null = null;
let diagnosticsEnabled = false;
let ipcRegistered = false;
export function isPerfDiagActive(): boolean {
return diagnosticsEnabled;
}
export function ensurePerfDiagIpcRegistered(): void {
if (ipcRegistered) {
return;
}
ipcRegistered = true;
ipcMain.handle('perf-diag-is-enabled', () => diagnosticsEnabled);
ipcMain.handle('perf-diag-report', (_event, entry: PerfDiagEntry) => {
const writer = activeWriter;
if (!diagnosticsEnabled || !writer) {
return false;
}
try {
writer.append(normalizeRendererEntry(entry));
return true;
} catch {
return false;
}
});
}
export function getActivePerfDiagWriter(): PerfDiagWriter | null {
return activeWriter;
}
export function startPerfDiagnostics(): PerfDiagWriter | null {
ensurePerfDiagIpcRegistered();
diagnosticsEnabled = isPerfDiagEnabled(process.env, app.isPackaged);
if (!diagnosticsEnabled) {
return null;
}
const sessionId = `${Date.now().toString(36)}-${process.pid}`;
const writer = new PerfDiagWriter({
userDataPath: app.getPath('userData'),
sessionId
});
activeWriter = writer;
registerProcessCrashHandlers(writer);
startProcessMetricsPolling(writer);
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'session',
payload: {
event: 'started',
sessionId,
filePath: writer.snapshotFilePath
}
});
return writer;
}
export function attachRendererDiagnosticsHooks(window: BrowserWindow): void {
const writer = activeWriter;
if (!writer) {
return;
}
window.webContents.on('render-process-gone', (_event, details) => {
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'crash',
payload: {
reason: details.reason,
exitCode: details.exitCode
}
});
void writer.flushSnapshot('render-process-gone');
});
window.webContents.on('unresponsive', () => {
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'unresponsive',
payload: {}
});
});
window.webContents.on('responsive', () => {
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'session',
payload: { event: 'renderer-responsive' }
});
});
}
export async function shutdownPerfDiagnostics(): Promise<void> {
if (!activeWriter) {
return;
}
await activeWriter.flushSnapshot('shutdown');
if (processPollTimer) {
clearInterval(processPollTimer);
processPollTimer = null;
}
activeWriter = null;
diagnosticsEnabled = false;
}
function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
app.on('child-process-gone', (_event, details) => {
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'crash',
payload: {
type: details.type,
reason: details.reason,
exitCode: details.exitCode,
serviceName: details.serviceName ?? null,
name: details.name ?? null
}
});
});
process.on('uncaughtException', (error) => {
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'crash',
payload: {
scope: 'main-uncaughtException',
message: error.message
}
});
void writer.flushSnapshot('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'crash',
payload: {
scope: 'main-unhandledRejection',
reason: String(reason)
}
});
});
}
function startProcessMetricsPolling(writer: PerfDiagWriter): void {
const sample = (): void => {
try {
const metrics = collectAppMetricsSnapshot();
const totalKb = sumWorkingSetKb(metrics.processes);
writer.append({
collectedAt: metrics.collectedAt,
source: 'main',
type: 'process',
payload: {
totalWorkingSetKb: totalKb,
processes: metrics.processes
}
});
} catch {
// Collector failures must never affect the app.
}
};
sample();
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
}
function normalizeRendererEntry(entry: PerfDiagEntry): PerfDiagEntry {
return {
collectedAt: Number(entry.collectedAt) || Date.now(),
source: 'renderer',
type: entry.type,
payload: entry.payload ?? {}
};
}