fix(profile): expose sampler availability
Some checks are pending
Check / Node 22 (push) Waiting to run

This commit is contained in:
Vincent Koc 2026-04-29 13:11:37 -07:00
parent 7b5f706398
commit e8fb9b4380
No known key found for this signature in database
5 changed files with 180 additions and 39 deletions

View File

@ -58,6 +58,8 @@ export async function buildCiSummary(options = {}) {
importLoopP95Ms: reports.importLoop?.summary?.p95WallMs ?? 0,
importLoopMaxRssMb: reports.importLoop?.summary?.maxPeakRssMb ?? 0,
importLoopMaxCpuMs: reports.importLoop?.summary?.maxCpuMsEstimate ?? 0,
importLoopRssSampleCount: metricSampleCount(reports.importLoop, "rss", "maxPeakRssMb"),
importLoopCpuSampleCount: metricSampleCount(reports.importLoop, "cpu", "maxCpuMsEstimate"),
},
topIssues: topIssues(reports.compatibility),
refRegressions: (reports.refDiff?.regressions ?? []).slice(0, 20),
@ -148,7 +150,7 @@ export function renderCiSummaryMarkdown(summary) {
["Jiti loader candidates", summary.summary.loaderJitiCandidates],
[
"Import loop",
`p50 ${summary.summary.importLoopP50Ms} ms / p95 ${summary.summary.importLoopP95Ms} ms / max RSS ${summary.summary.importLoopMaxRssMb} MB / CPU ${summary.summary.importLoopMaxCpuMs} ms`,
`p50 ${summary.summary.importLoopP50Ms} ms / p95 ${summary.summary.importLoopP95Ms} ms / max RSS ${formatSampledMetric(summary.summary.importLoopMaxRssMb, summary.summary.importLoopRssSampleCount)} / CPU ${formatSampledMetric(summary.summary.importLoopMaxCpuMs, summary.summary.importLoopCpuSampleCount, "ms")}`,
],
],
["Metric", "Value"],
@ -221,3 +223,35 @@ function topIssues(report) {
function markdownTable(rows, headers) {
return renderPaddedMarkdownTable(rows, headers, { nullValue: "-" });
}
function metricSampleCount(report, kind, maxMetric) {
const summaryKey = kind === "rss" ? "rssSampleCount" : "cpuSampleCount";
const summaryCount = report?.summary?.[summaryKey];
if (Number.isFinite(summaryCount)) {
return summaryCount;
}
const sampleCount = inferSampleCount(report?.samples, kind);
if (sampleCount > 0) {
return sampleCount;
}
return (report?.summary?.[maxMetric] ?? 0) > 0 ? 1 : 0;
}
function inferSampleCount(samples = [], kind) {
if (!Array.isArray(samples)) {
return 0;
}
return samples.reduce((sum, sample) => {
if (kind === "rss") {
return sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0));
}
return sum + (sample.cpuSampleCount ?? 0);
}, 0);
}
function formatSampledMetric(value, count, unit = "MB") {
if ((count ?? 0) <= 0) {
return "n/a";
}
return `${value} ${unit}`;
}

View File

