11use berry:: parse:: parse_lockfile;
2- use berry_test:: load_fixture;
2+ use berry_test:: { load_fixture, load_fixture_from_path } ;
33use clap:: Parser ;
44use memory_stats:: memory_stats;
55use std:: collections:: HashMap ;
66use std:: fs;
7- use std:: path:: Path ;
7+ use std:: hint:: black_box;
8+ use std:: path:: { Path , PathBuf } ;
89use std:: time:: Instant ;
910
1011#[ derive( Parser ) ]
@@ -15,6 +16,10 @@ struct Args {
1516 #[ arg( short, long) ]
1617 fixture : Option < String > ,
1718
19+ /// Path to a lockfile to benchmark
20+ #[ arg( long = "fixture-path" , value_name = "PATH" , conflicts_with_all = [ "fixture" , "all" ] ) ]
21+ fixture_path : Option < PathBuf > ,
22+
1823 /// Benchmark all fixtures
1924 #[ arg( short, long) ]
2025 all : bool ,
@@ -52,6 +57,55 @@ struct Args {
5257 fail_on_regression : bool ,
5358}
5459
60+ #[ derive( Clone ) ]
61+ enum FixtureSource {
62+ FixtureName ( String ) ,
63+ ArbitraryPath ( PathBuf ) ,
64+ }
65+
66+ #[ derive( Clone ) ]
67+ struct FixtureTarget {
68+ label : String ,
69+ source : FixtureSource ,
70+ }
71+
72+ impl FixtureTarget {
73+ fn from_fixture_name ( name : impl Into < String > ) -> Self {
74+ let name = name. into ( ) ;
75+ Self {
76+ label : name. clone ( ) ,
77+ source : FixtureSource :: FixtureName ( name) ,
78+ }
79+ }
80+
81+ fn from_path ( path : impl Into < PathBuf > ) -> Self {
82+ let path = path. into ( ) ;
83+ let label = path
84+ . file_name ( )
85+ . and_then ( |name| name. to_str ( ) )
86+ . map ( std:: string:: ToString :: to_string)
87+ . unwrap_or_else ( || path. display ( ) . to_string ( ) ) ;
88+ Self {
89+ label,
90+ source : FixtureSource :: ArbitraryPath ( path) ,
91+ }
92+ }
93+
94+ fn source_path ( & self ) -> Option < & Path > {
95+ match & self . source {
96+ FixtureSource :: ArbitraryPath ( path) => Some ( path. as_path ( ) ) ,
97+ FixtureSource :: FixtureName ( _) => None ,
98+ }
99+ }
100+
101+ fn load_contents ( & self ) -> String {
102+ match & self . source {
103+ FixtureSource :: FixtureName ( name) => load_fixture ( name) ,
104+ FixtureSource :: ArbitraryPath ( path) => load_fixture_from_path ( path) ,
105+ }
106+ }
107+ }
108+
55109#[ derive( serde:: Serialize , serde:: Deserialize , Clone ) ]
56110struct BenchmarkResult {
57111 fixture : String ,
@@ -60,6 +114,12 @@ struct BenchmarkResult {
60114 min_time_ms : f64 ,
61115 max_time_ms : f64 ,
62116 std_dev_ms : f64 ,
117+ #[ serde( default ) ]
118+ std_error_ms : f64 ,
119+ #[ serde( default ) ]
120+ ci_95_lower_ms : f64 ,
121+ #[ serde( default ) ]
122+ ci_95_upper_ms : f64 ,
63123 runs : usize ,
64124 heap_usage_bytes : Option < usize > ,
65125 virtual_usage_bytes : Option < usize > ,
@@ -68,34 +128,75 @@ struct BenchmarkResult {
68128 mb_per_s : f64 ,
69129}
70130
131+ #[ derive( Default ) ]
132+ struct StatsSummary {
133+ mean : f64 ,
134+ min : f64 ,
135+ max : f64 ,
136+ std_dev : f64 ,
137+ std_error : f64 ,
138+ ci_low : f64 ,
139+ ci_high : f64 ,
140+ }
141+
71142#[ allow( clippy:: cast_precision_loss) ]
72- fn calculate_stats ( times : & [ f64 ] ) -> ( f64 , f64 , f64 , f64 ) {
143+ fn calculate_stats ( times : & [ f64 ] ) -> StatsSummary {
144+ if times. is_empty ( ) {
145+ return StatsSummary :: default ( ) ;
146+ }
147+
73148 let mean = times. iter ( ) . sum :: < f64 > ( ) / times. len ( ) as f64 ;
74- let variance = times. iter ( ) . map ( |& x| ( x - mean) . powi ( 2 ) ) . sum :: < f64 > ( ) / times. len ( ) as f64 ;
75- let std_dev = variance. sqrt ( ) ;
76149 let min = times. iter ( ) . fold ( f64:: INFINITY , |a, & b| a. min ( b) ) ;
77150 let max = times. iter ( ) . fold ( f64:: NEG_INFINITY , |a, & b| a. max ( b) ) ;
78-
79- ( mean, min, max, std_dev)
151+ let variance = if times. len ( ) > 1 {
152+ times. iter ( ) . map ( |& x| ( x - mean) . powi ( 2 ) ) . sum :: < f64 > ( ) / ( times. len ( ) - 1 ) as f64
153+ } else {
154+ 0.0
155+ } ;
156+ let std_dev = variance. sqrt ( ) ;
157+ let std_error = if times. len ( ) > 1 {
158+ std_dev / ( times. len ( ) as f64 ) . sqrt ( )
159+ } else {
160+ 0.0
161+ } ;
162+ let ci_margin = 1.96 * std_error;
163+ let ci_low = ( mean - ci_margin) . max ( 0.0 ) ;
164+ let ci_high = mean + ci_margin;
165+
166+ StatsSummary {
167+ mean,
168+ min,
169+ max,
170+ std_dev,
171+ std_error,
172+ ci_low,
173+ ci_high,
174+ }
80175}
81176
82177fn benchmark_fixture (
83- fixture_name : & str ,
178+ target : & FixtureTarget ,
84179 warmup : usize ,
85180 runs : usize ,
86181 verbose : bool ,
87182) -> BenchmarkResult {
88- let fixture = load_fixture ( fixture_name ) ;
183+ let fixture = target . load_contents ( ) ;
89184 let file_size = fixture. len ( ) ;
185+ let fixture_str = fixture. as_str ( ) ;
90186
91- println ! ( "Benchmarking {fixture_name} ({file_size} bytes)..." ) ;
187+ println ! ( "Benchmarking {} ({file_size} bytes)..." , target. label) ;
188+ if verbose {
189+ if let Some ( path) = target. source_path ( ) {
190+ println ! ( " Source path: {}" , path. display( ) ) ;
191+ }
192+ }
92193
93194 // Warmup runs
94195 for i in 0 ..warmup {
95196 let start = Instant :: now ( ) ;
96- let result = parse_lockfile ( & fixture ) ;
197+ let result = parse_lockfile ( black_box ( fixture_str ) ) ;
97198 let duration = start. elapsed ( ) ;
98- assert ! ( result. is_ok( ) , "Should parse {fixture_name } successfully" ) ;
199+ assert ! ( result. is_ok( ) , "Should parse {} successfully" , target . label ) ;
99200
100201 if verbose {
101202 println ! (
@@ -109,15 +210,15 @@ fn benchmark_fixture(
109210
110211 // Measure heap usage with a single run
111212 let before = memory_stats ( ) . unwrap ( ) ;
112- let result = parse_lockfile ( & fixture ) ;
213+ let result = parse_lockfile ( black_box ( fixture_str ) ) ;
113214 let after = memory_stats ( ) . unwrap ( ) ;
114215
115216 let heap_usage = isize:: try_from ( after. physical_mem ) . expect ( "physical mem too large" )
116217 - isize:: try_from ( before. physical_mem ) . expect ( "physical mem too large" ) ;
117218 let virtual_usage = isize:: try_from ( after. virtual_mem ) . expect ( "virtual mem too large" )
118219 - isize:: try_from ( before. virtual_mem ) . expect ( "virtual mem too large" ) ;
119220
120- assert ! ( result. is_ok( ) , "Should parse {fixture_name } successfully" ) ;
221+ assert ! ( result. is_ok( ) , "Should parse {} successfully" , target . label ) ;
121222
122223 if verbose {
123224 println ! ( " Heap usage: {heap_usage} bytes (physical), {virtual_usage} bytes (virtual)" ) ;
@@ -128,7 +229,7 @@ fn benchmark_fixture(
128229
129230 for i in 0 ..runs {
130231 let start = Instant :: now ( ) ;
131- let result = parse_lockfile ( & fixture ) ;
232+ let result = parse_lockfile ( black_box ( fixture_str ) ) ;
132233 let duration = start. elapsed ( ) ;
133234 let time_ms = duration. as_secs_f64 ( ) * 1000.0 ;
134235 times. push ( time_ms) ;
@@ -137,28 +238,31 @@ fn benchmark_fixture(
137238 println ! ( " Run {}: {:.3}ms" , i + 1 , time_ms) ;
138239 }
139240
140- assert ! ( result. is_ok( ) , "Should parse {fixture_name } successfully" ) ;
241+ assert ! ( result. is_ok( ) , "Should parse {} successfully" , target . label ) ;
141242 }
142243
143- let ( mean , min , max , std_dev ) = calculate_stats ( & times) ;
244+ let stats = calculate_stats ( & times) ;
144245
145246 // Derived metrics
146247 let kib = file_size as f64 / 1024.0 ;
147- let time_per_kib_ms = if kib > 0.0 { mean / kib } else { 0.0 } ;
248+ let time_per_kib_ms = if kib > 0.0 { stats . mean / kib } else { 0.0 } ;
148249 let mb = file_size as f64 / 1_000_000.0 ;
149- let mb_per_s = if mean > 0.0 {
150- mb / ( mean / 1000.0 )
250+ let mb_per_s = if stats . mean > 0.0 {
251+ mb / ( stats . mean / 1000.0 )
151252 } else {
152253 f64:: INFINITY
153254 } ;
154255
155256 BenchmarkResult {
156- fixture : fixture_name . to_string ( ) ,
257+ fixture : target . label . clone ( ) ,
157258 file_size,
158- mean_time_ms : mean,
159- min_time_ms : min,
160- max_time_ms : max,
161- std_dev_ms : std_dev,
259+ mean_time_ms : stats. mean ,
260+ min_time_ms : stats. min ,
261+ max_time_ms : stats. max ,
262+ std_dev_ms : stats. std_dev ,
263+ std_error_ms : stats. std_error ,
264+ ci_95_lower_ms : stats. ci_low ,
265+ ci_95_upper_ms : stats. ci_high ,
162266 runs,
163267 heap_usage_bytes : Some ( heap_usage. unsigned_abs ( ) ) ,
164268 virtual_usage_bytes : Some ( virtual_usage. unsigned_abs ( ) ) ,
@@ -259,17 +363,19 @@ fn print_results(results: &[BenchmarkResult], format: &str) {
259363 } else {
260364 println ! ( "\n Benchmark Results:" ) ;
261365 println ! (
262- "{:<28} {:>12} {:>12 } {:>12} {:>12} {:>12} {:>12}" ,
263- "Fixture" , "Bytes" , "Mean (ms)" , "Min (ms)" , "Max (ms)" , "ms/KiB" , "MB/s"
366+ "{:<28} {:>12} {:>20 } {:>12} {:>12} {:>12} {:>12}" ,
367+ "Fixture" , "Bytes" , "Mean +/- CI95 (ms)" , "Min (ms)" , "Max (ms)" , "ms/KiB" , "MB/s"
264368 ) ;
265- println ! ( "{:-<104 }" , "" ) ;
369+ println ! ( "{:-<120 }" , "" ) ;
266370
267371 for result in results {
372+ let ci_margin = ( result. ci_95_upper_ms - result. ci_95_lower_ms ) . abs ( ) / 2.0 ;
373+ let mean_column = format ! ( "{:.3} +/- {:.3}" , result. mean_time_ms, ci_margin) ;
268374 println ! (
269- "{:<28} {:>12} {:>12.3 } {:>12.3} {:>12.3} {:>12.3} {:>12.2}" ,
375+ "{:<28} {:>12} {:>20 } {:>12.3} {:>12.3} {:>12.3} {:>12.2}" ,
270376 result. fixture,
271377 result. file_size,
272- result . mean_time_ms ,
378+ mean_column ,
273379 result. min_time_ms,
274380 result. max_time_ms,
275381 result. time_per_kib_ms,
@@ -282,23 +388,28 @@ fn print_results(results: &[BenchmarkResult], format: &str) {
282388fn main ( ) {
283389 let args = Args :: parse ( ) ;
284390
285- let fixtures = if let Some ( fixture) = args. fixture {
286- vec ! [ fixture]
391+ let fixtures: Vec < FixtureTarget > = if let Some ( path) = args. fixture_path {
392+ vec ! [ FixtureTarget :: from_path( path) ]
393+ } else if let Some ( fixture) = args. fixture {
394+ vec ! [ FixtureTarget :: from_fixture_name( fixture) ]
287395 } else if args. all {
288396 discover_all_fixture_names ( )
397+ . into_iter ( )
398+ . map ( FixtureTarget :: from_fixture_name)
399+ . collect ( )
289400 } else {
290401 // Default to a few key fixtures
291402 vec ! [
292- "minimal-berry.lock" . to_string ( ) ,
293- "workspaces.yarn.lock" . to_string ( ) ,
294- "auxiliary-packages.yarn.lock" . to_string ( ) ,
403+ FixtureTarget :: from_fixture_name ( "minimal-berry.lock" ) ,
404+ FixtureTarget :: from_fixture_name ( "workspaces.yarn.lock" ) ,
405+ FixtureTarget :: from_fixture_name ( "auxiliary-packages.yarn.lock" ) ,
295406 ]
296407 } ;
297408
298409 let mut results = Vec :: new ( ) ;
299410
300- for fixture in fixtures {
301- let result = benchmark_fixture ( & fixture, args. warmup , args. runs , args. verbose ) ;
411+ for fixture in & fixtures {
412+ let result = benchmark_fixture ( fixture, args. warmup , args. runs , args. verbose ) ;
302413 results. push ( result) ;
303414 }
304415
0 commit comments