Skip to main content

criterion/
report.rs

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