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
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>
342 lines
8.2 KiB
TypeScript
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 ?? {}
|
|
};
|
|
}
|