Files
Toju/electron/diagnostics/diagnostics.lifecycle.ts
Myx bb0ac930ad
All checks were successful
Queue Release Build / prepare (push) Successful in 20s
Deploy Web Apps / deploy (push) Successful in 9m2s
Queue Release Build / build-windows (push) Successful in 28m8s
Queue Release Build / build-linux (push) Successful in 47m26s
Queue Release Build / build-android (push) Successful in 19m52s
Queue Release Build / finalize (push) Successful in 4m42s
Improve attachment memory safety, downloads, and high-memory alert UX.
Stream large receives to disk with chunk acks to cap renderer RAM, evict
off-screen display blobs, and route exports through a disk-aware download
service. Fix the high-memory dialog (backdrop dismiss, copy, log actions),
allow diagnostics paths in the path jail, and restore persisted image
hydration after reload.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 00:25:22 +02:00

342 lines
8.2 KiB
TypeScript

import {
app,
BrowserWindow,
ipcMain,
shell
} from 'electron';
import { collectAppMetricsSnapshot, type AppMetricsSnapshot } from '../app-metrics';
import { getMainWindow } from '../window/create-window';
import { resolveReadablePath } from '../path-jail';
import { sumWorkingSetKb } from './process-metrics.rules';
import { isPerfDiagEnabled } from './diagnostics.flags';
import { exceedsHighMemoryThreshold } from './high-memory-alert.rules';
import { captureHighMemoryDiagnostics } from './high-memory-capture';
import { collectSessionContext } from './session-context.collector';
import {
clearHighMemoryAlert,
readHighMemoryAlert,
writeHighMemoryAlert,
type HighMemoryAlertRecord
} from './high-memory-alert.store';
import type { PerfDiagEntry } from './diagnostics.models';
import { PerfDiagWriter } from './diagnostics.writer';
const PROCESS_POLL_INTERVAL_MS = 5_000;
export const HIGH_MEMORY_ALERT_PENDING_CHANNEL = 'high-memory-alert-pending';
let activeWriter: PerfDiagWriter | null = null;
let processPollTimer: NodeJS.Timeout | null = null;
let diagnosticsEnabled = false;
let ipcRegistered = false;
let highMemoryAlertTriggeredThisSession = false;
let sessionStartedAt = 0;
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;
}
});
ipcMain.handle('get-pending-high-memory-alert', async () => {
return readHighMemoryAlert(app.getPath('userData'));
});
ipcMain.handle('acknowledge-high-memory-alert', async () => {
await clearHighMemoryAlert(app.getPath('userData'));
return true;
});
ipcMain.handle('export-high-memory-diagnostics', async () => {
const metrics = collectAppMetricsSnapshot();
const totalKb = sumWorkingSetKb(metrics.processes) ?? 0;
const record = await captureHighMemoryDiagnostics({
userDataPath: app.getPath('userData'),
sessionStartedAt,
metrics,
totalWorkingSetKb: totalKb,
writer: activeWriter,
mainWindow: getMainWindow(),
reason: 'manual'
});
await persistAndNotifyHighMemoryAlert(record);
return record;
});
ipcMain.handle('show-log-file-in-folder', async (_event, filePath: string) => {
if (typeof filePath !== 'string' || !filePath.trim()) {
return {
shown: false,
reason: 'missing-path'
};
}
const scopedPath = await resolveReadablePath(filePath);
if (!scopedPath) {
return {
shown: false,
reason: 'outside-app-data'
};
}
shell.showItemInFolder(scopedPath);
return { shown: true };
});
}
export function getActivePerfDiagWriter(): PerfDiagWriter | null {
return activeWriter;
}
export function startHighMemoryMonitoring(): void {
ensurePerfDiagIpcRegistered();
if (!sessionStartedAt) {
sessionStartedAt = Date.now();
highMemoryAlertTriggeredThisSession = false;
}
if (processPollTimer) {
return;
}
const sample = (): void => {
try {
const metrics = collectAppMetricsSnapshot();
const totalKb = sumWorkingSetKb(metrics.processes);
if (activeWriter && diagnosticsEnabled) {
activeWriter.append({
collectedAt: metrics.collectedAt,
source: 'main',
type: 'process',
payload: {
totalWorkingSetKb: totalKb,
processes: metrics.processes
}
});
}
void maybeTriggerHighMemoryAlert(metrics, totalKb);
} catch {
// Collector failures must never affect the app.
}
};
sample();
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
}
export function startPerfDiagnostics(): PerfDiagWriter | null {
ensurePerfDiagIpcRegistered();
startHighMemoryMonitoring();
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);
const userDataPath = app.getPath('userData');
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'session',
payload: {
event: 'started',
sessionId,
filePath: writer.snapshotFilePath
}
});
writer.append({
collectedAt: Date.now(),
source: 'main',
type: 'environment',
payload: {
...collectSessionContext({
sessionStartedAt,
userDataPath
})
}
});
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');
activeWriter = null;
diagnosticsEnabled = false;
}
export function shutdownHighMemoryMonitoring(): void {
if (processPollTimer) {
clearInterval(processPollTimer);
processPollTimer = null;
}
}
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)
}
});
});
}
async function maybeTriggerHighMemoryAlert(
metrics: AppMetricsSnapshot,
totalWorkingSetKb: number | null
): Promise<void> {
if (highMemoryAlertTriggeredThisSession || !exceedsHighMemoryThreshold(totalWorkingSetKb)) {
return;
}
highMemoryAlertTriggeredThisSession = true;
const record = await captureHighMemoryDiagnostics({
userDataPath: app.getPath('userData'),
sessionStartedAt,
metrics,
totalWorkingSetKb: totalWorkingSetKb ?? 0,
writer: activeWriter,
mainWindow: getMainWindow(),
reason: 'threshold'
});
await persistAndNotifyHighMemoryAlert(record);
}
async function persistAndNotifyHighMemoryAlert(record: HighMemoryAlertRecord): Promise<void> {
await writeHighMemoryAlert(app.getPath('userData'), record);
notifyHighMemoryAlert(record);
}
function notifyHighMemoryAlert(record: HighMemoryAlertRecord): void {
getMainWindow()?.webContents.send(HIGH_MEMORY_ALERT_PENDING_CHANNEL, record);
}
function normalizeRendererEntry(entry: PerfDiagEntry): PerfDiagEntry {
return {
collectedAt: Number(entry.collectedAt) || Date.now(),
source: 'renderer',
type: entry.type,
payload: entry.payload ?? {}
};
}