import type { ProcessMemoryInfo, ProcessMetric } from 'electron/main' import { app } from 'electron/main' const PROCESS_METRICS_INTERVAL_MS = 60_000 const PROCESS_METRICS_MAX_SAMPLES = 12 const PROCESS_METRICS_RECENT_WINDOW = 5 interface SampledProcessMetric { pid: number creationTime: number type: ProcessMetric['type'] name: string | null serviceName: string | null workingSetSize: number peakWorkingSetSize: number privateBytes: number | null percentCPUUsage: number sandboxed: boolean | null } interface ProcessMetricsSample { capturedAt: number mainProcess: { pid: number memory: ProcessMemoryInfo | null } totals: { processCount: number workingSetSize: number privateBytes: number } processes: SampledProcessMetric[] } const samples: ProcessMetricsSample[] = [] let samplerHandle: ReturnType | null = null function normalizeProcessMetric(metric: ProcessMetric): SampledProcessMetric { return { pid: metric.pid, creationTime: metric.creationTime, type: metric.type, name: metric.name ?? null, serviceName: metric.serviceName ?? null, workingSetSize: metric.memory.workingSetSize, peakWorkingSetSize: metric.memory.peakWorkingSetSize, privateBytes: metric.memory.privateBytes ?? null, percentCPUUsage: Number(metric.cpu.percentCPUUsage.toFixed(2)), sandboxed: metric.sandboxed ?? null, } } async function captureMainProcessMemory(): Promise { try { return await process.getProcessMemoryInfo() } catch (error) { console.warn('[desktop-metrics] process.getProcessMemoryInfo failed', error) return null } } export async function sampleProcessMetrics(): Promise { const [mainProcessMemory, processMetrics] = await Promise.all([ captureMainProcessMemory(), Promise.resolve(app.getAppMetrics()), ]) const processes = processMetrics.map(normalizeProcessMetric) const sample: ProcessMetricsSample = { capturedAt: Date.now(), mainProcess: { pid: process.pid, memory: mainProcessMemory, }, totals: { processCount: processes.length, workingSetSize: processes.reduce((total, item) => total + item.workingSetSize, 0), privateBytes: processes.reduce((total, item) => total + (item.privateBytes ?? 0), 0), }, processes, } samples.push(sample) if (samples.length > PROCESS_METRICS_MAX_SAMPLES) { samples.splice(0, samples.length - PROCESS_METRICS_MAX_SAMPLES) } } export function initProcessMetricsSampler(): void { if (samplerHandle) { return } void sampleProcessMetrics() samplerHandle = setInterval(() => { void sampleProcessMetrics() }, PROCESS_METRICS_INTERVAL_MS) samplerHandle.unref?.() } export function getProcessMetricsSnapshot() { const latestSample = samples.at(-1) ?? null return { intervalMs: PROCESS_METRICS_INTERVAL_MS, sampleCount: samples.length, latestCapturedAt: latestSample?.capturedAt ?? null, latestSample, recentSamples: samples.slice(-PROCESS_METRICS_RECENT_WINDOW), } }