1
+ use std:: collections:: HashMap ;
1
2
use std:: ffi:: { OsStr , OsString } ;
2
- use std:: io:: Write ;
3
+ use std:: fs:: File ;
4
+ use std:: io:: { BufReader , BufWriter , Write } ;
3
5
use std:: ops:: { Not , Range } ;
4
6
use std:: path:: PathBuf ;
5
7
use std:: time:: Duration ;
6
8
use std:: { env, net, process} ;
7
9
8
10
use anyhow:: { Context , Result , anyhow, bail} ;
9
11
use path_macro:: path;
12
+ use serde_derive:: { Deserialize , Serialize } ;
13
+ use tempfile:: TempDir ;
10
14
use walkdir:: WalkDir ;
11
15
use xshell:: { Shell , cmd} ;
12
16
@@ -179,8 +183,8 @@ impl Command {
179
183
Command :: Doc { flags } => Self :: doc ( flags) ,
180
184
Command :: Fmt { flags } => Self :: fmt ( flags) ,
181
185
Command :: Clippy { flags } => Self :: clippy ( flags) ,
182
- Command :: Bench { target, no_install, benches } =>
183
- Self :: bench ( target, no_install, benches) ,
186
+ Command :: Bench { target, no_install, save_baseline , load_baseline , benches } =>
187
+ Self :: bench ( target, no_install, save_baseline , load_baseline , benches) ,
184
188
Command :: Toolchain { flags } => Self :: toolchain ( flags) ,
185
189
Command :: RustcPull { commit } => Self :: rustc_pull ( commit. clone ( ) ) ,
186
190
Command :: RustcPush { github_user, branch } => Self :: rustc_push ( github_user, branch) ,
@@ -379,27 +383,44 @@ impl Command {
379
383
Ok ( ( ) )
380
384
}
381
385
382
- fn bench ( target : Option < String > , no_install : bool , benches : Vec < String > ) -> Result < ( ) > {
386
+ fn bench (
387
+ target : Option < String > ,
388
+ no_install : bool ,
389
+ save_baseline : Option < String > ,
390
+ load_baseline : Option < String > ,
391
+ benches : Vec < String > ,
392
+ ) -> Result < ( ) > {
393
+ if save_baseline. is_some ( ) && load_baseline. is_some ( ) {
394
+ bail ! ( "Only one of `--save-baseline` and `--load-baseline` can be set" ) ;
395
+ }
396
+
383
397
// The hyperfine to use
384
398
let hyperfine = env:: var ( "HYPERFINE" ) ;
385
399
let hyperfine = hyperfine. as_deref ( ) . unwrap_or ( "hyperfine -w 1 -m 5 --shell=none" ) ;
386
400
let hyperfine = shell_words:: split ( hyperfine) ?;
387
401
let Some ( ( program_name, args) ) = hyperfine. split_first ( ) else {
388
402
bail ! ( "expected HYPERFINE environment variable to be non-empty" ) ;
389
403
} ;
404
+
390
405
if !no_install {
391
406
// Make sure we have an up-to-date Miri installed and selected the right toolchain.
392
407
Self :: install ( vec ! [ ] ) ?;
393
408
}
409
+ let results_json_dir = if save_baseline. is_some ( ) || load_baseline. is_some ( ) {
410
+ Some ( TempDir :: new ( ) ?)
411
+ } else {
412
+ None
413
+ } ;
394
414
415
+ let miri_dir = miri_dir ( ) ?;
395
416
let sh = Shell :: new ( ) ?;
396
- sh. change_dir ( miri_dir ( ) ? ) ;
417
+ sh. change_dir ( & miri_dir) ;
397
418
let benches_dir = "bench-cargo-miri" ;
398
- let benches: Vec < OsString > = if benches. is_empty ( ) {
419
+ let benches: Vec < String > = if benches. is_empty ( ) {
399
420
sh. read_dir ( benches_dir) ?
400
421
. into_iter ( )
401
422
. filter ( |path| path. is_dir ( ) )
402
- . map ( Into :: into )
423
+ . map ( |path| path . into_os_string ( ) . into_string ( ) . unwrap ( ) )
403
424
. collect ( )
404
425
} else {
405
426
benches. into_iter ( ) . map ( Into :: into) . collect ( )
@@ -414,16 +435,75 @@ impl Command {
414
435
let target_flag = & target_flag;
415
436
let toolchain = active_toolchain ( ) ?;
416
437
// Run the requested benchmarks
417
- for bench in benches {
438
+ for bench in & benches {
418
439
let current_bench = path ! ( benches_dir / bench / "Cargo.toml" ) ;
440
+ let mut export_json = None ;
441
+ if let Some ( baseline_temp_dir) = & results_json_dir {
442
+ export_json = Some ( format ! (
443
+ "--export-json={}" ,
444
+ path!( baseline_temp_dir / format!( "{bench}.bench.json" ) ) . display( )
445
+ ) ) ;
446
+ }
419
447
// We don't attempt to escape `current_bench`, but we wrap it in quotes.
420
448
// That seems to make Windows CI happy.
421
449
cmd ! (
422
450
sh,
423
- "{program_name} {args...} 'cargo +'{toolchain}' miri run '{target_flag}' --manifest-path \" '{current_bench}'\" '"
451
+ "{program_name} {args...} {export_json...} 'cargo +'{toolchain}' miri run '{target_flag}' --manifest-path \" '{current_bench}'\" '"
424
452
)
425
453
. run ( ) ?;
426
454
}
455
+
456
+ // Gather/load results for baseline saving.
457
+
458
+ #[ derive( Serialize , Deserialize ) ]
459
+ struct BenchResult {
460
+ mean : f64 ,
461
+ stddev : f64 ,
462
+ }
463
+
464
+ let gather_results = || -> Result < HashMap < & str , BenchResult > > {
465
+ let baseline_temp_dir = results_json_dir. unwrap ( ) ;
466
+ let mut results = HashMap :: new ( ) ;
467
+ for bench in & benches {
468
+ let result = File :: open ( path ! ( baseline_temp_dir / format!( "{bench}.bench.json" ) ) ) ?;
469
+ let mut result: serde_json:: Value =
470
+ serde_json:: from_reader ( BufReader :: new ( result) ) ?;
471
+ let result: BenchResult = serde_json:: from_value ( result[ "results" ] [ 0 ] . take ( ) ) ?;
472
+ results. insert ( bench as & str , result) ;
473
+ }
474
+ Ok ( results)
475
+ } ;
476
+
477
+ if let Some ( baseline_file) = save_baseline {
478
+ let results = gather_results ( ) ?;
479
+ let baseline = File :: create ( baseline_file) ?;
480
+ serde_json:: to_writer_pretty ( BufWriter :: new ( baseline) , & results) ?;
481
+ } else if let Some ( baseline_file) = load_baseline {
482
+ let new_results = gather_results ( ) ?;
483
+ let baseline_results: HashMap < String , BenchResult > = {
484
+ let f = File :: open ( baseline_file) ?;
485
+ serde_json:: from_reader ( BufReader :: new ( f) ) ?
486
+ } ;
487
+ println ! (
488
+ "Comparison with baseline (relative speed, lower is better for the new results):"
489
+ ) ;
490
+ for ( bench, new_result) in new_results. iter ( ) {
491
+ let Some ( baseline_result) = baseline_results. get ( * bench) else { continue } ;
492
+
493
+ // Compare results (inspired by hyperfine)
494
+ let ratio = new_result. mean / baseline_result. mean ;
495
+ // https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulae
496
+ // Covariance asssumed to be 0, i.e. variables are assumed to be independent
497
+ let ratio_stddev = ratio
498
+ * f64:: sqrt (
499
+ ( new_result. stddev / new_result. mean ) . powi ( 2 )
500
+ + ( baseline_result. stddev / baseline_result. mean ) . powi ( 2 ) ,
501
+ ) ;
502
+
503
+ println ! ( " {bench}: {ratio:.2} ± {ratio_stddev:.2}" ) ;
504
+ }
505
+ }
506
+
427
507
Ok ( ( ) )
428
508
}
429
509
0 commit comments