Improve attachment memory safety, downloads, and high-memory alert UX.
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
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>
This commit is contained in:
@@ -10,19 +10,21 @@ import { resolveReadablePath } from '../path-jail';
|
||||
import { sumWorkingSetKb } from './process-metrics.rules';
|
||||
import { isPerfDiagEnabled } from './diagnostics.flags';
|
||||
import { exceedsHighMemoryThreshold } from './high-memory-alert.rules';
|
||||
import { buildHighMemoryDiagnosticPayload } from './high-memory-snapshot.rules';
|
||||
import { collectImmediateRendererSamples } from './immediate-renderer-samples.collector';
|
||||
import { captureHighMemoryDiagnostics } from './high-memory-capture';
|
||||
import { collectSessionContext } from './session-context.collector';
|
||||
import {
|
||||
clearHighMemoryAlert,
|
||||
readHighMemoryAlert,
|
||||
writeHighMemoryAlert
|
||||
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;
|
||||
@@ -67,6 +69,24 @@ export function ensurePerfDiagIpcRegistered(): void {
|
||||
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 {
|
||||
@@ -94,8 +114,48 @@ 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) {
|
||||
@@ -109,10 +169,7 @@ export function startPerfDiagnostics(): PerfDiagWriter | null {
|
||||
});
|
||||
|
||||
activeWriter = writer;
|
||||
highMemoryAlertTriggeredThisSession = false;
|
||||
sessionStartedAt = Date.now();
|
||||
registerProcessCrashHandlers(writer);
|
||||
startProcessMetricsPolling(writer);
|
||||
|
||||
const userDataPath = app.getPath('userData');
|
||||
|
||||
@@ -188,14 +245,15 @@ export async function shutdownPerfDiagnostics(): Promise<void> {
|
||||
}
|
||||
|
||||
await activeWriter.flushSnapshot('shutdown');
|
||||
activeWriter = null;
|
||||
diagnosticsEnabled = false;
|
||||
}
|
||||
|
||||
export function shutdownHighMemoryMonitoring(): void {
|
||||
if (processPollTimer) {
|
||||
clearInterval(processPollTimer);
|
||||
processPollTimer = null;
|
||||
}
|
||||
|
||||
activeWriter = null;
|
||||
diagnosticsEnabled = false;
|
||||
}
|
||||
|
||||
function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
|
||||
@@ -241,34 +299,7 @@ function registerProcessCrashHandlers(writer: PerfDiagWriter): void {
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
void maybeTriggerHighMemoryAlert(writer, metrics, totalKb);
|
||||
} catch {
|
||||
// Collector failures must never affect the app.
|
||||
}
|
||||
};
|
||||
|
||||
sample();
|
||||
processPollTimer = setInterval(sample, PROCESS_POLL_INTERVAL_MS);
|
||||
}
|
||||
|
||||
async function maybeTriggerHighMemoryAlert(
|
||||
writer: PerfDiagWriter,
|
||||
metrics: AppMetricsSnapshot,
|
||||
totalWorkingSetKb: number | null
|
||||
): Promise<void> {
|
||||
@@ -278,51 +309,26 @@ async function maybeTriggerHighMemoryAlert(
|
||||
|
||||
highMemoryAlertTriggeredThisSession = true;
|
||||
|
||||
const detectedAt = Date.now();
|
||||
const userDataPath = app.getPath('userData');
|
||||
const immediateRendererEntries = await collectImmediateRendererSamples(getMainWindow());
|
||||
const environment = collectSessionContext({
|
||||
const record = await captureHighMemoryDiagnostics({
|
||||
userDataPath: app.getPath('userData'),
|
||||
sessionStartedAt,
|
||||
userDataPath
|
||||
metrics,
|
||||
totalWorkingSetKb: totalWorkingSetKb ?? 0,
|
||||
writer: activeWriter,
|
||||
mainWindow: getMainWindow(),
|
||||
reason: 'threshold'
|
||||
});
|
||||
|
||||
for (const entry of immediateRendererEntries) {
|
||||
writer.append(entry);
|
||||
}
|
||||
await persistAndNotifyHighMemoryAlert(record);
|
||||
}
|
||||
|
||||
writer.append({
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'environment',
|
||||
payload: {
|
||||
...environment
|
||||
}
|
||||
});
|
||||
async function persistAndNotifyHighMemoryAlert(record: HighMemoryAlertRecord): Promise<void> {
|
||||
await writeHighMemoryAlert(app.getPath('userData'), record);
|
||||
notifyHighMemoryAlert(record);
|
||||
}
|
||||
|
||||
writer.append({
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'high-memory',
|
||||
payload: buildHighMemoryDiagnosticPayload({
|
||||
detectedAt,
|
||||
totalWorkingSetKb: totalWorkingSetKb ?? 0,
|
||||
metrics,
|
||||
environment,
|
||||
mainProcessMemory: process.memoryUsage(),
|
||||
ringEntries: writer.bufferedEntries,
|
||||
immediateRendererEntries,
|
||||
sessionId: writer.sessionId
|
||||
})
|
||||
});
|
||||
|
||||
await writer.flushSnapshot('high-memory-threshold');
|
||||
|
||||
await writeHighMemoryAlert(userDataPath, {
|
||||
logFilePath: writer.snapshotFilePath,
|
||||
detectedAt,
|
||||
peakWorkingSetKb: totalWorkingSetKb ?? 0,
|
||||
sessionId: writer.sessionId
|
||||
});
|
||||
function notifyHighMemoryAlert(record: HighMemoryAlertRecord): void {
|
||||
getMainWindow()?.webContents.send(HIGH_MEMORY_ALERT_PENDING_CHANNEL, record);
|
||||
}
|
||||
|
||||
function normalizeRendererEntry(entry: PerfDiagEntry): PerfDiagEntry {
|
||||
|
||||
@@ -33,7 +33,8 @@ describe('high-memory-alert.store', () => {
|
||||
logFilePath: path.join(userDataPath, 'diagnostics', 'perf-session.jsonl'),
|
||||
detectedAt: 1_700_000_000_000,
|
||||
peakWorkingSetKb: 2_200_000,
|
||||
sessionId: 'session-1'
|
||||
sessionId: 'session-1',
|
||||
reason: 'threshold' as const
|
||||
};
|
||||
|
||||
await writeHighMemoryAlert(userDataPath, record);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as fsp from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
export type HighMemoryAlertReason = 'manual' | 'threshold';
|
||||
|
||||
export interface HighMemoryAlertRecord {
|
||||
logFilePath: string;
|
||||
detectedAt: number;
|
||||
peakWorkingSetKb: number;
|
||||
sessionId: string;
|
||||
reason?: HighMemoryAlertReason;
|
||||
}
|
||||
|
||||
export function resolveHighMemoryAlertPath(userDataPath: string): string {
|
||||
@@ -31,7 +34,10 @@ export async function readHighMemoryAlert(userDataPath: string): Promise<HighMem
|
||||
logFilePath: parsed.logFilePath,
|
||||
detectedAt: parsed.detectedAt,
|
||||
peakWorkingSetKb: parsed.peakWorkingSetKb,
|
||||
sessionId: parsed.sessionId
|
||||
sessionId: parsed.sessionId,
|
||||
...(parsed.reason === 'manual' || parsed.reason === 'threshold'
|
||||
? { reason: parsed.reason }
|
||||
: {})
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
57
electron/diagnostics/high-memory-capture.spec.ts
Normal file
57
electron/diagnostics/high-memory-capture.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fsp from 'fs/promises';
|
||||
|
||||
import { captureHighMemoryDiagnostics } from './high-memory-capture';
|
||||
|
||||
vi.mock('./immediate-renderer-samples.collector', () => ({
|
||||
collectImmediateRendererSamples: vi.fn(async () => [])
|
||||
}));
|
||||
|
||||
vi.mock('./session-context.collector', () => ({
|
||||
collectSessionContext: vi.fn(() => ({
|
||||
platform: 'linux',
|
||||
userDataPath: '/tmp/user-data'
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('captureHighMemoryDiagnostics', () => {
|
||||
let userDataPath = '';
|
||||
|
||||
beforeEach(async () => {
|
||||
userDataPath = await fsp.mkdtemp(path.join(os.tmpdir(), 'metoyou-high-memory-capture-'));
|
||||
});
|
||||
|
||||
it('writes a diagnostics snapshot and returns an alert record', async () => {
|
||||
const record = await captureHighMemoryDiagnostics({
|
||||
userDataPath,
|
||||
sessionStartedAt: Date.now() - 60_000,
|
||||
metrics: {
|
||||
collectedAt: Date.now(),
|
||||
processes: [
|
||||
{
|
||||
pid: 1,
|
||||
type: 'Browser',
|
||||
workingSetKb: 2_200_000
|
||||
}
|
||||
]
|
||||
},
|
||||
totalWorkingSetKb: 2_200_000,
|
||||
writer: null,
|
||||
mainWindow: null,
|
||||
reason: 'manual'
|
||||
});
|
||||
|
||||
expect(record.peakWorkingSetKb).toBe(2_200_000);
|
||||
expect(record.reason).toBe('manual');
|
||||
expect(record.logFilePath).toContain(userDataPath);
|
||||
await expect(fsp.stat(record.logFilePath)).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
80
electron/diagnostics/high-memory-capture.ts
Normal file
80
electron/diagnostics/high-memory-capture.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { BrowserWindow } from 'electron';
|
||||
import type { AppMetricsSnapshot } from '../app-metrics';
|
||||
import { buildHighMemoryDiagnosticPayload } from './high-memory-snapshot.rules';
|
||||
import { collectImmediateRendererSamples } from './immediate-renderer-samples.collector';
|
||||
import { collectSessionContext } from './session-context.collector';
|
||||
import type { HighMemoryAlertRecord } from './high-memory-alert.store';
|
||||
import type { PerfDiagEntry } from './diagnostics.models';
|
||||
import { PerfDiagWriter } from './diagnostics.writer';
|
||||
|
||||
export type HighMemoryCaptureReason = 'manual' | 'threshold';
|
||||
|
||||
export interface CaptureHighMemoryDiagnosticsInput {
|
||||
userDataPath: string;
|
||||
sessionStartedAt: number;
|
||||
metrics: AppMetricsSnapshot;
|
||||
totalWorkingSetKb: number;
|
||||
writer: PerfDiagWriter | null;
|
||||
mainWindow: BrowserWindow | null;
|
||||
reason: HighMemoryCaptureReason;
|
||||
}
|
||||
|
||||
export async function captureHighMemoryDiagnostics(
|
||||
input: CaptureHighMemoryDiagnosticsInput
|
||||
): Promise<HighMemoryAlertRecord> {
|
||||
const detectedAt = Date.now();
|
||||
const writer = input.writer ?? new PerfDiagWriter({
|
||||
userDataPath: input.userDataPath,
|
||||
sessionId: `${input.reason}-${detectedAt.toString(36)}-${process.pid}`
|
||||
});
|
||||
const immediateRendererEntries = await collectImmediateRendererSamples(input.mainWindow);
|
||||
const environment = collectSessionContext({
|
||||
sessionStartedAt: input.sessionStartedAt,
|
||||
userDataPath: input.userDataPath
|
||||
});
|
||||
|
||||
appendEntries(writer, immediateRendererEntries);
|
||||
appendEntries(writer, [
|
||||
{
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'environment',
|
||||
payload: {
|
||||
...environment
|
||||
}
|
||||
},
|
||||
{
|
||||
collectedAt: detectedAt,
|
||||
source: 'main',
|
||||
type: 'high-memory',
|
||||
payload: buildHighMemoryDiagnosticPayload({
|
||||
detectedAt,
|
||||
totalWorkingSetKb: input.totalWorkingSetKb,
|
||||
metrics: input.metrics,
|
||||
environment,
|
||||
mainProcessMemory: process.memoryUsage(),
|
||||
ringEntries: writer.bufferedEntries,
|
||||
immediateRendererEntries,
|
||||
sessionId: writer.sessionId
|
||||
})
|
||||
}
|
||||
]);
|
||||
|
||||
await writer.flushSnapshot(
|
||||
input.reason === 'manual' ? 'manual-export' : 'high-memory-threshold'
|
||||
);
|
||||
|
||||
return {
|
||||
logFilePath: writer.snapshotFilePath,
|
||||
detectedAt,
|
||||
peakWorkingSetKb: input.totalWorkingSetKb,
|
||||
sessionId: writer.sessionId,
|
||||
reason: input.reason
|
||||
};
|
||||
}
|
||||
|
||||
function appendEntries(writer: PerfDiagWriter, entries: readonly PerfDiagEntry[]): void {
|
||||
for (const entry of entries) {
|
||||
writer.append(entry);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,11 @@ export {
|
||||
attachRendererDiagnosticsHooks,
|
||||
ensurePerfDiagIpcRegistered,
|
||||
getActivePerfDiagWriter,
|
||||
HIGH_MEMORY_ALERT_PENDING_CHANNEL,
|
||||
isPerfDiagActive,
|
||||
shutdownHighMemoryMonitoring,
|
||||
shutdownPerfDiagnostics,
|
||||
startHighMemoryMonitoring,
|
||||
startPerfDiagnostics
|
||||
} from './diagnostics.lifecycle';
|
||||
export type { PerfDiagEntry, PerfDiagEntryType, PerfDiagSource } from './diagnostics.models';
|
||||
|
||||
Reference in New Issue
Block a user