• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #[cfg(feature = "csv_output")]
2 use crate::csv_report::FileCsvReport;
3 use crate::stats::bivariate::regression::Slope;
4 use crate::stats::univariate::outliers::tukey::LabeledSample;
5 use crate::{html::Html, stats::bivariate::Data};
6 
7 use crate::estimate::{ChangeDistributions, ChangeEstimates, Distributions, Estimate, Estimates};
8 use crate::format;
9 use crate::measurement::ValueFormatter;
10 use crate::stats::univariate::Sample;
11 use crate::stats::Distribution;
12 use crate::{PlotConfiguration, Throughput};
13 use anes::{Attribute, ClearLine, Color, ResetAttributes, SetAttribute, SetForegroundColor};
14 use std::cmp;
15 use std::collections::HashSet;
16 use std::fmt;
17 use std::io::stderr;
18 use std::io::Write;
19 use std::path::{Path, PathBuf};
20 
21 const MAX_DIRECTORY_NAME_LEN: usize = 64;
22 const MAX_TITLE_LEN: usize = 100;
23 
24 pub(crate) struct ComparisonData {
25     pub p_value: f64,
26     pub t_distribution: Distribution<f64>,
27     pub t_value: f64,
28     pub relative_estimates: ChangeEstimates,
29     pub relative_distributions: ChangeDistributions,
30     pub significance_threshold: f64,
31     pub noise_threshold: f64,
32     pub base_iter_counts: Vec<f64>,
33     pub base_sample_times: Vec<f64>,
34     pub base_avg_times: Vec<f64>,
35     pub base_estimates: Estimates,
36 }
37 
38 pub(crate) struct MeasurementData<'a> {
39     pub data: Data<'a, f64, f64>,
40     pub avg_times: LabeledSample<'a, f64>,
41     pub absolute_estimates: Estimates,
42     pub distributions: Distributions,
43     pub comparison: Option<ComparisonData>,
44     pub throughput: Option<Throughput>,
45 }
46 impl<'a> MeasurementData<'a> {
iter_counts(&self) -> &Sample<f64>47     pub fn iter_counts(&self) -> &Sample<f64> {
48         self.data.x()
49     }
50 
51     #[cfg(feature = "csv_output")]
sample_times(&self) -> &Sample<f64>52     pub fn sample_times(&self) -> &Sample<f64> {
53         self.data.y()
54     }
55 }
56 
57 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
58 pub enum ValueType {
59     Bytes,
60     Elements,
61     Value,
62 }
63 
64 #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
65 pub struct BenchmarkId {
66     pub group_id: String,
67     pub function_id: Option<String>,
68     pub value_str: Option<String>,
69     pub throughput: Option<Throughput>,
70     full_id: String,
71     directory_name: String,
72     title: String,
73 }
74 
truncate_to_character_boundary(s: &mut String, max_len: usize)75 fn truncate_to_character_boundary(s: &mut String, max_len: usize) {
76     let mut boundary = cmp::min(max_len, s.len());
77     while !s.is_char_boundary(boundary) {
78         boundary -= 1;
79     }
80     s.truncate(boundary);
81 }
82 
make_filename_safe(string: &str) -> String83 pub fn make_filename_safe(string: &str) -> String {
84     let mut string = string.replace(
85         &['?', '"', '/', '\\', '*', '<', '>', ':', '|', '^'][..],
86         "_",
87     );
88 
89     // Truncate to last character boundary before max length...
90     truncate_to_character_boundary(&mut string, MAX_DIRECTORY_NAME_LEN);
91 
92     if cfg!(target_os = "windows") {
93         {
94             string = string
95                 // On Windows, spaces in the end of the filename are ignored and will be trimmed.
96                 //
97                 // Without trimming ourselves, creating a directory `dir ` will silently create
98                 // `dir` instead, but then operations on files like `dir /file` will fail.
99                 //
100                 // Also note that it's important to do this *after* trimming to MAX_DIRECTORY_NAME_LEN,
101                 // otherwise it can trim again to a name with a trailing space.
102                 .trim_end()
103                 // On Windows, file names are not case-sensitive, so lowercase everything.
104                 .to_lowercase();
105         }
106     }
107 
108     string
109 }
110 
111 impl BenchmarkId {
new( group_id: String, function_id: Option<String>, value_str: Option<String>, throughput: Option<Throughput>, ) -> BenchmarkId112     pub fn new(
113         group_id: String,
114         function_id: Option<String>,
115         value_str: Option<String>,
116         throughput: Option<Throughput>,
117     ) -> BenchmarkId {
118         let full_id = match (&function_id, &value_str) {
119             (&Some(ref func), &Some(ref val)) => format!("{}/{}/{}", group_id, func, val),
120             (&Some(ref func), &None) => format!("{}/{}", group_id, func),
121             (&None, &Some(ref val)) => format!("{}/{}", group_id, val),
122             (&None, &None) => group_id.clone(),
123         };
124 
125         let mut title = full_id.clone();
126         truncate_to_character_boundary(&mut title, MAX_TITLE_LEN);
127         if title != full_id {
128             title.push_str("...");
129         }
130 
131         let directory_name = match (&function_id, &value_str) {
132             (&Some(ref func), &Some(ref val)) => format!(
133                 "{}/{}/{}",
134                 make_filename_safe(&group_id),
135                 make_filename_safe(func),
136                 make_filename_safe(val)
137             ),
138             (&Some(ref func), &None) => format!(
139                 "{}/{}",
140                 make_filename_safe(&group_id),
141                 make_filename_safe(func)
142             ),
143             (&None, &Some(ref val)) => format!(
144                 "{}/{}",
145                 make_filename_safe(&group_id),
146                 make_filename_safe(val)
147             ),
148             (&None, &None) => make_filename_safe(&group_id),
149         };
150 
151         BenchmarkId {
152             group_id,
153             function_id,
154             value_str,
155             throughput,
156             full_id,
157             directory_name,
158             title,
159         }
160     }
161 
id(&self) -> &str162     pub fn id(&self) -> &str {
163         &self.full_id
164     }
165 
as_title(&self) -> &str166     pub fn as_title(&self) -> &str {
167         &self.title
168     }
169 
as_directory_name(&self) -> &str170     pub fn as_directory_name(&self) -> &str {
171         &self.directory_name
172     }
173 
as_number(&self) -> Option<f64>174     pub fn as_number(&self) -> Option<f64> {
175         match self.throughput {
176             Some(Throughput::Bytes(n))
177             | Some(Throughput::Elements(n))
178             | Some(Throughput::BytesDecimal(n)) => Some(n as f64),
179             None => self
180                 .value_str
181                 .as_ref()
182                 .and_then(|string| string.parse::<f64>().ok()),
183         }
184     }
185 
value_type(&self) -> Option<ValueType>186     pub fn value_type(&self) -> Option<ValueType> {
187         match self.throughput {
188             Some(Throughput::Bytes(_)) => Some(ValueType::Bytes),
189             Some(Throughput::BytesDecimal(_)) => Some(ValueType::Bytes),
190             Some(Throughput::Elements(_)) => Some(ValueType::Elements),
191             None => self
192                 .value_str
193                 .as_ref()
194                 .and_then(|string| string.parse::<f64>().ok())
195                 .map(|_| ValueType::Value),
196         }
197     }
198 
ensure_directory_name_unique(&mut self, existing_directories: &HashSet<String>)199     pub fn ensure_directory_name_unique(&mut self, existing_directories: &HashSet<String>) {
200         if !existing_directories.contains(self.as_directory_name()) {
201             return;
202         }
203 
204         let mut counter = 2;
205         loop {
206             let new_dir_name = format!("{}_{}", self.as_directory_name(), counter);
207             if !existing_directories.contains(&new_dir_name) {
208                 self.directory_name = new_dir_name;
209                 return;
210             }
211             counter += 1;
212         }
213     }
214 
ensure_title_unique(&mut self, existing_titles: &HashSet<String>)215     pub fn ensure_title_unique(&mut self, existing_titles: &HashSet<String>) {
216         if !existing_titles.contains(self.as_title()) {
217             return;
218         }
219 
220         let mut counter = 2;
221         loop {
222             let new_title = format!("{} #{}", self.as_title(), counter);
223             if !existing_titles.contains(&new_title) {
224                 self.title = new_title;
225                 return;
226             }
227             counter += 1;
228         }
229     }
230 }
231 impl fmt::Display for BenchmarkId {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result232     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233         f.write_str(self.as_title())
234     }
235 }
236 impl fmt::Debug for BenchmarkId {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result237     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238         fn format_opt(opt: &Option<String>) -> String {
239             match *opt {
240                 Some(ref string) => format!("\"{}\"", string),
241                 None => "None".to_owned(),
242             }
243         }
244 
245         write!(
246             f,
247             "BenchmarkId {{ group_id: \"{}\", function_id: {}, value_str: {}, throughput: {:?} }}",
248             self.group_id,
249             format_opt(&self.function_id),
250             format_opt(&self.value_str),
251             self.throughput,
252         )
253     }
254 }
255 
256 pub struct ReportContext {
257     pub output_directory: PathBuf,
258     pub plot_config: PlotConfiguration,
259 }
260 impl ReportContext {
report_path<P: AsRef<Path> + ?Sized>(&self, id: &BenchmarkId, file_name: &P) -> PathBuf261     pub fn report_path<P: AsRef<Path> + ?Sized>(&self, id: &BenchmarkId, file_name: &P) -> PathBuf {
262         let mut path = self.output_directory.clone();
263         path.push(id.as_directory_name());
264         path.push("report");
265         path.push(file_name);
266         path
267     }
268 }
269 
270 pub(crate) trait Report {
test_start(&self, _id: &BenchmarkId, _context: &ReportContext)271     fn test_start(&self, _id: &BenchmarkId, _context: &ReportContext) {}
test_pass(&self, _id: &BenchmarkId, _context: &ReportContext)272     fn test_pass(&self, _id: &BenchmarkId, _context: &ReportContext) {}
273 
benchmark_start(&self, _id: &BenchmarkId, _context: &ReportContext)274     fn benchmark_start(&self, _id: &BenchmarkId, _context: &ReportContext) {}
profile(&self, _id: &BenchmarkId, _context: &ReportContext, _profile_ns: f64)275     fn profile(&self, _id: &BenchmarkId, _context: &ReportContext, _profile_ns: f64) {}
warmup(&self, _id: &BenchmarkId, _context: &ReportContext, _warmup_ns: f64)276     fn warmup(&self, _id: &BenchmarkId, _context: &ReportContext, _warmup_ns: f64) {}
terminated(&self, _id: &BenchmarkId, _context: &ReportContext)277     fn terminated(&self, _id: &BenchmarkId, _context: &ReportContext) {}
analysis(&self, _id: &BenchmarkId, _context: &ReportContext)278     fn analysis(&self, _id: &BenchmarkId, _context: &ReportContext) {}
measurement_start( &self, _id: &BenchmarkId, _context: &ReportContext, _sample_count: u64, _estimate_ns: f64, _iter_count: u64, )279     fn measurement_start(
280         &self,
281         _id: &BenchmarkId,
282         _context: &ReportContext,
283         _sample_count: u64,
284         _estimate_ns: f64,
285         _iter_count: u64,
286     ) {
287     }
measurement_complete( &self, _id: &BenchmarkId, _context: &ReportContext, _measurements: &MeasurementData<'_>, _formatter: &dyn ValueFormatter, )288     fn measurement_complete(
289         &self,
290         _id: &BenchmarkId,
291         _context: &ReportContext,
292         _measurements: &MeasurementData<'_>,
293         _formatter: &dyn ValueFormatter,
294     ) {
295     }
summarize( &self, _context: &ReportContext, _all_ids: &[BenchmarkId], _formatter: &dyn ValueFormatter, )296     fn summarize(
297         &self,
298         _context: &ReportContext,
299         _all_ids: &[BenchmarkId],
300         _formatter: &dyn ValueFormatter,
301     ) {
302     }
final_summary(&self, _context: &ReportContext)303     fn final_summary(&self, _context: &ReportContext) {}
group_separator(&self)304     fn group_separator(&self) {}
305 }
306 
307 pub(crate) struct Reports {
308     pub(crate) cli_enabled: bool,
309     pub(crate) cli: CliReport,
310     pub(crate) bencher_enabled: bool,
311     pub(crate) bencher: BencherReport,
312     pub(crate) csv_enabled: bool,
313     pub(crate) html: Option<Html>,
314 }
315 macro_rules! reports_impl {
316     (fn $name:ident(&self, $($argn:ident: $argt:ty),*)) => {
317         fn $name(&self, $($argn: $argt),* ) {
318             if self.cli_enabled {
319                 self.cli.$name($($argn),*);
320             }
321             if self.bencher_enabled {
322                 self.bencher.$name($($argn),*);
323             }
324             #[cfg(feature = "csv_output")]
325             if self.csv_enabled {
326                 FileCsvReport.$name($($argn),*);
327             }
328             if let Some(reporter) = &self.html {
329                 reporter.$name($($argn),*);
330             }
331         }
332     };
333 }
334 
335 impl Report for Reports {
336     reports_impl!(fn test_start(&self, id: &BenchmarkId, context: &ReportContext));
337     reports_impl!(fn test_pass(&self, id: &BenchmarkId, context: &ReportContext));
338     reports_impl!(fn benchmark_start(&self, id: &BenchmarkId, context: &ReportContext));
339     reports_impl!(fn profile(&self, id: &BenchmarkId, context: &ReportContext, profile_ns: f64));
340     reports_impl!(fn warmup(&self, id: &BenchmarkId, context: &ReportContext, warmup_ns: f64));
341     reports_impl!(fn terminated(&self, id: &BenchmarkId, context: &ReportContext));
342     reports_impl!(fn analysis(&self, id: &BenchmarkId, context: &ReportContext));
343     reports_impl!(fn measurement_start(
344         &self,
345         id: &BenchmarkId,
346         context: &ReportContext,
347         sample_count: u64,
348         estimate_ns: f64,
349         iter_count: u64
350     ));
351     reports_impl!(
352     fn measurement_complete(
353         &self,
354         id: &BenchmarkId,
355         context: &ReportContext,
356         measurements: &MeasurementData<'_>,
357         formatter: &dyn ValueFormatter
358     ));
359     reports_impl!(
360     fn summarize(
361         &self,
362         context: &ReportContext,
363         all_ids: &[BenchmarkId],
364         formatter: &dyn ValueFormatter
365     ));
366 
367     reports_impl!(fn final_summary(&self, context: &ReportContext));
368     reports_impl!(fn group_separator(&self, ));
369 }
370 
371 #[derive(Debug, Clone, Copy, Eq, PartialEq)]
372 pub(crate) enum CliVerbosity {
373     Quiet,
374     Normal,
375     Verbose,
376 }
377 
378 pub(crate) struct CliReport {
379     pub enable_text_overwrite: bool,
380     pub enable_text_coloring: bool,
381     pub verbosity: CliVerbosity,
382 }
383 impl CliReport {
new( enable_text_overwrite: bool, enable_text_coloring: bool, verbosity: CliVerbosity, ) -> CliReport384     pub fn new(
385         enable_text_overwrite: bool,
386         enable_text_coloring: bool,
387         verbosity: CliVerbosity,
388     ) -> CliReport {
389         CliReport {
390             enable_text_overwrite,
391             enable_text_coloring,
392             verbosity,
393         }
394     }
395 
text_overwrite(&self)396     fn text_overwrite(&self) {
397         if self.enable_text_overwrite {
398             eprint!("\r{}", ClearLine::All)
399         }
400     }
401 
402     // Passing a String is the common case here.
403     #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_pass_by_value))]
print_overwritable(&self, s: String)404     fn print_overwritable(&self, s: String) {
405         if self.enable_text_overwrite {
406             eprint!("{}", s);
407             stderr().flush().unwrap();
408         } else {
409             eprintln!("{}", s);
410         }
411     }
412 
with_color(&self, color: Color, s: &str) -> String413     fn with_color(&self, color: Color, s: &str) -> String {
414         if self.enable_text_coloring {
415             format!("{}{}{}", SetForegroundColor(color), s, ResetAttributes)
416         } else {
417             String::from(s)
418         }
419     }
420 
green(&self, s: &str) -> String421     fn green(&self, s: &str) -> String {
422         self.with_color(Color::DarkGreen, s)
423     }
424 
yellow(&self, s: &str) -> String425     fn yellow(&self, s: &str) -> String {
426         self.with_color(Color::DarkYellow, s)
427     }
428 
red(&self, s: &str) -> String429     fn red(&self, s: &str) -> String {
430         self.with_color(Color::DarkRed, s)
431     }
432 
bold(&self, s: String) -> String433     fn bold(&self, s: String) -> String {
434         if self.enable_text_coloring {
435             format!("{}{}{}", SetAttribute(Attribute::Bold), s, ResetAttributes)
436         } else {
437             s
438         }
439     }
440 
faint(&self, s: String) -> String441     fn faint(&self, s: String) -> String {
442         if self.enable_text_coloring {
443             format!("{}{}{}", SetAttribute(Attribute::Faint), s, ResetAttributes)
444         } else {
445             s
446         }
447     }
448 
outliers(&self, sample: &LabeledSample<'_, f64>)449     pub fn outliers(&self, sample: &LabeledSample<'_, f64>) {
450         let (los, lom, _, him, his) = sample.count();
451         let noutliers = los + lom + him + his;
452         let sample_size = sample.len();
453 
454         if noutliers == 0 {
455             return;
456         }
457 
458         let percent = |n: usize| 100. * n as f64 / sample_size as f64;
459 
460         println!(
461             "{}",
462             self.yellow(&format!(
463                 "Found {} outliers among {} measurements ({:.2}%)",
464                 noutliers,
465                 sample_size,
466                 percent(noutliers)
467             ))
468         );
469 
470         let print = |n, label| {
471             if n != 0 {
472                 println!("  {} ({:.2}%) {}", n, percent(n), label);
473             }
474         };
475 
476         print(los, "low severe");
477         print(lom, "low mild");
478         print(him, "high mild");
479         print(his, "high severe");
480     }
481 }
482 impl Report for CliReport {
test_start(&self, id: &BenchmarkId, _: &ReportContext)483     fn test_start(&self, id: &BenchmarkId, _: &ReportContext) {
484         println!("Testing {}", id);
485     }
test_pass(&self, _: &BenchmarkId, _: &ReportContext)486     fn test_pass(&self, _: &BenchmarkId, _: &ReportContext) {
487         println!("Success");
488     }
489 
benchmark_start(&self, id: &BenchmarkId, _: &ReportContext)490     fn benchmark_start(&self, id: &BenchmarkId, _: &ReportContext) {
491         self.print_overwritable(format!("Benchmarking {}", id));
492     }
493 
profile(&self, id: &BenchmarkId, _: &ReportContext, warmup_ns: f64)494     fn profile(&self, id: &BenchmarkId, _: &ReportContext, warmup_ns: f64) {
495         self.text_overwrite();
496         self.print_overwritable(format!(
497             "Benchmarking {}: Profiling for {}",
498             id,
499             format::time(warmup_ns)
500         ));
501     }
502 
warmup(&self, id: &BenchmarkId, _: &ReportContext, warmup_ns: f64)503     fn warmup(&self, id: &BenchmarkId, _: &ReportContext, warmup_ns: f64) {
504         self.text_overwrite();
505         self.print_overwritable(format!(
506             "Benchmarking {}: Warming up for {}",
507             id,
508             format::time(warmup_ns)
509         ));
510     }
511 
terminated(&self, id: &BenchmarkId, _: &ReportContext)512     fn terminated(&self, id: &BenchmarkId, _: &ReportContext) {
513         self.text_overwrite();
514         println!("Benchmarking {}: Complete (Analysis Disabled)", id);
515     }
516 
analysis(&self, id: &BenchmarkId, _: &ReportContext)517     fn analysis(&self, id: &BenchmarkId, _: &ReportContext) {
518         self.text_overwrite();
519         self.print_overwritable(format!("Benchmarking {}: Analyzing", id));
520     }
521 
measurement_start( &self, id: &BenchmarkId, _: &ReportContext, sample_count: u64, estimate_ns: f64, iter_count: u64, )522     fn measurement_start(
523         &self,
524         id: &BenchmarkId,
525         _: &ReportContext,
526         sample_count: u64,
527         estimate_ns: f64,
528         iter_count: u64,
529     ) {
530         self.text_overwrite();
531         let iter_string = if matches!(self.verbosity, CliVerbosity::Verbose) {
532             format!("{} iterations", iter_count)
533         } else {
534             format::iter_count(iter_count)
535         };
536 
537         self.print_overwritable(format!(
538             "Benchmarking {}: Collecting {} samples in estimated {} ({})",
539             id,
540             sample_count,
541             format::time(estimate_ns),
542             iter_string
543         ));
544     }
545 
measurement_complete( &self, id: &BenchmarkId, _: &ReportContext, meas: &MeasurementData<'_>, formatter: &dyn ValueFormatter, )546     fn measurement_complete(
547         &self,
548         id: &BenchmarkId,
549         _: &ReportContext,
550         meas: &MeasurementData<'_>,
551         formatter: &dyn ValueFormatter,
552     ) {
553         self.text_overwrite();
554 
555         let typical_estimate = &meas.absolute_estimates.typical();
556 
557         {
558             let mut id = id.as_title().to_owned();
559 
560             if id.len() > 23 {
561                 println!("{}", self.green(&id));
562                 id.clear();
563             }
564             let id_len = id.len();
565 
566             println!(
567                 "{}{}time:   [{} {} {}]",
568                 self.green(&id),
569                 " ".repeat(24 - id_len),
570                 self.faint(
571                     formatter.format_value(typical_estimate.confidence_interval.lower_bound)
572                 ),
573                 self.bold(formatter.format_value(typical_estimate.point_estimate)),
574                 self.faint(
575                     formatter.format_value(typical_estimate.confidence_interval.upper_bound)
576                 )
577             );
578         }
579 
580         if let Some(ref throughput) = meas.throughput {
581             println!(
582                 "{}thrpt:  [{} {} {}]",
583                 " ".repeat(24),
584                 self.faint(formatter.format_throughput(
585                     throughput,
586                     typical_estimate.confidence_interval.upper_bound
587                 )),
588                 self.bold(formatter.format_throughput(throughput, typical_estimate.point_estimate)),
589                 self.faint(formatter.format_throughput(
590                     throughput,
591                     typical_estimate.confidence_interval.lower_bound
592                 )),
593             )
594         }
595 
596         if !matches!(self.verbosity, CliVerbosity::Quiet) {
597             if let Some(ref comp) = meas.comparison {
598                 let different_mean = comp.p_value < comp.significance_threshold;
599                 let mean_est = &comp.relative_estimates.mean;
600                 let point_estimate = mean_est.point_estimate;
601                 let mut point_estimate_str = format::change(point_estimate, true);
602                 // The change in throughput is related to the change in timing. Reducing the timing by
603                 // 50% increases the throughput by 100%.
604                 let to_thrpt_estimate = |ratio: f64| 1.0 / (1.0 + ratio) - 1.0;
605                 let mut thrpt_point_estimate_str =
606                     format::change(to_thrpt_estimate(point_estimate), true);
607                 let explanation_str: String;
608 
609                 if !different_mean {
610                     explanation_str = "No change in performance detected.".to_owned();
611                 } else {
612                     let comparison = compare_to_threshold(mean_est, comp.noise_threshold);
613                     match comparison {
614                         ComparisonResult::Improved => {
615                             point_estimate_str = self.green(&self.bold(point_estimate_str));
616                             thrpt_point_estimate_str =
617                                 self.green(&self.bold(thrpt_point_estimate_str));
618                             explanation_str =
619                                 format!("Performance has {}.", self.green("improved"));
620                         }
621                         ComparisonResult::Regressed => {
622                             point_estimate_str = self.red(&self.bold(point_estimate_str));
623                             thrpt_point_estimate_str =
624                                 self.red(&self.bold(thrpt_point_estimate_str));
625                             explanation_str = format!("Performance has {}.", self.red("regressed"));
626                         }
627                         ComparisonResult::NonSignificant => {
628                             explanation_str = "Change within noise threshold.".to_owned();
629                         }
630                     }
631                 }
632 
633                 if meas.throughput.is_some() {
634                     println!("{}change:", " ".repeat(17));
635 
636                     println!(
637                         "{}time:   [{} {} {}] (p = {:.2} {} {:.2})",
638                         " ".repeat(24),
639                         self.faint(format::change(
640                             mean_est.confidence_interval.lower_bound,
641                             true
642                         )),
643                         point_estimate_str,
644                         self.faint(format::change(
645                             mean_est.confidence_interval.upper_bound,
646                             true
647                         )),
648                         comp.p_value,
649                         if different_mean { "<" } else { ">" },
650                         comp.significance_threshold
651                     );
652                     println!(
653                         "{}thrpt:  [{} {} {}]",
654                         " ".repeat(24),
655                         self.faint(format::change(
656                             to_thrpt_estimate(mean_est.confidence_interval.upper_bound),
657                             true
658                         )),
659                         thrpt_point_estimate_str,
660                         self.faint(format::change(
661                             to_thrpt_estimate(mean_est.confidence_interval.lower_bound),
662                             true
663                         )),
664                     );
665                 } else {
666                     println!(
667                         "{}change: [{} {} {}] (p = {:.2} {} {:.2})",
668                         " ".repeat(24),
669                         self.faint(format::change(
670                             mean_est.confidence_interval.lower_bound,
671                             true
672                         )),
673                         point_estimate_str,
674                         self.faint(format::change(
675                             mean_est.confidence_interval.upper_bound,
676                             true
677                         )),
678                         comp.p_value,
679                         if different_mean { "<" } else { ">" },
680                         comp.significance_threshold
681                     );
682                 }
683 
684                 println!("{}{}", " ".repeat(24), explanation_str);
685             }
686         }
687 
688         if !matches!(self.verbosity, CliVerbosity::Quiet) {
689             self.outliers(&meas.avg_times);
690         }
691 
692         if matches!(self.verbosity, CliVerbosity::Verbose) {
693             let format_short_estimate = |estimate: &Estimate| -> String {
694                 format!(
695                     "[{} {}]",
696                     formatter.format_value(estimate.confidence_interval.lower_bound),
697                     formatter.format_value(estimate.confidence_interval.upper_bound)
698                 )
699             };
700 
701             let data = &meas.data;
702             if let Some(slope_estimate) = meas.absolute_estimates.slope.as_ref() {
703                 println!(
704                     "{:<7}{} {:<15}[{:0.7} {:0.7}]",
705                     "slope",
706                     format_short_estimate(slope_estimate),
707                     "R^2",
708                     Slope(slope_estimate.confidence_interval.lower_bound).r_squared(data),
709                     Slope(slope_estimate.confidence_interval.upper_bound).r_squared(data),
710                 );
711             }
712             println!(
713                 "{:<7}{} {:<15}{}",
714                 "mean",
715                 format_short_estimate(&meas.absolute_estimates.mean),
716                 "std. dev.",
717                 format_short_estimate(&meas.absolute_estimates.std_dev),
718             );
719             println!(
720                 "{:<7}{} {:<15}{}",
721                 "median",
722                 format_short_estimate(&meas.absolute_estimates.median),
723                 "med. abs. dev.",
724                 format_short_estimate(&meas.absolute_estimates.median_abs_dev),
725             );
726         }
727     }
728 
group_separator(&self)729     fn group_separator(&self) {
730         println!();
731     }
732 }
733 
734 pub struct BencherReport;
735 impl Report for BencherReport {
measurement_start( &self, id: &BenchmarkId, _context: &ReportContext, _sample_count: u64, _estimate_ns: f64, _iter_count: u64, )736     fn measurement_start(
737         &self,
738         id: &BenchmarkId,
739         _context: &ReportContext,
740         _sample_count: u64,
741         _estimate_ns: f64,
742         _iter_count: u64,
743     ) {
744         print!("test {} ... ", id);
745     }
746 
measurement_complete( &self, _id: &BenchmarkId, _: &ReportContext, meas: &MeasurementData<'_>, formatter: &dyn ValueFormatter, )747     fn measurement_complete(
748         &self,
749         _id: &BenchmarkId,
750         _: &ReportContext,
751         meas: &MeasurementData<'_>,
752         formatter: &dyn ValueFormatter,
753     ) {
754         let mut values = [
755             meas.absolute_estimates.median.point_estimate,
756             meas.absolute_estimates.std_dev.point_estimate,
757         ];
758         let unit = formatter.scale_for_machines(&mut values);
759 
760         println!(
761             "bench: {:>11} {}/iter (+/- {})",
762             format::integer(values[0]),
763             unit,
764             format::integer(values[1])
765         );
766     }
767 
group_separator(&self)768     fn group_separator(&self) {
769         println!();
770     }
771 }
772 
773 enum ComparisonResult {
774     Improved,
775     Regressed,
776     NonSignificant,
777 }
778 
compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult779 fn compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult {
780     let ci = &estimate.confidence_interval;
781     let lb = ci.lower_bound;
782     let ub = ci.upper_bound;
783 
784     if lb < -noise && ub < -noise {
785         ComparisonResult::Improved
786     } else if lb > noise && ub > noise {
787         ComparisonResult::Regressed
788     } else {
789         ComparisonResult::NonSignificant
790     }
791 }
792 
793 #[cfg(test)]
794 mod test {
795     use super::*;
796 
797     #[test]
test_make_filename_safe_replaces_characters()798     fn test_make_filename_safe_replaces_characters() {
799         let input = "?/\\*\"";
800         let safe = make_filename_safe(input);
801         assert_eq!("_____", &safe);
802     }
803 
804     #[test]
test_make_filename_safe_truncates_long_strings()805     fn test_make_filename_safe_truncates_long_strings() {
806         let input = "this is a very long string. it is too long to be safe as a directory name, and so it needs to be truncated. what a long string this is.";
807         let safe = make_filename_safe(input);
808         assert!(input.len() > MAX_DIRECTORY_NAME_LEN);
809         assert_eq!(&input[0..MAX_DIRECTORY_NAME_LEN], &safe);
810     }
811 
812     #[test]
test_make_filename_safe_respects_character_boundaries()813     fn test_make_filename_safe_respects_character_boundaries() {
814         let input = "✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓";
815         let safe = make_filename_safe(input);
816         assert!(safe.len() < MAX_DIRECTORY_NAME_LEN);
817     }
818 
819     #[test]
test_benchmark_id_make_directory_name_unique()820     fn test_benchmark_id_make_directory_name_unique() {
821         let existing_id = BenchmarkId::new(
822             "group".to_owned(),
823             Some("function".to_owned()),
824             Some("value".to_owned()),
825             None,
826         );
827         let mut directories = HashSet::new();
828         directories.insert(existing_id.as_directory_name().to_owned());
829 
830         let mut new_id = existing_id.clone();
831         new_id.ensure_directory_name_unique(&directories);
832         assert_eq!("group/function/value_2", new_id.as_directory_name());
833         directories.insert(new_id.as_directory_name().to_owned());
834 
835         new_id = existing_id;
836         new_id.ensure_directory_name_unique(&directories);
837         assert_eq!("group/function/value_3", new_id.as_directory_name());
838         directories.insert(new_id.as_directory_name().to_owned());
839     }
840     #[test]
test_benchmark_id_make_long_directory_name_unique()841     fn test_benchmark_id_make_long_directory_name_unique() {
842         let long_name = (0..MAX_DIRECTORY_NAME_LEN).map(|_| 'a').collect::<String>();
843         let existing_id = BenchmarkId::new(long_name, None, None, None);
844         let mut directories = HashSet::new();
845         directories.insert(existing_id.as_directory_name().to_owned());
846 
847         let mut new_id = existing_id.clone();
848         new_id.ensure_directory_name_unique(&directories);
849         assert_ne!(existing_id.as_directory_name(), new_id.as_directory_name());
850     }
851 }
852