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_character_boundary(&mut string, MAX_DIRECTORY_NAME_LEN);
93
94 if cfg!(target_os = "windows") {
95 {
96 string = string
97 .trim_end()
105 .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 #[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 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}