215 lines
4.8 KiB
TypeScript
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 ?? {}
|
|
};
|
|
}
|