test: summarize diagnostic report memory growth

This commit is contained in:
Vincent Koc 2026-04-03 18:19:23 +09:00
parent f9785c63e7
commit c6f95a0c37
2 changed files with 99 additions and 1 deletions

View File

@ -1,4 +1,6 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const ESCAPE = String.fromCodePoint(27);
const BELL = String.fromCodePoint(7);
@ -196,3 +198,86 @@ export function sampleProcessTreeRssKb(rootPid) {
return { rssKb, processCount };
}
const REPORT_FILE_PATTERN =
/^report\.(?<date>\d+)\.(?<time>\d+)\.(?<pid>\d+)\.0\.(?<sequence>\d+)\.json$/u;
function readDiagnosticReport(reportPath) {
try {
const raw = fs.readFileSync(reportPath, "utf8");
const parsed = JSON.parse(raw);
const rssBytes = parsed?.resourceUsage?.rss;
const usedHeapBytes = parsed?.javascriptHeap?.usedMemory;
const externalBytes = parsed?.javascriptHeap?.externalMemory;
if (
!Number.isFinite(rssBytes) ||
!Number.isFinite(usedHeapBytes) ||
!Number.isFinite(externalBytes)
) {
return null;
}
return {
rssKb: Math.round(rssBytes / 1024),
usedHeapKb: Math.round(usedHeapBytes / 1024),
externalKb: Math.round(externalBytes / 1024),
};
} catch {
return null;
}
}
export function summarizeDiagnosticReports(reportDir) {
if (typeof reportDir !== "string" || reportDir.trim() === "") {
return [];
}
let entries;
try {
entries = fs.readdirSync(reportDir, { withFileTypes: true });
} catch {
return [];
}
const reportsByPid = new Map();
for (const entry of entries) {
if (!entry.isFile()) {
continue;
}
const match = entry.name.match(REPORT_FILE_PATTERN);
if (!match?.groups) {
continue;
}
const pid = Number.parseInt(match.groups.pid, 10);
const sequence = Number.parseInt(match.groups.sequence, 10);
if (!Number.isInteger(pid) || !Number.isInteger(sequence)) {
continue;
}
const reportPath = path.join(reportDir, entry.name);
const report = readDiagnosticReport(reportPath);
if (!report) {
continue;
}
const bucket = reportsByPid.get(pid) ?? [];
bucket.push({ pid, sequence, ...report });
reportsByPid.set(pid, bucket);
}
return [...reportsByPid.entries()]
.map(([pid, reports]) => {
const ordered = reports.toSorted((left, right) => left.sequence - right.sequence);
const first = ordered[0];
const last = ordered.at(-1);
if (!first || !last) {
return null;
}
return {
pid,
first,
last,
rssDeltaKb: last.rssKb - first.rssKb,
usedHeapDeltaKb: last.usedHeapKb - first.usedHeapKb,
externalDeltaKb: last.externalKb - first.externalKb,
};
})
.filter((entry) => entry !== null)
.toSorted((left, right) => right.rssDeltaKb - left.rssDeltaKb);
}

View File

@ -6,6 +6,7 @@ import {
getProcessTreeRecords,
parseCompletedTestFileLines,
sampleProcessTreeRssKb,
summarizeDiagnosticReports,
} from "../test-parallel-memory.mjs";
import {
appendCapturedOutput,
@ -650,12 +651,24 @@ export async function executePlan(plan, options = {}) {
.toSorted((left, right) => right.deltaKb - left.deltaKb)
.slice(0, memoryTraceTopCount)
.map((record) => `${record.file}:${formatMemoryDeltaKb(record.deltaKb)}`);
const reportGrowth =
reportOnSignalEnabled && heapSnapshotDir
? summarizeDiagnosticReports(heapSnapshotDir)
.filter((record) => record.rssDeltaKb > 0 || record.usedHeapDeltaKb > 0)
.slice(0, memoryTraceTopCount)
.map(
(record) =>
`pid=${String(record.pid)} rss=${formatMemoryDeltaKb(record.rssDeltaKb)} heap=${formatMemoryDeltaKb(record.usedHeapDeltaKb)} external=${formatMemoryDeltaKb(record.externalDeltaKb)}`,
)
: [];
console.log(
`[test-parallel][mem] summary ${unit.id} files=${memoryFileRecords.length} peak=${formatMemoryKb(
peakTreeSample?.rssKb ?? 0,
)} totalDelta=${formatMemoryDeltaKb(totalDeltaKb)} peakAt=${
peakTreeSample?.reason ?? "n/a"
} top=${topGrowthFiles.length > 0 ? topGrowthFiles.join(", ") : "none"}`,
} top=${topGrowthFiles.length > 0 ? topGrowthFiles.join(", ") : "none"} reports=${
reportGrowth.length > 0 ? reportGrowth.join(", ") : "none"
}`,
);
};
const clearChildTimers = () => {