Skip to content

Commit dc9296c

Browse files
committed
ci: report startup timing with size
1 parent 7b794e4 commit dc9296c

3 files changed

Lines changed: 103 additions & 3 deletions

File tree

.github/workflows/size.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ jobs:
3434
git checkout --detach "${{ github.event.pull_request.base.sha }}"
3535
pnpm install --frozen-lockfile
3636
pnpm build
37-
node /tmp/agent-device-size-report.mjs --json /tmp/agent-device-size-base.json
37+
node /tmp/agent-device-size-report.mjs \
38+
--startup-runs 7 \
39+
--json /tmp/agent-device-size-base.json
3840
3941
- name: Measure PR size
4042
run: |
@@ -43,6 +45,7 @@ jobs:
4345
pnpm build
4446
node scripts/size-report.mjs \
4547
--compare /tmp/agent-device-size-base.json \
48+
--startup-runs 7 \
4649
--json .tmp/size-report.json \
4750
--markdown .tmp/size-report.md
4851

scripts/integration-progress.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,7 @@ function readClientCommandMethods() {
514514

515515
function readCommandContractBlocks(text) {
516516
const starts = [
517+
...text.matchAll(/defineExecutableCommand\(\s*metadata\(\s*['"]([^'"]+)['"]\s*\)/g),
517518
...text.matchAll(/defineFieldCommand\(\s*['"]([^'"]+)['"]/g),
518519
...text.matchAll(/defineCommand\(\s*\{[\s\S]*?\bname:\s*['"]([^'"]+)['"]/g),
519520
]

scripts/size-report.mjs

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import fs from 'node:fs';
33
import path from 'node:path';
44
import { execFileSync } from 'node:child_process';
5+
import { performance } from 'node:perf_hooks';
56
import { gzipSync } from 'node:zlib';
67

78
const COMMENT_MARKER = '<!-- agent-device-size-report -->';
@@ -12,8 +13,14 @@ const VALUE_ARGS = new Map([
1213
['--compare', 'compare'],
1314
['--post-comment', 'postComment'],
1415
['--pr', 'pr'],
16+
['--startup-runs', 'startupRuns'],
1517
]);
1618

19+
const STARTUP_BENCHMARKS = [
20+
{ name: 'CLI --version', args: ['--version'] },
21+
{ name: 'CLI --help', args: ['--help'] },
22+
];
23+
1724
const args = parseArgs(process.argv.slice(2));
1825
const cwd = path.resolve(args.cwd ?? process.cwd());
1926

@@ -22,7 +29,9 @@ if (args.postComment) {
2229
process.exit(0);
2330
}
2431

25-
const report = collectReport(cwd);
32+
const report = collectReport(cwd, {
33+
startupRuns: parseNonNegativeInteger(args.startupRuns ?? '0', '--startup-runs'),
34+
});
2635
const baseReport = args.compare ? JSON.parse(fs.readFileSync(args.compare, 'utf8')) : null;
2736

2837
if (args.json) {
@@ -67,6 +76,7 @@ Options:
6776
--json <path> Write the raw size report JSON.
6877
--markdown <path> Write the markdown report.
6978
--compare <path> Compare against a previously written JSON report.
79+
--startup-runs <count> Measure startup medians for side-effect-free CLI commands.
7080
--post-comment <path> Post or update the markdown report on the current PR.
7181
--pr <number> Pull request number for --post-comment.
7282
`);
@@ -81,7 +91,15 @@ function readValue(argv, index, flag) {
8191
return value;
8292
}
8393

84-
function collectReport(root) {
94+
function parseNonNegativeInteger(value, flag) {
95+
const parsed = Number(value);
96+
if (!Number.isInteger(parsed) || parsed < 0) {
97+
throw new Error(`${flag} must be a non-negative integer`);
98+
}
99+
return parsed;
100+
}
101+
102+
function collectReport(root, options) {
85103
const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
86104
const jsFiles = walk(path.join(root, 'dist', 'src')).filter((file) => file.endsWith('.js'));
87105
if (jsFiles.length === 0) {
@@ -114,10 +132,54 @@ function collectReport(root) {
114132
generatedAt: new Date().toISOString(),
115133
js,
116134
npmPack: collectNpmPack(root),
135+
...(options.startupRuns > 0 ? { startup: collectStartupBenchmarks(root, options.startupRuns) } : {}),
117136
chunks: chunks.slice(0, 20),
118137
};
119138
}
120139

140+
function collectStartupBenchmarks(root, runs) {
141+
return {
142+
runs,
143+
benchmarks: STARTUP_BENCHMARKS.map((benchmark) =>
144+
measureStartupBenchmark(root, benchmark, runs),
145+
),
146+
};
147+
}
148+
149+
function measureStartupBenchmark(root, benchmark, runs) {
150+
const samplesMs = [];
151+
runStartupCommand(root, benchmark.args);
152+
for (let index = 0; index < runs; index += 1) {
153+
const start = performance.now();
154+
runStartupCommand(root, benchmark.args);
155+
samplesMs.push(performance.now() - start);
156+
}
157+
const sortedSamples = [...samplesMs].sort((left, right) => left - right);
158+
return {
159+
name: benchmark.name,
160+
command: `agent-device ${benchmark.args.join(' ')}`,
161+
medianMs: median(sortedSamples),
162+
minMs: sortedSamples[0],
163+
maxMs: sortedSamples.at(-1),
164+
samplesMs,
165+
};
166+
}
167+
168+
function runStartupCommand(root, args) {
169+
execFileSync(process.execPath, ['bin/agent-device.mjs', ...args], {
170+
cwd: root,
171+
stdio: 'ignore',
172+
timeout: 5_000,
173+
});
174+
}
175+
176+
function median(sortedValues) {
177+
const midpoint = Math.floor(sortedValues.length / 2);
178+
return sortedValues.length % 2 === 0
179+
? (sortedValues[midpoint - 1] + sortedValues[midpoint]) / 2
180+
: sortedValues[midpoint];
181+
}
182+
121183
function walk(root) {
122184
if (!fs.existsSync(root)) return [];
123185
const entries = fs.readdirSync(root, { withFileTypes: true });
@@ -165,6 +227,7 @@ function formatMarkdown(report, baseReport) {
165227
const changedChunks = baseReport
166228
? formatChangedChunks(report.chunks, baseReport.chunks ?? [])
167229
: formatTopChunks(report.chunks);
230+
const startup = formatStartupBenchmarks(report.startup, baseReport?.startup);
168231

169232
return `${COMMENT_MARKER}
170233
## Size Report
@@ -173,6 +236,7 @@ function formatMarkdown(report, baseReport) {
173236
|---|---:|---:|---:|
174237
${rows.join('\n')}
175238
239+
${startup}
176240
${changedChunks}
177241
`;
178242
}
@@ -231,6 +295,38 @@ function formatDiff(base, current) {
231295
return typeof base === 'number' ? formatSignedBytes(current - base) : '-';
232296
}
233297

298+
function formatStartupBenchmarks(startup, baseStartup) {
299+
if (!startup) return '';
300+
const baseByName = new Map((baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark]));
301+
const rows = startup.benchmarks.map((benchmark) => {
302+
const base = baseByName.get(benchmark.name);
303+
return `| ${benchmark.name} | ${formatMaybeMs(base?.medianMs)} | ${formatMs(benchmark.medianMs)} | ${formatMsDiff(base?.medianMs, benchmark.medianMs)} |`;
304+
});
305+
return `Startup median (${startup.runs} runs, lower is better):
306+
307+
| Scenario | Base | Current | Diff |
308+
|---|---:|---:|---:|
309+
${rows.join('\n')}
310+
311+
`;
312+
}
313+
314+
function formatMaybeMs(value) {
315+
return typeof value === 'number' ? formatMs(value) : '-';
316+
}
317+
318+
function formatMsDiff(base, current) {
319+
if (typeof base !== 'number') return '-';
320+
const diff = current - base;
321+
if (diff === 0) return '0 ms';
322+
const sign = diff > 0 ? '+' : '-';
323+
return `${sign}${formatMs(Math.abs(diff))}`;
324+
}
325+
326+
function formatMs(value) {
327+
return value < 1000 ? `${value.toFixed(1)} ms` : `${(value / 1000).toFixed(2)} s`;
328+
}
329+
234330
function formatBytes(value) {
235331
const absoluteValue = Math.abs(value);
236332
if (absoluteValue < 1000) return `${value} B`;

0 commit comments

Comments
 (0)