Files
makelore/src/lib/performance-diagnostics.ts

92 lines
2.6 KiB
TypeScript

import { invokeIpc } from '@/lib/api-client';
type LongTaskAggregate = {
count: number;
totalDurationMs: number;
maxDurationMs: number;
};
type MainPerformanceSnapshot = {
capturedAt?: number;
eventLoopDelayMs?: number;
processes?: Array<unknown>;
windows?: number;
webContents?: number;
gpu?: Record<string, string>;
background?: {
activity?: { visible?: boolean; module?: string | null };
leaseCount?: number;
};
};
/**
* Installs development-only, aggregate renderer diagnostics. No URLs, page
* content, prompts or file names are retained or sent to Main. Production
* builds return a no-op cleanup so this cannot become a hidden polling task.
*/
export function installRendererPerformanceDiagnostics(): () => void {
if (typeof window === 'undefined' || window.electron?.isDev !== true) {
return () => undefined;
}
const longTasks: LongTaskAggregate = {
count: 0,
totalDurationMs: 0,
maxDurationMs: 0,
};
let firstPaintMs: number | null = null;
let disposed = false;
let paintFrame = 0;
const observer = typeof PerformanceObserver === 'undefined'
? null
: new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const duration = Math.round(entry.duration * 100) / 100;
longTasks.count += 1;
longTasks.totalDurationMs += duration;
longTasks.maxDurationMs = Math.max(longTasks.maxDurationMs, duration);
}
});
try {
observer?.observe({ type: 'longtask', buffered: true } as PerformanceObserverInit);
} catch {
// Chromium versions without the Long Tasks entry type are supported.
}
paintFrame = window.requestAnimationFrame(() => {
firstPaintMs = Math.round(performance.now() * 100) / 100;
});
const report = () => {
if (disposed) return;
const renderer = {
firstPaintMs,
longTasks: {
count: longTasks.count,
totalDurationMs: Math.round(longTasks.totalDurationMs * 100) / 100,
maxDurationMs: Math.round(longTasks.maxDurationMs * 100) / 100,
},
};
void invokeIpc<MainPerformanceSnapshot>('app:performance')
.then((main) => {
if (!disposed) {
console.debug('[performance]', { renderer, main });
}
})
.catch(() => undefined);
};
// A low-frequency sample is enough for the development panel and avoids
// adding another active request loop to normal renderer operation.
const timer = window.setInterval(report, 30_000);
return () => {
disposed = true;
window.cancelAnimationFrame(paintFrame);
window.clearInterval(timer);
observer?.disconnect();
};
}