import type { AppMetricsProcessSnapshot, AppMetricsSnapshot } from '../app-metrics'; import type { PerfDiagEntry } from './diagnostics.models'; import { formatWorkingSetGb, HIGH_MEMORY_THRESHOLD_KB } from './high-memory-alert.rules'; import type { SessionContextSnapshot } from './session-context.collector'; export interface RankedProcessSnapshot extends AppMetricsProcessSnapshot { sharePercent: number; } export interface HighMemorySummary { detectedAt: number; thresholdKb: number; thresholdGb: string; totalWorkingSetKb: number; totalWorkingSetGb: string; topProcesses: RankedProcessSnapshot[]; } export interface LatestRendererSamples { store: Record | null; heap: Record | null; components: Record | null; } export function rankProcessesByWorkingSet( processes: readonly AppMetricsProcessSnapshot[], totalWorkingSetKb: number | null ): RankedProcessSnapshot[] { const total = totalWorkingSetKb ?? 0; return [...processes] .filter((process) => process.workingSetKb != null && process.workingSetKb > 0) .sort((left, right) => (right.workingSetKb ?? 0) - (left.workingSetKb ?? 0)) .map((process) => ({ ...process, sharePercent: total > 0 ? Math.round(((process.workingSetKb ?? 0) / total) * 1000) / 10 : 0 })); } export function extractLatestRendererSamples(entries: readonly PerfDiagEntry[]): LatestRendererSamples { let store: Record | null = null; let heap: Record | null = null; let components: Record | null = null; for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; if (entry.source !== 'renderer') { continue; } if (!store && entry.type === 'store') { store = entry.payload; } if (!heap && entry.type === 'heap') { heap = entry.payload; } if (!components && entry.type === 'components') { components = entry.payload; } if (store && heap && components) { break; } } return { store, heap, components }; } export function extractProcessHistory( entries: readonly PerfDiagEntry[], limit = 24 ): Record[] { const history: Record[] = []; for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; if (entry.type !== 'process') { continue; } history.unshift({ collectedAt: entry.collectedAt, ...entry.payload }); if (history.length >= limit) { break; } } return history; } export function summarizeRingBuffer(entries: readonly PerfDiagEntry[]): Record { const counts: Record = {}; for (const entry of entries) { const key = `${entry.source}:${entry.type}`; counts[key] = (counts[key] ?? 0) + 1; } return counts; } export function buildHighMemorySummary( totalWorkingSetKb: number, processes: readonly AppMetricsProcessSnapshot[], detectedAt: number ): HighMemorySummary { return { detectedAt, thresholdKb: HIGH_MEMORY_THRESHOLD_KB, thresholdGb: formatWorkingSetGb(HIGH_MEMORY_THRESHOLD_KB), totalWorkingSetKb, totalWorkingSetGb: formatWorkingSetGb(totalWorkingSetKb), topProcesses: rankProcessesByWorkingSet(processes, totalWorkingSetKb).slice(0, 12) }; } export function formatMemoryUsageMb(memoryUsage: NodeJS.MemoryUsage): Record { return { rssMb: roundMb(memoryUsage.rss), heapTotalMb: roundMb(memoryUsage.heapTotal), heapUsedMb: roundMb(memoryUsage.heapUsed), externalMb: roundMb(memoryUsage.external), arrayBuffersMb: roundMb(memoryUsage.arrayBuffers ?? 0) }; } export function buildHighMemoryDiagnosticPayload(input: { detectedAt: number; totalWorkingSetKb: number; metrics: AppMetricsSnapshot; environment: SessionContextSnapshot; mainProcessMemory: NodeJS.MemoryUsage; ringEntries: readonly PerfDiagEntry[]; immediateRendererEntries: readonly PerfDiagEntry[]; sessionId: string; }): Record { const mergedRingEntries = [...input.ringEntries, ...input.immediateRendererEntries]; const recentRendererSamples = extractLatestRendererSamples(mergedRingEntries); return { event: 'high-memory-threshold', sessionId: input.sessionId, summary: buildHighMemorySummary( input.totalWorkingSetKb, input.metrics.processes, input.detectedAt ), environment: input.environment, metrics: input.metrics, mainProcessMemory: input.mainProcessMemory, mainProcessMemoryMb: formatMemoryUsageMb(input.mainProcessMemory), processHistory: extractProcessHistory(mergedRingEntries), ringSummary: summarizeRingBuffer(mergedRingEntries), recentRendererSamples, immediateRendererSamples: input.immediateRendererEntries.map((entry) => ({ collectedAt: entry.collectedAt, type: entry.type, payload: entry.payload })) }; } function roundMb(bytes: number): number { return Math.round((bytes / (1024 * 1024)) * 100) / 100; }