180 lines
5.0 KiB
TypeScript
180 lines
5.0 KiB
TypeScript
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<string, unknown> | null;
|
|
heap: Record<string, unknown> | null;
|
|
components: Record<string, unknown> | 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<string, unknown> | null = null;
|
|
let heap: Record<string, unknown> | null = null;
|
|
let components: Record<string, unknown> | 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<string, unknown>[] {
|
|
const history: Record<string, unknown>[] = [];
|
|
|
|
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<string, number> {
|
|
const counts: Record<string, number> = {};
|
|
|
|
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<string, number> {
|
|
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<string, unknown> {
|
|
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;
|
|
}
|