@@ -14,58 +14,58 @@ var extraOpts = {};
1414
1515// Parse --key=value options from remaining args
1616for ( var ai = 5 ; ai < process . argv . length ; ai ++ ) {
17- var optMatch = process . argv [ ai ] . match ( / ^ - - ( [ a - z - ] + ) = ( .* ) $ / ) ;
18- if ( optMatch ) { extraOpts [ optMatch [ 1 ] ] = optMatch [ 2 ] ; }
17+ var optMatch = process . argv [ ai ] . match ( / ^ - - ( [ a - z - ] + ) = ( .* ) $ / ) ;
18+ if ( optMatch ) { extraOpts [ optMatch [ 1 ] ] = optMatch [ 2 ] ; }
1919}
2020
2121if ( ! file ) {
22- console . error ( 'Usage: node benchmark-runner.js <file.less> [runs] [warmup]' ) ;
23- process . exit ( 1 ) ;
22+ console . error ( 'Usage: node benchmark-runner.js <file.less> [runs] [warmup]' ) ;
23+ process . exit ( 1 ) ;
2424}
2525
2626// Find Less compiler - try multiple paths for different version eras
2727var less ;
2828var lessPath = '' ;
2929var tryPaths = [
30- // v4.x monorepo (after build)
31- './packages/less' ,
32- // v3.x / v2.x (lib in repo)
33- '.' ,
34- './lib/less-node' ,
35- // Fallback
36- 'less'
30+ // v4.x monorepo (after build)
31+ './packages/less' ,
32+ // v3.x / v2.x (lib in repo)
33+ '.' ,
34+ './lib/less-node' ,
35+ // Fallback
36+ 'less'
3737] ;
3838
3939for ( var i = 0 ; i < tryPaths . length ; i ++ ) {
40- try {
41- var p = tryPaths [ i ] ;
42- // Use path.resolve for relative paths, but keep bare package names for Node resolution
43- var mod = require ( p . startsWith ( '.' ) ? path . resolve ( p ) : p ) ;
44- // Handle both direct export and .default (ESM interop)
45- less = mod && mod . default ? mod . default : mod ;
46- if ( less && ( less . render || less . parse ) ) {
47- lessPath = p ;
48- break ;
49- }
50- less = null ;
51- } catch ( e ) {
40+ try {
41+ var p = tryPaths [ i ] ;
42+ // Use path.resolve for relative paths, but keep bare package names for Node resolution
43+ var mod = require ( p . startsWith ( '.' ) ? path . resolve ( p ) : p ) ;
44+ // Handle both direct export and .default (ESM interop)
45+ less = mod && mod . default ? mod . default : mod ;
46+ if ( less && ( less . render || less . parse ) ) {
47+ lessPath = p ;
48+ break ;
49+ }
50+ less = null ;
51+ } catch ( e ) {
5252 // try next
53- }
53+ }
5454}
5555
5656if ( ! less ) {
57- console . error ( JSON . stringify ( { error : 'Could not find Less compiler' , tried : tryPaths } ) ) ;
58- process . exit ( 2 ) ;
57+ console . error ( JSON . stringify ( { error : 'Could not find Less compiler' , tried : tryPaths } ) ) ;
58+ process . exit ( 2 ) ;
5959}
6060
6161// Determine version
6262var version = 'unknown' ;
6363if ( less . version ) {
64- if ( Array . isArray ( less . version ) ) {
65- version = less . version . join ( '.' ) ;
66- } else {
67- version = String ( less . version ) ;
68- }
64+ if ( Array . isArray ( less . version ) ) {
65+ version = less . version . join ( '.' ) ;
66+ } else {
67+ version = String ( less . version ) ;
68+ }
6969}
7070
7171var filePath = path . resolve ( file ) ;
@@ -78,98 +78,56 @@ var parseTimes = [];
7878var completed = 0 ;
7979var errors = [ ] ;
8080
81+ /**
82+ * Returns the current high-resolution time in milliseconds.
83+ * @returns {number } Current time in ms, with sub-ms precision.
84+ */
8185function hrNow ( ) {
82- var hr = process . hrtime ( ) ;
83- return hr [ 0 ] * 1000 + hr [ 1 ] / 1e6 ;
86+ var hr = process . hrtime ( ) ;
87+ return hr [ 0 ] * 1000 + hr [ 1 ] / 1e6 ;
8488}
8589
90+ /**
91+ * Runs the Less compiler against the input file exactly once, recording
92+ * the elapsed time. Pushes to renderTimes on success; records errors.
93+ * @param {function(Error|null): void } callback Called with an Error if the run failed.
94+ * @returns {void }
95+ */
8696function runOnce ( callback ) {
87- var start = hrNow ( ) ;
88- var opts = {
89- filename : filePath ,
90- paths : [ fileDir ]
91- } ;
92- // Forward extra options (e.g. --math=always)
93- for ( var key in extraOpts ) { opts [ key ] = extraOpts [ key ] ; }
94- less . render ( data , opts , function ( err , output ) {
95- var end = hrNow ( ) ;
96- if ( err ) {
97- errors . push ( { run : completed , error : err . message || String ( err ) } ) ;
98- callback ( err ) ;
99- return ;
100- }
101- renderTimes . push ( end - start ) ;
102- completed ++ ;
103- callback ( null ) ;
104- } ) ;
105- }
10697
98+ /**
99+ * Invokes runOnce repeatedly until totalRuns has been reached, then
100+ * reports results. Bails early after 4 errors to avoid hanging on a broken Less.
101+ * @param {number } i Current iteration counter.
102+ * @returns {void }
103+ */
107104function runAll ( i ) {
108- if ( i >= totalRuns ) {
109- reportResults ( ) ;
110- return ;
111- }
112- runOnce ( function ( err ) {
113- if ( err && errors . length > 3 ) {
114- // Too many errors, bail
115- reportResults ( ) ;
116- return ;
117- }
118- runAll ( i + 1 ) ;
119- } ) ;
120- }
121105
106+ /**
107+ * Computes summary statistics for a list of timing samples, optionally
108+ * skipping the warmup window.
109+ * @param {number[] } times Elapsed-time samples in milliseconds.
110+ * @param {boolean } skipWarmup When true, the first warmupRuns samples are dropped.
111+ * @returns {{
112+ * min: number,
113+ * max: number,
114+ * avg: number,
115+ * median: number,
116+ * stddev: number,
117+ * variance_pct: number,
118+ * samples: number,
119+ * throughput_kbs: number
120+ * }|null } Summary stats, or null if there are too few samples.
121+ */
122122function analyze ( times , skipWarmup ) {
123- var start = skipWarmup ? warmupRuns : 0 ;
124- if ( times . length <= start ) return null ;
125- var effective = times . slice ( start ) ;
126- var total = 0 , min = Infinity , max = 0 ;
127- for ( var i = 0 ; i < effective . length ; i ++ ) {
128- total += effective [ i ] ;
129- min = Math . min ( min , effective [ i ] ) ;
130- max = Math . max ( max , effective [ i ] ) ;
131- }
132- var avg = total / effective . length ;
133-
134- // Median
135- var sorted = effective . slice ( ) . sort ( function ( a , b ) { return a - b ; } ) ;
136- var mid = Math . floor ( sorted . length / 2 ) ;
137- var median = sorted . length % 2 ? sorted [ mid ] : ( sorted [ mid - 1 ] + sorted [ mid ] ) / 2 ;
138-
139- // Standard deviation and coefficient of variation
140- var sumSqDiff = 0 ;
141- for ( var i = 0 ; i < effective . length ; i ++ ) {
142- sumSqDiff += ( effective [ i ] - avg ) * ( effective [ i ] - avg ) ;
143- }
144- var stddev = Math . sqrt ( sumSqDiff / effective . length ) ;
145- var variancePct = avg === 0 ? 0 : ( stddev / avg ) * 100 ;
146-
147- return {
148- min : Math . round ( min * 100 ) / 100 ,
149- max : Math . round ( max * 100 ) / 100 ,
150- avg : Math . round ( avg * 100 ) / 100 ,
151- median : Math . round ( median * 100 ) / 100 ,
152- stddev : Math . round ( stddev * 100 ) / 100 ,
153- variance_pct : Math . round ( variancePct * 100 ) / 100 ,
154- samples : effective . length ,
155- throughput_kbs : Math . round ( 1000 / avg * data . length / 1024 )
156- } ;
157- }
158123
124+ /**
125+ * Emits the final benchmark result as JSON to stdout. Includes the
126+ * detected Less version, compiler path, input file metadata, and the
127+ * aggregate render statistics.
128+ * @returns {void }
129+ */
159130function reportResults ( ) {
160- var result = {
161- version : version ,
162- lessPath : lessPath ,
163- file : path . basename ( file ) ,
164- fileSize : data . length ,
165- fileSizeKB : Math . round ( data . length / 1024 * 10 ) / 10 ,
166- totalRuns : totalRuns ,
167- warmupRuns : warmupRuns ,
168- completedRuns : completed ,
169- errors : errors . length > 0 ? errors : undefined ,
170- render : analyze ( renderTimes , true )
171- } ;
172- console . log ( JSON . stringify ( result ) ) ;
173131}
174132
175133runAll ( 0 ) ;
0 commit comments