22import fs from 'node:fs' ;
33import path from 'node:path' ;
44import { execFileSync } from 'node:child_process' ;
5+ import { performance } from 'node:perf_hooks' ;
56import { gzipSync } from 'node:zlib' ;
67
78const 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+
1724const args = parseArgs ( process . argv . slice ( 2 ) ) ;
1825const 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+ } ) ;
2635const baseReport = args . compare ? JSON . parse ( fs . readFileSync ( args . compare , 'utf8' ) ) : null ;
2736
2837if ( 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+
121183function 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+
234330function formatBytes ( value ) {
235331 const absoluteValue = Math . abs ( value ) ;
236332 if ( absoluteValue < 1000 ) return `${ value } B` ;
0 commit comments