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