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;
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(short = 'g', long = "significant-only", default_value_t = false)]
95 significant_only: bool,
96
97 #[arg(short = 'o', long = "filter-outliers")]
99 filter_outliers: bool,
100
101 #[arg(short = 'q')]
103 quiet: bool,
104
105 #[arg(short = 'v', long = "verbose", default_value_t = false)]
106 verbose: bool,
107
108 #[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 #[arg(long = "seed")]
120 seed: Option<u64>,
121
122 #[arg(short = 's', long = "samples")]
124 samples: Option<NonZeroUsize>,
125
126 #[arg(short = 't', long = "time")]
128 time: Option<f64>,
129
130 #[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#[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 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 const PROGRESS_UPDATE_INTERVAL: Duration = Duration::from_millis(50);
301
302 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 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 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 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 .filter(|(_, f)| filter.is_empty() || glob_match(filter, f))
403 .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 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 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 let time_start = Instant::now();
433 while !commpage.is_some_done() {
434 let duration = time_start.elapsed();
435 if let LoopMode::Time(required_duration) = loop_mode {
437 if duration >= required_duration {
438 break;
439 }
440 }
441
442 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 commpage.set_stop();
467 let wall_time = time_start.elapsed();
468
469 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 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 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 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
602struct 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: Duration,
641 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 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 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 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 #[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}