Skip to main content

tango_bench/
cli.rs

1//! Contains functionality of a `cargo bench` harness
2use crate::{available_aux_metrics, commpage::Role, worker, Benchmark, Error};
3use anyhow::{bail, Context};
4use clap::{ArgAction, Parser};
5use core::fmt;
6use glob_match::glob_match;
7use std::{
8    env::{self, temp_dir},
9    fmt::Display,
10    fs,
11    io::Write,
12    num::NonZeroUsize,
13    ops::Deref,
14    path::{Path, PathBuf},
15    process::{Command, ExitCode, Stdio},
16    time::Duration,
17};
18
19pub type Result<T> = anyhow::Result<T>;
20
21#[derive(Parser, Debug)]
22enum Subcommand {
23    /// List benchmarks
24    List {
25        #[command(flatten)]
26        bench_flags: CargoBenchFlags,
27    },
28    /// List auxiliary metrics
29    AuxMetrics {
30        #[command(flatten)]
31        bench_flags: CargoBenchFlags,
32    },
33    /// Run paired benchmarking to compare two executables
34    Compare(PairedOpts),
35    /// Run a single benchmark in a solo (isolated) mode
36    Solo(SoloOpts),
37    /// Internal worker mode (used by the runner to spawn child processes)
38    #[command(name = "__worker", hide = true)]
39    Worker(WorkerOpts),
40}
41
42#[derive(Parser, Debug)]
43struct WorkerOpts {
44    /// Shared memory name for the commpage
45    #[arg(long = "shmem")]
46    shmem: String,
47
48    /// Role of this worker (candidate or baseline)
49    #[arg(long = "role")]
50    role: String,
51}
52
53#[derive(Parser, Debug)]
54struct PairedOpts {
55    #[command(flatten)]
56    bench_flags: CargoBenchFlags,
57
58    /// Path to the executable to test against. Tango will test against itself if no executable given
59    path: Option<PathBuf>,
60
61    /// write CSV dumps of all the measurements in a given location
62    #[arg(short = 'd', long = "dump")]
63    path_to_dump: Option<PathBuf>,
64
65    /// generate gnuplot graphs for each test (requires --dump [path] to be specified)
66    #[arg(long = "gnuplot")]
67    gnuplot: bool,
68
69    /// seed for the random number generator or omit to use a random seed
70    #[arg(long = "seed")]
71    seed: Option<u64>,
72
73    /// Number of samples to take for each test
74    #[arg(short = 's', long = "samples")]
75    samples: Option<NonZeroUsize>,
76
77    /// Duration of each sample in seconds
78    #[arg(short = 't', long = "time")]
79    time: Option<f64>,
80
81    /// Consider benchmark differences below this percentage as noise (default: 1%)
82    #[arg(long = "noise-threshold", default_value_t = 1.0)]
83    noise_threshold: f64,
84
85    /// Terminate early on first statistically significant performance regression
86    #[arg(long = "fail-fast")]
87    fail_fast: bool,
88
89    /// Filter tests by name (eg. '*/{sorted,unsorted}/[0-9]*')
90    #[arg(short = 'f', long = "filter")]
91    filter: Option<String>,
92
93    /// Report only statistically significant results
94    #[arg(short = 'g', long = "significant-only", default_value_t = false)]
95    significant_only: bool,
96
97    /// Enable outlier detection
98    #[arg(short = 'o', long = "filter-outliers")]
99    filter_outliers: bool,
100
101    /// Quiet mode
102    #[arg(short = 'q')]
103    quiet: bool,
104
105    #[arg(short = 'v', long = "verbose", default_value_t = false)]
106    verbose: bool,
107
108    /// Disables reporting a progress of a running benchmark
109    #[arg(long = "progress", default_value_t = false, action = ArgAction::SetTrue)]
110    progress_report: bool,
111}
112
113#[derive(Parser, Debug)]
114struct SoloOpts {
115    #[command(flatten)]
116    bench_flags: CargoBenchFlags,
117
118    /// seed for the random number generator or omit to use a random seed
119    #[arg(long = "seed")]
120    seed: Option<u64>,
121
122    /// Number of samples to take for each test
123    #[arg(short = 's', long = "samples")]
124    samples: Option<NonZeroUsize>,
125
126    /// Duration of each sample in seconds
127    #[arg(short = 't', long = "time")]
128    time: Option<f64>,
129
130    /// Filter tests by name (eg. '*/{sorted,unsorted}/[0-9]*')
131    #[arg(short = 'f', long = "filter")]
132    filter: Option<String>,
133}
134
135#[derive(Parser, Debug)]
136#[command(author, version, about, long_about = None)]
137struct Opts {
138    #[command(subcommand)]
139    subcommand: Option<Subcommand>,
140
141    #[command(flatten)]
142    bench_flags: CargoBenchFlags,
143
144    #[arg(long = "color", default_value = "detect")]
145    coloring_mode: String,
146}
147
148/// Definition of the flags required to comply with `cargo bench` calling conventions.
149#[derive(Parser, Debug, Clone)]
150struct CargoBenchFlags {
151    #[arg(long = "bench", default_value_t = true, hide = true)]
152    bench: bool,
153}
154
155pub fn run(benchmarks: Vec<Benchmark>) -> Result<ExitCode> {
156    let opts = Opts::parse();
157
158    let subcommand = opts.subcommand.unwrap_or(Subcommand::List {
159        bench_flags: opts.bench_flags,
160    });
161
162    // Handle worker mode early, before setting up coloring etc.
163    if let Subcommand::Worker(ref worker_opts) = subcommand {
164        let role: Role = serde_json::from_value(serde_json::Value::String(
165            worker_opts.role.clone(),
166        ))
167        .map_err(|_| {
168            anyhow::anyhow!(
169                "Invalid role: '{}' (expected 'candidate' or 'baseline')",
170                worker_opts.role
171            )
172        })?;
173        worker::run_worker(&worker_opts.shmem, role, benchmarks)
174    } else {
175        match opts.coloring_mode.as_str() {
176            "always" => colored::control::set_override(true),
177            "never" => colored::control::set_override(false),
178            "detect" | "auto" => colored::control::unset_override(),
179            _ => eprintln!("[WARN] Invalid coloring mode: {}", opts.coloring_mode),
180        }
181
182        match subcommand {
183            Subcommand::Worker(_) => unreachable!(),
184            Subcommand::List { bench_flags: _ } => {
185                for bench in &benchmarks {
186                    println!("{}", bench.name());
187                }
188                Ok(ExitCode::SUCCESS)
189            }
190            Subcommand::AuxMetrics { bench_flags: _ } => {
191                for m in available_aux_metrics() {
192                    println!("{}", m.id);
193                }
194                Ok(ExitCode::SUCCESS)
195            }
196            Subcommand::Compare(opts) => paired_test::run_test(opts),
197            Subcommand::Solo(opts) => solo_test::run_test(opts, benchmarks),
198        }
199    }
200}
201
202fn create_loop_mode(samples: Option<NonZeroUsize>, time: Option<f64>) -> Result<LoopMode> {
203    let loop_mode = match (samples, time) {
204        (Some(samples), None) => LoopMode::Samples(samples.into()),
205        (None, Some(time)) => LoopMode::Time(Duration::from_millis((time * 1000.) as u64)),
206        (None, None) => LoopMode::Time(Duration::from_millis(100)),
207        (Some(_), Some(_)) => bail!("-t and -s are mutually exclusive"),
208    };
209    Ok(loop_mode)
210}
211
212#[derive(Clone, Copy)]
213enum LoopMode {
214    Samples(usize),
215    Time(Duration),
216}
217
218impl LoopMode {
219    fn should_continue(&self, iter_no: usize, loop_time: Duration) -> bool {
220        match self {
221            LoopMode::Samples(samples) => iter_no < *samples,
222            LoopMode::Time(duration) => loop_time < *duration,
223        }
224    }
225}
226
227mod solo_test {
228    use super::*;
229    use crate::Summary;
230
231    pub(super) fn run_test(opts: SoloOpts, mut benchmarks: Vec<Benchmark>) -> Result<ExitCode> {
232        let SoloOpts {
233            bench_flags: _,
234            filter,
235            samples,
236            time,
237            seed,
238        } = opts;
239
240        let filter = filter.as_deref().unwrap_or("");
241        let loop_mode = create_loop_mode(samples, time)?;
242        let seed = seed.unwrap_or_else(rand::random);
243
244        for bench in &mut benchmarks {
245            let name = bench.name().to_string();
246            if !filter.is_empty() && !glob_match(filter, &name) {
247                continue;
248            }
249
250            let mut sampler = bench.prepare_state(seed);
251            let iters = sampler.estimate_iterations(10);
252            let iterations = iters.max(1);
253
254            let mut sample_values = vec![];
255            let mut loop_time = Duration::from_secs(0);
256            let mut i = 0;
257
258            if let LoopMode::Samples(n) = loop_mode {
259                sample_values.reserve(n);
260            }
261
262            while loop_mode.should_continue(i, loop_time) {
263                let elapsed_ns = sampler.measure(iterations);
264                let per_iter = elapsed_ns as f64 / iterations as f64;
265                sample_values.push(per_iter);
266                loop_time += Duration::from_nanos(elapsed_ns);
267                i += 1;
268            }
269
270            let result = Summary::from(&sample_values).ok_or(Error::NoMeasurements)?;
271            reporting::default_reporter_solo(&name, &result);
272        }
273
274        Ok(ExitCode::SUCCESS)
275    }
276}
277
278mod paired_test {
279    use super::*;
280    use crate::{
281        aux::CPU_SYSTEM_TIME_ID,
282        calculate_run_result,
283        child::ChildHandle,
284        cli::reporting::{report_system_time_bias, BenchmarkProgress},
285        commpage::{Commpage, Role},
286        protocol::RunBenchmarkResult,
287    };
288    use fs::File;
289    use reporting::Reporter;
290    use std::{
291        collections::HashSet,
292        io::{self, BufWriter},
293        thread::sleep,
294        time::Instant,
295    };
296
297    const TIME_SLICE: Duration = Duration::from_millis(10);
298
299    /// How frequent we report progress to the user
300    const PROGRESS_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
301
302    /// If benchmark duration is less than this duration do not report the progress to the user.
303    /// Two reasons for this:
304    ///  1. when benchmark is fast additional report leads to flickering and worsen the experience
305    ///  2. additional reporting may lead to unneeded context switches
306    const PROGRESS_REPORTING_THRESHOLD: Duration = Duration::from_secs(1);
307
308    pub(super) fn run_test(opts: PairedOpts) -> Result<ExitCode> {
309        let PairedOpts {
310            bench_flags: _,
311            path,
312            verbose,
313            filter,
314            samples,
315            time,
316            filter_outliers,
317            path_to_dump,
318            gnuplot,
319            noise_threshold,
320            fail_fast,
321            significant_only,
322            seed,
323            quiet,
324            progress_report,
325        } = opts;
326
327        let candidate_exe = env::current_exe().context("Unable to determine current executable")?;
328        // If no baseline executable was given, comparing against itself
329        let mut path = path.unwrap_or(candidate_exe.clone());
330        if path.is_relative() {
331            if let Ok(pwd) = env::current_dir() {
332                path = pwd.join(path)
333            }
334        };
335
336        if !fs::exists(&path)? {
337            let description = format!("Benchmark not found: {}", path.display());
338            return Err(io::Error::new(io::ErrorKind::NotFound, description).into());
339        }
340
341        let filter = filter.as_deref().unwrap_or("");
342        let seed = seed.unwrap_or_else(rand::random);
343        let loop_mode = create_loop_mode(samples, time)?;
344
345        let mut commpage = Commpage::create().context("Failed to create commpage")?;
346
347        let mut child_c = ChildHandle::spawn(&candidate_exe, &commpage, Role::Candidate)
348            .context("Failed to spawn candidate")?;
349        let mut child_b = ChildHandle::spawn(&path, &commpage, Role::Baseline)
350            .context("Failed to spawn baseline")?;
351
352        let c_list = child_c
353            .list_benchmarks()
354            .context("Failed to list benchmarks (candidate)")?;
355        let b_list = child_b
356            .list_benchmarks()
357            .context("Failed to list benchmarks (baseline)")?;
358        let c_benchmarks = c_list.benchmarks;
359        let b_benchmarks = b_list.benchmarks;
360
361        if let Some(path) = &path_to_dump {
362            if !path.exists() {
363                fs::create_dir_all(path)?;
364            }
365        }
366        if gnuplot && path_to_dump.is_none() && !quiet {
367            eprintln!("warn: --gnuplot requires -d to be specified. No plots will be generated")
368        }
369
370        let mut sample_dumps = vec![];
371
372        let reporter: &dyn Reporter = if verbose {
373            &reporting::VerboseReporter
374        } else {
375            &reporting::DefaultReporter
376        };
377
378        // Determine num_samples
379        let num_samples = match loop_mode {
380            LoopMode::Samples(n) => n,
381            LoopMode::Time(_) => 0,
382        };
383
384        let mut exit_code = ExitCode::SUCCESS;
385
386        // Calculate common aux-metric names if verbose mode activated
387        let aux_metrics = if verbose {
388            let b_metrics = HashSet::<String>::from_iter(b_list.aux_metrics);
389            let c_metrics = HashSet::from_iter(c_list.aux_metrics);
390            b_metrics
391                .intersection(&c_metrics)
392                .cloned()
393                .collect::<Vec<_>>()
394        } else {
395            vec![]
396        };
397
398        let selected_benchmarks = c_benchmarks
399            .iter()
400            .enumerate()
401            // Filtering benchmarks by -f
402            .filter(|(_, f)| filter.is_empty() || glob_match(filter, f))
403            // finding position of the benchmark in a baseline
404            .filter_map(|(c_idx, f)| {
405                b_benchmarks
406                    .iter()
407                    .position(|b_func| b_func == f)
408                    .map(|b_idx| (c_idx, b_idx, f))
409            });
410        for (c_idx, b_idx, func_name) in selected_benchmarks {
411            // Estimate iterations
412            let c_iters = child_c
413                .estimate_iterations(c_idx, seed, TIME_SLICE)
414                .context("Failed to estimate iterations (candidate)")?;
415            let b_iters = child_b
416                .estimate_iterations(b_idx, seed, TIME_SLICE)
417                .context("Failed to estimate iterations (baseline)")?;
418            let iterations = c_iters.max(1).min(b_iters.max(1));
419
420            commpage.reset();
421
422            // Start measurement on both children (non-blocking)
423            child_c.start_benchmark(c_idx, seed, iterations, num_samples, aux_metrics.clone())?;
424            child_b.start_benchmark(b_idx, seed, iterations, num_samples, aux_metrics.clone())?;
425
426            let expected_duration = match loop_mode {
427                LoopMode::Samples(samples) => TIME_SLICE * samples as u32,
428                LoopMode::Time(duration) => duration,
429            };
430
431            // Wait for children to finish, handling time-budget and progress reporting
432            let time_start = Instant::now();
433            while !commpage.is_some_done() {
434                let duration = time_start.elapsed();
435                // Monitoring execution time and stopping benchmark on a timer
436                if let LoopMode::Time(required_duration) = loop_mode {
437                    if duration >= required_duration {
438                        break;
439                    }
440                }
441
442                // Reporting progress to CLI
443                if progress_report {
444                    let progress = match loop_mode {
445                        LoopMode::Time(required_duration) => BenchmarkProgress::SamplingTime {
446                            duration,
447                            required_duration,
448                        },
449                        LoopMode::Samples(samples_total) => BenchmarkProgress::SamplingNo {
450                            samples_total,
451                            sample_no: commpage
452                                .load_cursor_value(Role::Candidate)
453                                .min(commpage.load_cursor_value(Role::Baseline)),
454                        },
455                    };
456                    if expected_duration > PROGRESS_REPORTING_THRESHOLD
457                        || time_start.elapsed() > PROGRESS_REPORTING_THRESHOLD
458                    {
459                        reporting::report_progress(func_name, progress);
460                    }
461                }
462
463                sleep(PROGRESS_UPDATE_INTERVAL);
464            }
465            // Signaling children to exit
466            commpage.set_stop();
467            let wall_time = time_start.elapsed();
468
469            // Both children are done — read their RPC responses
470            let RunBenchmarkResult {
471                samples: c_samples,
472                aux_metrics: c_aux_metrics,
473            } = child_c
474                .finish_benchmark()
475                .context("Candidate benchmark failed")?;
476            let RunBenchmarkResult {
477                samples: b_samples,
478                aux_metrics: b_aux_metrics,
479            } = child_b
480                .finish_benchmark()
481                .context("Baseline benchmark failed")?;
482
483            // println!("b: {:?}", b_samples);
484            // println!("c: {:?}", c_samples);
485
486            let samples = b_samples.into_iter().zip(c_samples).collect::<Vec<_>>();
487
488            let run_result =
489                calculate_run_result(&samples, iterations, filter_outliers, noise_threshold)
490                    .ok_or(Error::NoMeasurements)
491                    .with_context(|| {
492                        format!("Unable to calculate results for a benchmark: {}", func_name)
493                    })?;
494
495            if let Some(path) = &path_to_dump {
496                let file_name = format!("{}.csv", func_name.replace('/', "-"));
497                let file_path = path.join(file_name);
498                write_csv(&file_path, samples, iterations)
499                    .context("Unable to write raw measurements")?;
500                sample_dumps.push(file_path);
501            }
502
503            if run_result.diff_estimate.significant || !significant_only {
504                reporter.benchmark_finished(
505                    func_name,
506                    &run_result,
507                    (&aux_metrics, &b_aux_metrics, &c_aux_metrics),
508                );
509            }
510
511            // Reporting warning if system time is >5%
512            if !quiet {
513                check_system_time_bias(
514                    &aux_metrics,
515                    func_name,
516                    wall_time,
517                    c_aux_metrics,
518                    b_aux_metrics,
519                );
520            }
521
522            if run_result.diff_estimate.significant && run_result.diff_estimate.pct > 0. {
523                exit_code = ExitCode::FAILURE;
524                if fail_fast {
525                    break;
526                }
527            }
528        }
529
530        if gnuplot && !sample_dumps.is_empty() {
531            generate_plots(sample_dumps.as_slice())?;
532        }
533
534        let _ = child_c.shutdown();
535        let _ = child_b.shutdown();
536
537        Ok(exit_code)
538    }
539
540    /// Checking if test spent too much time in a system/kernel mode.
541    fn check_system_time_bias(
542        aux_metrics: &[String],
543        func_name: &str,
544        wall_time: Duration,
545        c_aux_metrics: Vec<u64>,
546        b_aux_metrics: Vec<u64>,
547    ) {
548        let sys_time_idx = aux_metrics
549            .iter()
550            .position(|name| name == CPU_SYSTEM_TIME_ID);
551        if let Some(system_time_idx) = sys_time_idx {
552            let c_sys_time = Duration::from_nanos(c_aux_metrics[system_time_idx]);
553            let b_sys_time = Duration::from_nanos(b_aux_metrics[system_time_idx]);
554
555            if c_sys_time * 20 > wall_time || b_sys_time * 20 > wall_time {
556                report_system_time_bias(func_name, wall_time, b_sys_time, c_sys_time);
557            }
558        }
559    }
560
561    fn write_csv<A: Display, B: Display>(
562        path: impl AsRef<Path>,
563        values: impl IntoIterator<Item = (A, B)>,
564        iterations: usize,
565    ) -> io::Result<()> {
566        let mut file = BufWriter::new(File::create(path)?);
567        for (a, b) in values {
568            writeln!(&mut file, "{},{},{iterations}", a, b)?;
569        }
570        Ok(())
571    }
572
573    fn generate_plots(sample_dumps: &[PathBuf]) -> Result<()> {
574        let gnuplot_file = AutoDelete(temp_dir().join("tango-plot.gnuplot"));
575        fs::write(&*gnuplot_file, include_bytes!("plot.gnuplot"))?;
576        let gnuplot_file_str = gnuplot_file.to_str().unwrap();
577
578        for input in sample_dumps {
579            let csv_input = input.to_str().unwrap();
580            let svg_path = input.with_extension("svg");
581            let cmd = Command::new("gnuplot")
582                .args([
583                    "-c",
584                    gnuplot_file_str,
585                    csv_input,
586                    svg_path.to_str().unwrap(),
587                ])
588                .stdin(Stdio::null())
589                .stdout(Stdio::inherit())
590                .stderr(Stdio::inherit())
591                .status()
592                .context("Failed to execute gnuplot")?;
593
594            if !cmd.success() {
595                bail!("gnuplot command failed");
596            }
597        }
598        Ok(())
599    }
600}
601
602// Automatically removes a file when goes out of scope
603struct AutoDelete(PathBuf);
604
605impl Deref for AutoDelete {
606    type Target = PathBuf;
607
608    fn deref(&self) -> &Self::Target {
609        &self.0
610    }
611}
612
613impl Drop for AutoDelete {
614    fn drop(&mut self) {
615        if let Err(e) = fs::remove_file(&self.0) {
616            eprintln!("Failed to delete file {}: {}", self.0.display(), e);
617        }
618    }
619}
620
621mod reporting {
622    use crate::{
623        cli::{colorize, HumanTime},
624        RunResult, Summary,
625    };
626    use colored::Colorize;
627    use std::{
628        io::{self, stderr, Write},
629        time::Duration,
630    };
631
632    pub(super) enum BenchmarkProgress {
633        #[allow(dead_code)]
634        SamplingNo {
635            sample_no: usize,
636            samples_total: usize,
637        },
638        SamplingTime {
639            /// Duration benchmark is already running at the moment
640            duration: Duration,
641            /// How long benchmark should be running
642            required_duration: Duration,
643        },
644    }
645
646    fn clear_progress_line() {
647        eprint!("\r\x1b[2K");
648        let _ = io::stderr().flush();
649    }
650
651    pub(super) trait Reporter {
652        fn benchmark_finished(
653            &self,
654            name: &str,
655            results: &RunResult,
656            aux_metrics: (&[String], &[u64], &[u64]),
657        );
658    }
659
660    pub(super) struct VerboseReporter;
661
662    impl Reporter for VerboseReporter {
663        fn benchmark_finished(
664            &self,
665            name: &str,
666            results: &RunResult,
667            aux_metrics: (&[String], &[u64], &[u64]),
668        ) {
669            clear_progress_line();
670
671            let base = results.baseline;
672            let candidate = results.candidate;
673
674            let significant = results.diff_estimate.significant;
675
676            println!(
677                "{}  (n: {}, outliers: {})",
678                name.bold(),
679                results.diff.n,
680                results.outliers
681            );
682
683            println!(
684                "    {:12}   {:>15} {:>15} {:>15}",
685                "",
686                "baseline".bold(),
687                "candidate".bold(),
688                "\u{2206}".bold(),
689            );
690            println!("    {:12} \u{256d}{}", "", "\u{2500}".repeat(48));
691            println!(
692                "    {:12} \u{2502} {:>15} {:>15} {:>15}  {:>7}{}{}",
693                "mean",
694                HumanTime(base.mean),
695                HumanTime(candidate.mean),
696                colorize(
697                    format!("{}", HumanTime(results.diff.mean)),
698                    significant,
699                    results.diff.mean < 0.
700                ),
701                colorize(
702                    format!("{:+.2}", results.diff_estimate.pct),
703                    significant,
704                    results.diff.mean < 0.
705                ),
706                colorize("%", significant, results.diff.mean < 0.),
707                if significant { "*" } else { "" },
708            );
709            println!(
710                "    {:12} \u{2502} {:>15} {:>15} {:>15}",
711                "min",
712                HumanTime(base.min),
713                HumanTime(candidate.min),
714                HumanTime(candidate.min - base.min)
715            );
716            println!(
717                "    {:12} \u{2502} {:>15} {:>15} {:>15}",
718                "max",
719                HumanTime(base.max),
720                HumanTime(candidate.max),
721                HumanTime(candidate.max - base.max),
722            );
723            println!(
724                "    {:12} \u{2502} {:>15} {:>15} {:>15}",
725                "std. dev.",
726                HumanTime(base.variance.sqrt()),
727                HumanTime(candidate.variance.sqrt()),
728                HumanTime(results.diff.variance.sqrt()),
729            );
730
731            // Printing AUX metrics
732            let (names, b_aux, c_aux) = aux_metrics;
733            for ((name, b), c) in names.iter().zip(b_aux.iter()).zip(c_aux.iter()) {
734                println!(
735                    "{:>16} \u{2502} {:>15} {:>15} {:>15}  {:>+7.2}%",
736                    name,
737                    b,
738                    c,
739                    *c as i64 - *b as i64,
740                    (*c as f64 - *b as f64) / (*b as f64) * 100.0
741                );
742            }
743            println!();
744        }
745    }
746
747    pub(super) struct DefaultReporter;
748
749    impl Reporter for DefaultReporter {
750        fn benchmark_finished(
751            &self,
752            name: &str,
753            results: &RunResult,
754            _: (&[String], &[u64], &[u64]),
755        ) {
756            clear_progress_line();
757
758            let base = results.baseline;
759            let candidate = results.candidate;
760            let diff = results.diff;
761
762            let significant = results.diff_estimate.significant;
763
764            let speedup = results.diff_estimate.pct;
765            let candidate_faster = diff.mean < 0.;
766            println!(
767                "{:50} [ {:>8} ... {:>8} ]    {:>7}{}{}",
768                colorize(name, significant, candidate_faster),
769                HumanTime(base.mean),
770                colorize(
771                    format!("{}", HumanTime(candidate.mean)),
772                    significant,
773                    candidate_faster
774                ),
775                colorize(format!("{:+.2}", speedup), significant, candidate_faster),
776                colorize("%", significant, candidate_faster),
777                if significant { "*" } else { "" },
778            )
779        }
780    }
781
782    /// Reporting the progress of a current benchmarking
783    ///
784    /// Pay attention that frequent reporting might influence the results.
785    /// On AWS it was found that request reporting is adding around ~1% of difference
786    /// between benchmarks.
787    pub(super) fn report_progress(name: &str, progress: BenchmarkProgress) {
788        const BAR_WIDTH: usize = 23;
789        let progress_line = match progress {
790            BenchmarkProgress::SamplingNo {
791                sample_no,
792                samples_total,
793            } => {
794                let sample_no = sample_no.min(samples_total);
795                let filled = (sample_no * BAR_WIDTH) / samples_total;
796                let empty = BAR_WIDTH - filled;
797                format!(
798                    "\r\x1b[2K{:50} [{}{}] {}/{}",
799                    name,
800                    "#".repeat(filled),
801                    ".".repeat(empty),
802                    sample_no,
803                    samples_total,
804                )
805            }
806            BenchmarkProgress::SamplingTime {
807                duration: loop_time,
808                required_duration: total_duration,
809            } => {
810                let filled = loop_time.as_millis() as usize * BAR_WIDTH
811                    / total_duration.as_millis() as usize;
812                let empty = BAR_WIDTH - filled;
813                format!(
814                    "\r\x1b[2K{:50} [{}{}] {:.1}s",
815                    name,
816                    "#".repeat(filled),
817                    ".".repeat(empty),
818                    loop_time.as_secs_f32(),
819                )
820            }
821        };
822        // Explicitly using write_all() imstead of eprint!(), because latter generates a lot
823        // of write() syscalls, which is influencing fairness on Linux
824        let mut out = stderr();
825        let _ = out.write_all(progress_line.as_bytes());
826        let _ = out.flush();
827    }
828
829    pub(super) fn default_reporter_solo(name: &str, results: &Summary<f64>) {
830        println!(
831            "{:50}  [ {:>8} ... {:>8} ... {:>8} ]  stddev: {:>8}",
832            name,
833            HumanTime(results.min),
834            HumanTime(results.mean),
835            HumanTime(results.max),
836            HumanTime(results.variance.sqrt()),
837        )
838    }
839
840    pub(super) fn report_system_time_bias(
841        name: &str,
842        wall_time: Duration,
843        b_sys_time: Duration,
844        c_sys_time: Duration,
845    ) {
846        eprintln!(
847            "{}: {} benchmark spent too much time in system mode. Results may be inaccurate",
848            "WARN".red(),
849            name,
850        );
851        eprintln!("{:>20}: {:?}", "wall time", wall_time);
852        eprintln!("{:>20}: {:?}", "candidate sys", c_sys_time);
853        eprintln!("{:>20}: {:?}", "baseline sys", b_sys_time);
854    }
855}
856
857fn colorize(value: impl Into<String>, do_paint: bool, is_improved: bool) -> impl Display {
858    use colored::Colorize;
859
860    let value = value.into();
861    if do_paint {
862        if is_improved {
863            value.green()
864        } else {
865            value.red()
866        }
867    } else {
868        value.normal()
869    }
870}
871
872struct HumanTime(f64);
873
874impl fmt::Display for HumanTime {
875    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
876        const USEC: f64 = 1_000.;
877        const MSEC: f64 = USEC * 1_000.;
878        const SEC: f64 = MSEC * 1_000.;
879
880        if self.0.abs() > SEC {
881            f.pad(&format!("{:.1} s", self.0 / SEC))
882        } else if self.0.abs() > MSEC {
883            f.pad(&format!("{:.1} ms", self.0 / MSEC))
884        } else if self.0.abs() > USEC {
885            f.pad(&format!("{:.1} us", self.0 / USEC))
886        } else if self.0 == 0. {
887            f.pad("0 ns")
888        } else {
889            f.pad(&format!("{:.1} ns", self.0))
890        }
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897
898    #[test]
899    fn check_human_time() {
900        assert_eq!(format!("{}", HumanTime(0.1)), "0.1 ns");
901        assert_eq!(format!("{:>5}", HumanTime(0.)), " 0 ns");
902
903        assert_eq!(format!("{}", HumanTime(120.)), "120.0 ns");
904
905        assert_eq!(format!("{}", HumanTime(1200.)), "1.2 us");
906
907        assert_eq!(format!("{}", HumanTime(1200000.)), "1.2 ms");
908
909        assert_eq!(format!("{}", HumanTime(1200000000.)), "1.2 s");
910
911        assert_eq!(format!("{}", HumanTime(-1200000.)), "-1.2 ms");
912    }
913
914    // Sane checking some simple patterns
915    #[test]
916    fn check_glob() {
917        let patterns = vec!["a/*/*", "a/**", "*/32/*", "**/b", "a/{32,64}/*"];
918        let input = "a/32/b";
919        for pattern in patterns {
920            assert!(
921                glob_match(pattern, input),
922                "failed to match {} against {}",
923                pattern,
924                input
925            );
926        }
927    }
928}