1use 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 {
25 #[command(flatten)]
26 bench_flags: CargoBenchFlags,
27 },
28 AuxMetrics {
30 #[command(flatten)]
31 bench_flags: CargoBenchFlags,
32 },
33 Compare(PairedOpts),
35 Solo(SoloOpts),
37 #[command(name = "__worker", hide = true)]
39 Worker(WorkerOpts),
40}
41
42#[derive(Parser, Debug)]
43struct WorkerOpts {
44 #[arg(long = "shmem")]
46 shmem: String,
47
48 #[arg(long = "role")]
50 role: String,
51}
52
53#[derive(Parser, Debug)]
54struct PairedOpts {
55 #[command(flatten)]
56 bench_flags: CargoBenchFlags,
57
58 path: Option<PathBuf>,
60
61 #[arg(short = 'd', long = "dump")]
63 path_to_dump: Option<PathBuf>,
64
65 #[arg(long = "gnuplot")]
67 gnuplot: bool,
68
69 #[arg(long = "seed")]
71 seed: Option<u64>,
72
73 #[arg(short = 's', long = "samples")]
75 samples: Option<NonZeroUsize>,
76
77 #[arg(short = 't', long = "time")]
79 time: Option<f64>,
80
81 #[arg(long = "noise-threshold", default_value_t = 1.0)]
83 noise_threshold: f64,
84
85 #[arg(long = "fail-fast")]
87 fail_fast: bool,
88
89 #[arg(short = 'f', long = "filter")]
91 filter: Option<String>,
92
93 #[arg(long = "strip-candidate")]
96 strip_candidate: Option<String>,
97
98 #[arg(long = "strip-baseline")]
101 strip_baseline: Option<String>,
102
103 #[arg(short = 'g', long = "significant-only", default_value_t = false)]
105 significant_only: bool,
106
107 #[arg(short = 'o', long = "filter-outliers")]
109 filter_outliers: bool,
110
111 #[arg(short = 'q')]
113 quiet: bool,
114
115 #[arg(short = 'v', long = "verbose", default_value_t = false)]
116 verbose: bool,
117
118 #[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 #[arg(long = "seed")]
130 seed: Option<u64>,
131
132 #[arg(short = 's', long = "samples")]
134 samples: Option<NonZeroUsize>,
135
136 #[arg(short = 't', long = "time")]
138 time: Option<f64>,
139
140 #[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#[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 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
288struct 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 const PROGRESS_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
329
330 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 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 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 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 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 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 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 let time_start = Instant::now();
481 while !commpage.is_some_done() {
482 let duration = time_start.elapsed();
483 if let LoopMode::Time(required_duration) = loop_mode {
485 if duration >= required_duration {
486 break;
487 }
488 }
489
490 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 commpage.set_stop();
515 let wall_time = time_start.elapsed();
516
517 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 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 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 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 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 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 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
688struct 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: Duration,
727 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 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 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 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 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 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 assert_eq!(strip_name("foo/a/x/b", "foo/*/x/*"), Some("a/b".into()));
1020
1021 assert_eq!(strip_name("foo/bar", "foo/bar"), Some("foo/bar".into()));
1023
1024 assert_eq!(strip_name("foo/bar", "foo/b*"), Some("ar".into()));
1026 }
1027
1028 #[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}