202 lines
5.4 KiB
TypeScript
202 lines
5.4 KiB
TypeScript
import {
|
|
describe,
|
|
expect,
|
|
it
|
|
} from 'vitest';
|
|
import type { PerfDiagEntry } from './diagnostics.models';
|
|
import {
|
|
buildHighMemoryDiagnosticPayload,
|
|
buildHighMemorySummary,
|
|
extractLatestRendererSamples,
|
|
extractProcessHistory,
|
|
formatMemoryUsageMb,
|
|
rankProcessesByWorkingSet,
|
|
summarizeRingBuffer
|
|
} from './high-memory-snapshot.rules';
|
|
|
|
function createProcess(overrides: Partial<{
|
|
pid: number;
|
|
type: string;
|
|
workingSetKb: number | null;
|
|
peakWorkingSetKb: number | null;
|
|
privateBytesKb: number | null;
|
|
creationTime: number | null;
|
|
cpuPercent: number | null;
|
|
}> = {}) {
|
|
return {
|
|
pid: 1,
|
|
type: 'Tab',
|
|
workingSetKb: 1024,
|
|
peakWorkingSetKb: null,
|
|
privateBytesKb: null,
|
|
creationTime: null,
|
|
cpuPercent: null,
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
describe('high-memory-snapshot.rules', () => {
|
|
it('ranks processes by working set and computes share percentages', () => {
|
|
const tabProcess = createProcess({ pid: 1, type: 'Tab', workingSetKb: 512_000 });
|
|
const gpuProcess = createProcess({ pid: 2, type: 'GPU', workingSetKb: 1_536_000 });
|
|
const ranked = rankProcessesByWorkingSet([tabProcess, gpuProcess], 2_048_000);
|
|
|
|
expect(ranked[0]?.type).toBe('GPU');
|
|
expect(ranked[0]?.sharePercent).toBe(75);
|
|
expect(ranked[1]?.sharePercent).toBe(25);
|
|
});
|
|
|
|
it('extracts the latest renderer store, heap, and component samples', () => {
|
|
const entries: PerfDiagEntry[] = [
|
|
{
|
|
collectedAt: 1,
|
|
source: 'renderer',
|
|
type: 'store',
|
|
payload: { domains: { chat: 100 } }
|
|
},
|
|
{
|
|
collectedAt: 2,
|
|
source: 'renderer',
|
|
type: 'heap',
|
|
payload: { usedJsHeapMb: 120 }
|
|
},
|
|
{
|
|
collectedAt: 3,
|
|
source: 'renderer',
|
|
type: 'components',
|
|
payload: { suspectedLeaks: [{ name: 'ChatMessageItem', count: 40, expected: 20 }] }
|
|
},
|
|
{
|
|
collectedAt: 4,
|
|
source: 'renderer',
|
|
type: 'store',
|
|
payload: { domains: { chat: 500 } }
|
|
}
|
|
];
|
|
|
|
expect(extractLatestRendererSamples(entries)).toEqual({
|
|
store: { domains: { chat: 500 } },
|
|
heap: { usedJsHeapMb: 120 },
|
|
components: { suspectedLeaks: [{ name: 'ChatMessageItem', count: 40, expected: 20 }] }
|
|
});
|
|
});
|
|
|
|
it('extracts recent process history from the ring buffer', () => {
|
|
const entries: PerfDiagEntry[] = [
|
|
{
|
|
collectedAt: 1,
|
|
source: 'main',
|
|
type: 'process',
|
|
payload: { totalWorkingSetKb: 1000 }
|
|
},
|
|
{
|
|
collectedAt: 2,
|
|
source: 'main',
|
|
type: 'session',
|
|
payload: { event: 'noop' }
|
|
},
|
|
{
|
|
collectedAt: 3,
|
|
source: 'main',
|
|
type: 'process',
|
|
payload: { totalWorkingSetKb: 2000 }
|
|
}
|
|
];
|
|
|
|
expect(extractProcessHistory(entries)).toEqual([{ collectedAt: 1, totalWorkingSetKb: 1000 }, { collectedAt: 3, totalWorkingSetKb: 2000 }]);
|
|
});
|
|
|
|
it('summarizes ring buffer entry counts', () => {
|
|
expect(summarizeRingBuffer([
|
|
{ collectedAt: 1, source: 'main', type: 'process', payload: {} },
|
|
{ collectedAt: 2, source: 'renderer', type: 'heap', payload: {} },
|
|
{ collectedAt: 3, source: 'main', type: 'process', payload: {} }
|
|
])).toEqual({
|
|
'main:process': 2,
|
|
'renderer:heap': 1
|
|
});
|
|
});
|
|
|
|
it('builds a high-memory summary with threshold context', () => {
|
|
const summary = buildHighMemorySummary(
|
|
2_200_000,
|
|
[createProcess({ workingSetKb: 2_200_000 })],
|
|
1_700_000_000_000
|
|
);
|
|
|
|
expect(summary.totalWorkingSetGb).toBe('2.10');
|
|
expect(summary.thresholdGb).toBe('2.00');
|
|
expect(summary.topProcesses).toHaveLength(1);
|
|
});
|
|
|
|
it('builds a comprehensive high-memory diagnostic payload', () => {
|
|
const payload = buildHighMemoryDiagnosticPayload({
|
|
detectedAt: 1_700_000_000_000,
|
|
totalWorkingSetKb: 2_200_000,
|
|
metrics: {
|
|
collectedAt: 1_700_000_000_000,
|
|
processes: [
|
|
createProcess({
|
|
workingSetKb: 2_200_000,
|
|
peakWorkingSetKb: 2_300_000,
|
|
privateBytesKb: 1_800_000,
|
|
creationTime: 1,
|
|
cpuPercent: 12
|
|
})
|
|
]
|
|
},
|
|
environment: { appVersion: '1.0.0' },
|
|
mainProcessMemory: {
|
|
rss: 64 * 1024 * 1024,
|
|
heapTotal: 32 * 1024 * 1024,
|
|
heapUsed: 16 * 1024 * 1024,
|
|
external: 8 * 1024 * 1024,
|
|
arrayBuffers: 1024
|
|
},
|
|
ringEntries: [
|
|
{
|
|
collectedAt: 1,
|
|
source: 'main',
|
|
type: 'process',
|
|
payload: { totalWorkingSetKb: 2_000_000 }
|
|
}
|
|
],
|
|
immediateRendererEntries: [
|
|
{
|
|
collectedAt: 2,
|
|
source: 'renderer',
|
|
type: 'heap',
|
|
payload: { usedJsHeapMb: 300, route: '/room/abc' }
|
|
}
|
|
],
|
|
sessionId: 'session-1'
|
|
});
|
|
|
|
expect(payload.event).toBe('high-memory-threshold');
|
|
expect(payload.summary).toMatchObject({
|
|
totalWorkingSetKb: 2_200_000
|
|
});
|
|
|
|
expect(payload.processHistory).toHaveLength(1);
|
|
expect(payload.recentRendererSamples).toEqual({
|
|
store: null,
|
|
heap: { usedJsHeapMb: 300, route: '/room/abc' },
|
|
components: null
|
|
});
|
|
|
|
expect(formatMemoryUsageMb({
|
|
rss: 64 * 1024 * 1024,
|
|
heapTotal: 32 * 1024 * 1024,
|
|
heapUsed: 16 * 1024 * 1024,
|
|
external: 8 * 1024 * 1024,
|
|
arrayBuffers: 1024
|
|
})).toEqual({
|
|
rssMb: 64,
|
|
heapTotalMb: 32,
|
|
heapUsedMb: 16,
|
|
externalMb: 8,
|
|
arrayBuffersMb: 0
|
|
});
|
|
});
|
|
});
|