@ -30,6 +30,9 @@ export async function buildImportLoopProfile(options = {}) {
}
const wallMs = samples.map((sample) => sample.wallMs).sort((left, right) => left - right);
const rssSampleCount = samples.reduce((sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)), 0);
const cpuSampleCount = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
return {
generatedAt: options.generatedAt ?? defaultImportLoopProfileOptions.generatedAt,
mode: options.mode ?? "subprocess-cold-import-loop",
@ -40,6 +43,9 @@ export async function buildImportLoopProfile(options = {}) {
p95WallMs: percentile(wallMs, 0.95),
maxPeakRssMb: Math.max(0, ...samples.map((sample) => sample.peakRssMb)),
maxCpuMsEstimate: Math.max(0, ...samples.map((sample) => sample.cpuMsEstimate)),
statSampleCount,
rssSampleCount,
cpuSampleCount,
capturedCount: samples.reduce((sum, sample) => sum + sample.capturedCount, 0),
failCount: samples.filter((sample) => sample.exitCode !== 0 || sample.status !== "captured").length,
},
@ -85,7 +91,7 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
"",
"## Summary",
"",
markdownTable(Object.entries(report.summary).map(([key, value]) => [key, value]), ["Metric", "Value"]),
markdownTable(summaryRows(report), ["Metric", "Value"]),
"",
"## Samples",
"",
@ -95,11 +101,12 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
sample.status,
sample.capturedCount,
`${sample.wallMs} ms`,
`${sample.peakRssMb} MB`,
`${sample.cpuMsEstimate} ms`,
formatSampledMetric(sample.peakRssMb, sample.rssSampleCount),
formatSampledMetric(sample.cpuMsEstimate, sample.cpuSampleCount, "ms"),
`${sample.rssSampleCount ?? 0}/${sample.cpuSampleCount ?? 0}`,
sample.exitCode,
]),
["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "Exit"],
["Run", "Status", "Captured", "Wall", "Peak RSS", "CPU Estimate", "RSS/CPU samples", "Exit"],
),
].join("\n");
}
@ -130,10 +137,35 @@ async function runCaptureSample(options) {
peakRssMb: profile.peakRssMb,
peakCpuPercent: profile.peakCpuPercent,
cpuMsEstimate: profile.cpuMsEstimate,
statSampleCount: profile.statSampleCount,
rssSampleCount: profile.rssSampleCount,
cpuSampleCount: profile.cpuSampleCount,
stderrPreview: profile.stderrPreview,
};
}
function summaryRows(report) {
return [
["runs", report.summary.runs],
["p50WallMs", report.summary.p50WallMs],
["p95WallMs", report.summary.p95WallMs],
["maxPeakRssMb", formatSampledMetric(report.summary.maxPeakRssMb, report.summary.rssSampleCount)],
["maxCpuMsEstimate", formatSampledMetric(report.summary.maxCpuMsEstimate, report.summary.cpuSampleCount, "ms")],
["statSampleCount", report.summary.statSampleCount ?? 0],
["rssSampleCount", report.summary.rssSampleCount ?? 0],
["cpuSampleCount", report.summary.cpuSampleCount ?? 0],
["capturedCount", report.summary.capturedCount],
["failCount", report.summary.failCount],
];
}
function formatSampledMetric(value, count, unit = "MB") {
if ((count ?? 0) <= 0) {
return "n/a";
}
return `${value} ${unit}`;
}
function buildCaptureCommand(options) {
if (typeof options.captureCommand === "function") {
return options.captureCommand({

View File

@ -7,8 +7,12 @@ export async function runProfiledProcess(options) {
let firstRssKb = 0;
let peakRssKb = 0;
let peakCpuPercent = 0;
let statSampleCount = 0;
let rssSampleCount = 0;
let cpuSampleCount = 0;
const cpuSamples = [];
let pollInFlight = false;
const pendingStats = new Set();
const child = spawn(options.command, options.args ?? [], {
cwd: options.cwd,
@ -21,27 +25,43 @@ export async function runProfiledProcess(options) {
child.stderr?.on("data", (chunk) => stderr.push(chunk));
const recordStats = (stats) => {
if (stats.rssKb > 0 && firstRssKb === 0) {
if (stats.rssAvailable || stats.cpuAvailable) {
statSampleCount += 1;
}
if (stats.rssAvailable) {
rssSampleCount += 1;
}
if (stats.cpuAvailable) {
cpuSampleCount += 1;
}
if (stats.rssAvailable && stats.rssKb > 0 && firstRssKb === 0) {
firstRssKb = stats.rssKb;
}
peakRssKb = Math.max(peakRssKb, stats.rssKb);
peakCpuPercent = Math.max(peakCpuPercent, stats.cpuPercent);
if (stats.cpuPercent > 0) {
if (stats.rssAvailable) {
peakRssKb = Math.max(peakRssKb, stats.rssKb);
}
if (stats.cpuAvailable) {
peakCpuPercent = Math.max(peakCpuPercent, stats.cpuPercent);
cpuSamples.push(stats.cpuPercent);
}
};
const poll = setInterval(() => {
const sampleStats = () => {
if (pollInFlight) {
return;
}
pollInFlight = true;
readProcessStats(child.pid)
const pending = readProcessStats(child.pid)
.then(recordStats)
.finally(() => {
pollInFlight = false;
pendingStats.delete(pending);
});
}, options.pollMs ?? 100);
pendingStats.add(pending);
};
sampleStats();
const poll = setInterval(sampleStats, options.pollMs ?? 25);
const exitCode = await new Promise((resolve, reject) => {
child.on("error", (error) => {
@ -51,16 +71,10 @@ export async function runProfiledProcess(options) {
child.on("exit", (code) => resolve(code ?? 1));
});
clearInterval(poll);
await Promise.allSettled([...pendingStats]);
const finalStats = await readProcessStats(child.pid);
if (finalStats.rssKb > 0 && firstRssKb === 0) {
firstRssKb = finalStats.rssKb;
}
peakRssKb = Math.max(peakRssKb, finalStats.rssKb);
peakCpuPercent = Math.max(peakCpuPercent, finalStats.cpuPercent);
if (finalStats.cpuPercent > 0) {
cpuSamples.push(finalStats.cpuPercent);
}
recordStats(finalStats);
const wallMs = Math.round(performance.now() - start);
const averageCpuPercent =
@ -79,6 +93,9 @@ export async function runProfiledProcess(options) {
peakCpuPercent: Math.round(peakCpuPercent * 10) / 10,
cpuMsEstimate: Math.round((wallMs * cpuPercentForEstimate) / 100),
harnessHeapDeltaMb: Math.round((heapUsedMb() - heapStartMb) * 10) / 10,
statSampleCount,
rssSampleCount,
cpuSampleCount,
exitCode,
stdoutPreview: previewLines(stdout),
stderrPreview: previewLines(stderr),
@ -87,7 +104,7 @@ export async function runProfiledProcess(options) {
async function readProcessStats(pid) {
if (!pid || process.platform === "win32") {
return { rssKb: 0, cpuPercent: 0 };
return { rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 };
}
return new Promise((resolve) => {
const ps = spawn("ps", ["-o", "rss=", "-o", "%cpu=", "-p", String(pid)], {
@ -95,14 +112,18 @@ async function readProcessStats(pid) {
});
const chunks = [];
ps.stdout.on("data", (chunk) => chunks.push(chunk));
ps.on("error", () => resolve({ rssKb: 0, cpuPercent: 0 }));
ps.on("error", () => resolve({ rssAvailable: false, rssKb: 0, cpuAvailable: false, cpuPercent: 0 }));
ps.on("exit", () => {
const [rssRaw, cpuRaw] = Buffer.concat(chunks).toString("utf8").trim().split(/\s+/);
const rssKb = Number.parseInt(rssRaw, 10);
const cpuPercent = Number.parseFloat(cpuRaw);
const rssAvailable = Number.isFinite(rssKb);
const cpuAvailable = Number.isFinite(cpuPercent);
resolve({
rssKb: Number.isFinite(rssKb) ? rssKb : 0,
cpuPercent: Number.isFinite(cpuPercent) ? cpuPercent : 0,
rssAvailable,
rssKb: rssAvailable ? rssKb : 0,
cpuAvailable,
cpuPercent: cpuAvailable ? cpuPercent : 0,
});
});
});

View File

@ -46,8 +46,8 @@ export async function buildRuntimeProfile(options = {}) {
os: process.platform,
arch: process.arch,
node: process.version,
rssSampler: process.platform === "win32" ? "unavailable" : "ps",
cpuSampler: process.platform === "win32" ? "unavailable" : "ps-percent",
rssSampler: process.platform === "win32" ? "unavailable" : "ps-immediate-25ms",
cpuSampler: process.platform === "win32" ? "unavailable" : "ps-percent-immediate-25ms",
},
summary: summarizeProfile(commands),
groups: summarizeCommandGroups(commands),
@ -65,8 +65,8 @@ export function validateRuntimeProfile(profile) {
errors.push(`${command.id}: missing wall time`);
}
}
if (profile.platform?.rssSampler !== "unavailable" && profile.commands.every((command) => command.peakRssMb.max <= 0)) {
errors.push("all commands are missing peak RSS");
if (profile.platform?.rssSampler !== "unavailable" && profile.commands.every((command) => !hasRssSample(command))) {
errors.push("all commands are missing peak RSS samples");
}
return errors;
}
@ -98,10 +98,17 @@ export function renderRuntimeProfileMarkdown(profile, options = {}) {
[
["Commands", profile.summary.commandCount],
["P50 wall time", `${profile.summary.p50WallMs} ms`],
["P95 wall time", `${profile.summary.p95WallMs} ms`],
["Max peak RSS", `${profile.summary.maxPeakRssMb} MB`],
["Max RSS delta", `${profile.summary.maxRssDeltaMb} MB`],
["Max CPU estimate", `${profile.summary.maxCpuMsEstimate} ms`],
["Command P95 wall time", `${profile.summary.p95WallMs} ms`],
["Wall time basis", profile.summary.wallTimeBasis ?? "command-median-p95"],
["Profile samples", profile.summary.sampleCount ?? sampleCount(profile.commands)],
["RSS samples", profile.summary.rssSampleCount ?? rssSampleCount(profile.commands)],
["CPU samples", profile.summary.cpuSampleCount ?? cpuSampleCount(profile.commands)],
["Max peak RSS", formatSampledMetric(profile.summary.maxPeakRssMb, profile.summary.rssSampleCount ?? rssSampleCount(profile.commands))],
["Max RSS delta", formatSampledMetric(profile.summary.maxRssDeltaMb, profile.summary.rssSampleCount ?? rssSampleCount(profile.commands))],
[
"Max CPU estimate",
formatSampledMetric(profile.summary.maxCpuMsEstimate, profile.summary.cpuSampleCount ?? cpuSampleCount(profile.commands), "ms"),
],
["Max harness heap delta", `${profile.summary.maxHarnessHeapDeltaMb} MB`],
],
["Metric", "Value"],
@ -129,13 +136,14 @@ export function renderRuntimeProfileMarkdown(profile, options = {}) {
command.label,
`${command.wallMs.median} ms`,
`${command.wallMs.max} ms`,
`${command.peakRssMb.max} MB`,
`${command.rssDeltaMb.max} MB`,
`${command.cpuMsEstimate.max} ms`,
formatSampledMetric(command.peakRssMb.max, command.rssSampleCount),
formatSampledMetric(command.rssDeltaMb.max, command.rssSampleCount),
formatSampledMetric(command.cpuMsEstimate.max, command.cpuSampleCount, "ms"),
`${command.harnessHeapDeltaMb.max} MB`,
`${command.rssSampleCount ?? 0}/${command.cpuSampleCount ?? 0}`,
command.exitCodes.join(", "),
]),
["ID", "Label", "Median wall", "Max wall", "Max peak RSS", "Max RSS delta", "CPU estimate", "Heap delta", "Exit codes"],
["ID", "Label", "Median wall", "Max wall", "Max peak RSS", "Max RSS delta", "CPU estimate", "Heap delta", "RSS/CPU samples", "Exit codes"],
),
"",
"## Category Rollups",
@ -146,11 +154,12 @@ export function renderRuntimeProfileMarkdown(profile, options = {}) {
group.commandCount,
`${group.p50WallMs} ms`,
`${group.p95WallMs} ms`,
`${group.maxPeakRssMb} MB`,
`${group.maxCpuMsEstimate} ms`,
formatSampledMetric(group.maxPeakRssMb, group.rssSampleCount),
formatSampledMetric(group.maxCpuMsEstimate, group.cpuSampleCount, "ms"),
`${group.rssSampleCount ?? 0}/${group.cpuSampleCount ?? 0}`,
group.commands.join(", "),
]),
["Category", "Commands", "P50 wall", "P95 wall", "Max peak RSS", "CPU estimate", "Command IDs"],
["Category", "Commands", "P50 wall", "P95 wall", "Max peak RSS", "CPU estimate", "RSS/CPU samples", "Command IDs"],
),
].join("\n");
}
@ -189,8 +198,15 @@ function summarizeProfile(commands) {
const maxRssDeltaMb = Math.max(0, ...commands.map((command) => command.rssDeltaMb.max));
const maxCpuMsEstimate = Math.max(0, ...commands.map((command) => command.cpuMsEstimate.max));
const maxHarnessHeapDeltaMb = Math.max(0, ...commands.map((command) => command.harnessHeapDeltaMb.max));
const totalSampleCount = sampleCount(commands);
const totalRssSampleCount = rssSampleCount(commands);
const totalCpuSampleCount = cpuSampleCount(commands);
return {
commandCount: commands.length,
sampleCount: totalSampleCount,
rssSampleCount: totalRssSampleCount,
cpuSampleCount: totalCpuSampleCount,
wallTimeBasis: "command-median-p95",
p50WallMs: percentile(wallTimes, 0.5),
p95WallMs: percentile(wallTimes, 0.95),
maxPeakRssMb,
@ -206,6 +222,12 @@ function summarizeCommand(command, samples) {
const rssDeltaMb = samples.map((sample) => sample.rssDeltaMb).sort((left, right) => left - right);
const peakCpuPercent = samples.map((sample) => sample.peakCpuPercent).sort((left, right) => left - right);
const cpuMsEstimate = samples.map((sample) => sample.cpuMsEstimate).sort((left, right) => left - right);
const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
const rssSampleTotal = samples.reduce(
(sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)),
0,
);
const cpuSampleTotal = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
const harnessHeapDeltaMb = samples
.map((sample) => sample.harnessHeapDeltaMb)
.sort((left, right) => left - right);
@ -222,6 +244,9 @@ function summarizeCommand(command, samples) {
peakCpuPercent: summarizeNumbers(peakCpuPercent),
cpuMsEstimate: summarizeNumbers(cpuMsEstimate),
harnessHeapDeltaMb: summarizeNumbers(harnessHeapDeltaMb),
statSampleCount,
rssSampleCount: rssSampleTotal,
cpuSampleCount: cpuSampleTotal,
exitCodes: [...new Set(samples.map((sample) => sample.exitCode))].sort(),
};
}
@ -244,6 +269,8 @@ function summarizeCommandGroups(commands) {
const cpuMs = categoryCommands
.flatMap((command) => command.samples.map((sample) => sample.cpuMsEstimate))
.sort((left, right) => left - right);
const groupRssSampleCount = rssSampleCount(categoryCommands);
const groupCpuSampleCount = cpuSampleCount(categoryCommands);
return {
category,
commandCount: categoryCommands.length,
@ -251,11 +278,36 @@ function summarizeCommandGroups(commands) {
p95WallMs: percentile(wallTimes, 0.95),
maxPeakRssMb: peakRss.at(-1) ?? 0,
maxCpuMsEstimate: cpuMs.at(-1) ?? 0,
rssSampleCount: groupRssSampleCount,
cpuSampleCount: groupCpuSampleCount,
commands: categoryCommands.map((command) => command.id),
};
});
}
function hasRssSample(command) {
return (command.rssSampleCount ?? (command.peakRssMb?.max > 0 ? 1 : 0)) > 0;
}
function sampleCount(commands) {
return commands.reduce((sum, command) => sum + (command.samples?.length ?? 0), 0);
}
function rssSampleCount(commands) {
return commands.reduce((sum, command) => sum + (command.rssSampleCount ?? (command.peakRssMb?.max > 0 ? 1 : 0)), 0);
}
function cpuSampleCount(commands) {
return commands.reduce((sum, command) => sum + (command.cpuSampleCount ?? 0), 0);
}
function formatSampledMetric(value, count, unit = "MB") {
if ((count ?? 0) <= 0) {
return "n/a";
}
return `${value} ${unit}`;
}
function summarizeNumbers(values) {
return {
min: values[0],

View File

@ -97,6 +97,8 @@ test("ci summary rolls up compatibility, policy, ref diff, and profile findings"
p95WallMs: 75,
maxPeakRssMb: 40,
maxCpuMsEstimate: 30,
rssSampleCount: 2,
cpuSampleCount: 2,
},
},
},