1 use crate::report::{make_filename_safe, BenchmarkId, MeasurementData, Report, ReportContext};
2 use crate::stats::bivariate::regression::Slope;
3 
4 use crate::estimate::Estimate;
5 use crate::format;
6 use crate::fs;
7 use crate::measurement::ValueFormatter;
8 use crate::plot::{PlotContext, PlotData, Plotter};
9 use crate::SavedSample;
10 use criterion_plot::Size;
11 use serde::Serialize;
12 use std::cell::RefCell;
13 use std::cmp::Ordering;
14 use std::collections::{BTreeSet, HashMap};
15 use std::path::{Path, PathBuf};
16 use tinytemplate::TinyTemplate;
17 
18 const THUMBNAIL_SIZE: Option<Size> = Some(Size(450, 300));
19 
debug_context<S: Serialize>(path: &Path, context: &S)20 fn debug_context<S: Serialize>(path: &Path, context: &S) {
21     if crate::debug_enabled() {
22         let mut context_path = PathBuf::from(path);
23         context_path.set_extension("json");
24         println!("Writing report context to {:?}", context_path);
25         let result = fs::save(context, &context_path);
26         if let Err(e) = result {
27             error!("Failed to write report context debug output: {}", e);
28         }
29     }
30 }
31 
32 #[derive(Serialize)]
33 struct Context {
34     title: String,
35     confidence: String,
36 
37     thumbnail_width: usize,
38     thumbnail_height: usize,
39 
40     slope: Option<ConfidenceInterval>,
41     r2: ConfidenceInterval,
42     mean: ConfidenceInterval,
43     std_dev: ConfidenceInterval,
44     median: ConfidenceInterval,
45     mad: ConfidenceInterval,
46     throughput: Option<ConfidenceInterval>,
47 
48     additional_plots: Vec<Plot>,
49 
50     comparison: Option<Comparison>,
51 }
52 
53 #[derive(Serialize)]
54 struct IndividualBenchmark {
55     name: String,
56     path: String,
57     regression_exists: bool,
58 }
59 impl IndividualBenchmark {
from_id( output_directory: &Path, path_prefix: &str, id: &BenchmarkId, ) -> IndividualBenchmark60     fn from_id(
61         output_directory: &Path,
62         path_prefix: &str,
63         id: &BenchmarkId,
64     ) -> IndividualBenchmark {
65         let mut regression_path = PathBuf::from(output_directory);
66         regression_path.push(id.as_directory_name());
67         regression_path.push("report");
68         regression_path.push("regression.svg");
69 
70         IndividualBenchmark {
71             name: id.as_title().to_owned(),
72             path: format!("{}/{}", path_prefix, id.as_directory_name()),
73             regression_exists: regression_path.is_file(),
74         }
75     }
76 }
77 
78 #[derive(Serialize)]
79 struct SummaryContext {
80     group_id: String,
81 
82     thumbnail_width: usize,
83     thumbnail_height: usize,
84 
85     violin_plot: Option<String>,
86     line_chart: Option<String>,
87 
88     benchmarks: Vec<IndividualBenchmark>,
89 }
90 
91 #[derive(Serialize)]
92 struct ConfidenceInterval {
93     lower: String,
94     upper: String,
95     point: String,
96 }
97 
98 #[derive(Serialize)]
99 struct Plot {
100     name: String,
101     url: String,
102 }
103 impl Plot {
new(name: &str, url: &str) -> Plot104     fn new(name: &str, url: &str) -> Plot {
105         Plot {
106             name: name.to_owned(),
107             url: url.to_owned(),
108         }
109     }
110 }
111 
112 #[derive(Serialize)]
113 struct Comparison {
114     p_value: String,
115     inequality: String,
116     significance_level: String,
117     explanation: String,
118 
119     change: ConfidenceInterval,
120     thrpt_change: Option<ConfidenceInterval>,
121     additional_plots: Vec<Plot>,
122 }
123 
if_exists(output_directory: &Path, path: &Path) -> Option<String>124 fn if_exists(output_directory: &Path, path: &Path) -> Option<String> {
125     let report_path = path.join("report/index.html");
126     if PathBuf::from(output_directory).join(&report_path).is_file() {
127         Some(report_path.to_string_lossy().to_string())
128     } else {
129         None
130     }
131 }
132 #[derive(Serialize, Debug)]
133 struct ReportLink<'a> {
134     name: &'a str,
135     path: Option<String>,
136 }
137 impl<'a> ReportLink<'a> {
138     // TODO: Would be nice if I didn't have to keep making these components filename-safe.
group(output_directory: &Path, group_id: &'a str) -> ReportLink<'a>139     fn group(output_directory: &Path, group_id: &'a str) -> ReportLink<'a> {
140         let path = PathBuf::from(make_filename_safe(group_id));
141 
142         ReportLink {
143             name: group_id,
144             path: if_exists(output_directory, &path),
145         }
146     }
147 
function(output_directory: &Path, group_id: &str, function_id: &'a str) -> ReportLink<'a>148     fn function(output_directory: &Path, group_id: &str, function_id: &'a str) -> ReportLink<'a> {
149         let mut path = PathBuf::from(make_filename_safe(group_id));
150         path.push(make_filename_safe(function_id));
151 
152         ReportLink {
153             name: function_id,
154             path: if_exists(output_directory, &path),
155         }
156     }
157 
value(output_directory: &Path, group_id: &str, value_str: &'a str) -> ReportLink<'a>158     fn value(output_directory: &Path, group_id: &str, value_str: &'a str) -> ReportLink<'a> {
159         let mut path = PathBuf::from(make_filename_safe(group_id));
160         path.push(make_filename_safe(value_str));
161 
162         ReportLink {
163             name: value_str,
164             path: if_exists(output_directory, &path),
165         }
166     }
167 
individual(output_directory: &Path, id: &'a BenchmarkId) -> ReportLink<'a>168     fn individual(output_directory: &Path, id: &'a BenchmarkId) -> ReportLink<'a> {
169         let path = PathBuf::from(id.as_directory_name());
170         ReportLink {
171             name: id.as_title(),
172             path: if_exists(output_directory, &path),
173         }
174     }
175 }
176 
177 #[derive(Serialize)]
178 struct BenchmarkValueGroup<'a> {
179     value: Option<ReportLink<'a>>,
180     benchmarks: Vec<ReportLink<'a>>,
181 }
182 
183 #[derive(Serialize)]
184 struct BenchmarkGroup<'a> {
185     group_report: ReportLink<'a>,
186 
187     function_ids: Option<Vec<ReportLink<'a>>>,
188     values: Option<Vec<ReportLink<'a>>>,
189 
190     individual_links: Vec<BenchmarkValueGroup<'a>>,
191 }
192 impl<'a> BenchmarkGroup<'a> {
new(output_directory: &Path, ids: &[&'a BenchmarkId]) -> BenchmarkGroup<'a>193     fn new(output_directory: &Path, ids: &[&'a BenchmarkId]) -> BenchmarkGroup<'a> {
194         let group_id = &ids[0].group_id;
195         let group_report = ReportLink::group(output_directory, group_id);
196 
197         let mut function_ids = Vec::with_capacity(ids.len());
198         let mut values = Vec::with_capacity(ids.len());
199         let mut individual_links = HashMap::with_capacity(ids.len());
200 
201         for id in ids.iter() {
202             let function_id = id.function_id.as_ref().map(String::as_str);
203             let value = id.value_str.as_ref().map(String::as_str);
204 
205             let individual_link = ReportLink::individual(output_directory, id);
206 
207             function_ids.push(function_id);
208             values.push(value);
209 
210             individual_links.insert((function_id, value), individual_link);
211         }
212 
213         fn parse_opt(os: &Option<&str>) -> Option<f64> {
214             os.and_then(|s| s.parse::<f64>().ok())
215         }
216 
217         // If all of the value strings can be parsed into a number, sort/dedupe
218         // numerically. Otherwise sort lexicographically.
219         if values.iter().all(|os| parse_opt(os).is_some()) {
220             values.sort_unstable_by(|v1, v2| {
221                 let num1 = parse_opt(&v1);
222                 let num2 = parse_opt(&v2);
223 
224                 num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
225             });
226             values.dedup_by_key(|os| parse_opt(&os).unwrap());
227         } else {
228             values.sort_unstable();
229             values.dedup();
230         }
231 
232         // Sort and dedupe functions by name.
233         function_ids.sort_unstable();
234         function_ids.dedup();
235 
236         let mut value_groups = Vec::with_capacity(values.len());
237         for value in values.iter() {
238             let row = function_ids
239                 .iter()
240                 .filter_map(|f| individual_links.remove(&(*f, *value)))
241                 .collect::<Vec<_>>();
242             value_groups.push(BenchmarkValueGroup {
243                 value: value.map(|s| ReportLink::value(output_directory, group_id, s)),
244                 benchmarks: row,
245             });
246         }
247 
248         let function_ids = function_ids
249             .into_iter()
250             .map(|os| os.map(|s| ReportLink::function(output_directory, group_id, s)))
251             .collect::<Option<Vec<_>>>();
252         let values = values
253             .into_iter()
254             .map(|os| os.map(|s| ReportLink::value(output_directory, group_id, s)))
255             .collect::<Option<Vec<_>>>();
256 
257         BenchmarkGroup {
258             group_report,
259             function_ids,
260             values,
261             individual_links: value_groups,
262         }
263     }
264 }
265 
266 #[derive(Serialize)]
267 struct IndexContext<'a> {
268     groups: Vec<BenchmarkGroup<'a>>,
269 }
270 
271 pub struct Html {
272     templates: TinyTemplate<'static>,
273     plotter: RefCell<Box<dyn Plotter>>,
274 }
275 impl Html {
new(plotter: Box<dyn Plotter>) -> Html276     pub(crate) fn new(plotter: Box<dyn Plotter>) -> Html {
277         let mut templates = TinyTemplate::new();
278         templates
279             .add_template("report_link", include_str!("report_link.html.tt"))
280             .expect("Unable to parse report_link template.");
281         templates
282             .add_template("index", include_str!("index.html.tt"))
283             .expect("Unable to parse index template.");
284         templates
285             .add_template("benchmark_report", include_str!("benchmark_report.html.tt"))
286             .expect("Unable to parse benchmark_report template");
287         templates
288             .add_template("summary_report", include_str!("summary_report.html.tt"))
289             .expect("Unable to parse summary_report template");
290 
291         let plotter = RefCell::new(plotter);
292         Html { templates, plotter }
293     }
294 }
295 impl Report for Html {
measurement_complete( &self, id: &BenchmarkId, report_context: &ReportContext, measurements: &MeasurementData<'_>, formatter: &dyn ValueFormatter, )296     fn measurement_complete(
297         &self,
298         id: &BenchmarkId,
299         report_context: &ReportContext,
300         measurements: &MeasurementData<'_>,
301         formatter: &dyn ValueFormatter,
302     ) {
303         try_else_return!({
304             let mut report_dir = report_context.output_directory.clone();
305             report_dir.push(id.as_directory_name());
306             report_dir.push("report");
307             fs::mkdirp(&report_dir)
308         });
309 
310         let typical_estimate = &measurements.absolute_estimates.typical();
311 
312         let time_interval = |est: &Estimate| -> ConfidenceInterval {
313             ConfidenceInterval {
314                 lower: formatter.format_value(est.confidence_interval.lower_bound),
315                 point: formatter.format_value(est.point_estimate),
316                 upper: formatter.format_value(est.confidence_interval.upper_bound),
317             }
318         };
319 
320         let data = measurements.data;
321 
322         elapsed! {
323             "Generating plots",
324             self.generate_plots(id, report_context, formatter, measurements)
325         }
326 
327         let mut additional_plots = vec![
328             Plot::new("Typical", "typical.svg"),
329             Plot::new("Mean", "mean.svg"),
330             Plot::new("Std. Dev.", "SD.svg"),
331             Plot::new("Median", "median.svg"),
332             Plot::new("MAD", "MAD.svg"),
333         ];
334         if measurements.absolute_estimates.slope.is_some() {
335             additional_plots.push(Plot::new("Slope", "slope.svg"));
336         }
337 
338         let throughput = measurements
339             .throughput
340             .as_ref()
341             .map(|thr| ConfidenceInterval {
342                 lower: formatter
343                     .format_throughput(thr, typical_estimate.confidence_interval.upper_bound),
344                 upper: formatter
345                     .format_throughput(thr, typical_estimate.confidence_interval.lower_bound),
346                 point: formatter.format_throughput(thr, typical_estimate.point_estimate),
347             });
348 
349         let context = Context {
350             title: id.as_title().to_owned(),
351             confidence: format!(
352                 "{:.2}",
353                 typical_estimate.confidence_interval.confidence_level
354             ),
355 
356             thumbnail_width: THUMBNAIL_SIZE.unwrap().0,
357             thumbnail_height: THUMBNAIL_SIZE.unwrap().1,
358 
359             slope: measurements
360                 .absolute_estimates
361                 .slope
362                 .as_ref()
363                 .map(time_interval),
364             mean: time_interval(&measurements.absolute_estimates.mean),
365             median: time_interval(&measurements.absolute_estimates.median),
366             mad: time_interval(&measurements.absolute_estimates.median_abs_dev),
367             std_dev: time_interval(&measurements.absolute_estimates.std_dev),
368             throughput,
369 
370             r2: ConfidenceInterval {
371                 lower: format!(
372                     "{:0.7}",
373                     Slope(typical_estimate.confidence_interval.lower_bound).r_squared(&data)
374                 ),
375                 upper: format!(
376                     "{:0.7}",
377                     Slope(typical_estimate.confidence_interval.upper_bound).r_squared(&data)
378                 ),
379                 point: format!(
380                     "{:0.7}",
381                     Slope(typical_estimate.point_estimate).r_squared(&data)
382                 ),
383             },
384 
385             additional_plots,
386 
387             comparison: self.comparison(measurements),
388         };
389 
390         let mut report_path = report_context.output_directory.clone();
391         report_path.push(id.as_directory_name());
392         report_path.push("report");
393         report_path.push("index.html");
394         debug_context(&report_path, &context);
395 
396         let text = self
397             .templates
398             .render("benchmark_report", &context)
399             .expect("Failed to render benchmark report template");
400         try_else_return!(fs::save_string(&text, &report_path));
401     }
402 
summarize( &self, context: &ReportContext, all_ids: &[BenchmarkId], formatter: &dyn ValueFormatter, )403     fn summarize(
404         &self,
405         context: &ReportContext,
406         all_ids: &[BenchmarkId],
407         formatter: &dyn ValueFormatter,
408     ) {
409         let all_ids = all_ids
410             .iter()
411             .filter(|id| {
412                 let id_dir = context.output_directory.join(id.as_directory_name());
413                 fs::is_dir(&id_dir)
414             })
415             .collect::<Vec<_>>();
416         if all_ids.is_empty() {
417             return;
418         }
419 
420         let group_id = all_ids[0].group_id.clone();
421 
422         let data = self.load_summary_data(&context.output_directory, &all_ids);
423 
424         let mut function_ids = BTreeSet::new();
425         let mut value_strs = Vec::with_capacity(all_ids.len());
426         for id in all_ids {
427             if let Some(ref function_id) = id.function_id {
428                 function_ids.insert(function_id);
429             }
430             if let Some(ref value_str) = id.value_str {
431                 value_strs.push(value_str);
432             }
433         }
434 
435         fn try_parse(s: &str) -> Option<f64> {
436             s.parse::<f64>().ok()
437         }
438 
439         // If all of the value strings can be parsed into a number, sort/dedupe
440         // numerically. Otherwise sort lexicographically.
441         if value_strs.iter().all(|os| try_parse(&*os).is_some()) {
442             value_strs.sort_unstable_by(|v1, v2| {
443                 let num1 = try_parse(&v1);
444                 let num2 = try_parse(&v2);
445 
446                 num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
447             });
448             value_strs.dedup_by_key(|os| try_parse(&os).unwrap());
449         } else {
450             value_strs.sort_unstable();
451             value_strs.dedup();
452         }
453 
454         for function_id in function_ids {
455             let samples_with_function: Vec<_> = data
456                 .iter()
457                 .by_ref()
458                 .filter(|&&(ref id, _)| id.function_id.as_ref() == Some(&function_id))
459                 .collect();
460 
461             if samples_with_function.len() > 1 {
462                 let subgroup_id =
463                     BenchmarkId::new(group_id.clone(), Some(function_id.clone()), None, None);
464 
465                 self.generate_summary(
466                     &subgroup_id,
467                     &*samples_with_function,
468                     context,
469                     formatter,
470                     false,
471                 );
472             }
473         }
474 
475         for value_str in value_strs {
476             let samples_with_value: Vec<_> = data
477                 .iter()
478                 .by_ref()
479                 .filter(|&&(ref id, _)| id.value_str.as_ref() == Some(&value_str))
480                 .collect();
481 
482             if samples_with_value.len() > 1 {
483                 let subgroup_id =
484                     BenchmarkId::new(group_id.clone(), None, Some(value_str.clone()), None);
485 
486                 self.generate_summary(
487                     &subgroup_id,
488                     &*samples_with_value,
489                     context,
490                     formatter,
491                     false,
492                 );
493             }
494         }
495 
496         let mut all_data = data.iter().by_ref().collect::<Vec<_>>();
497         // First sort the ids/data by value.
498         // If all of the value strings can be parsed into a number, sort/dedupe
499         // numerically. Otherwise sort lexicographically.
500         let all_values_numeric = all_data.iter().all(|(ref id, _)| {
501             id.value_str
502                 .as_ref()
503                 .map(String::as_str)
504                 .and_then(try_parse)
505                 .is_some()
506         });
507         if all_values_numeric {
508             all_data.sort_unstable_by(|(a, _), (b, _)| {
509                 let num1 = a.value_str.as_ref().map(String::as_str).and_then(try_parse);
510                 let num2 = b.value_str.as_ref().map(String::as_str).and_then(try_parse);
511 
512                 num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
513             });
514         } else {
515             all_data.sort_unstable_by_key(|(id, _)| id.value_str.as_ref());
516         }
517         // Next, sort the ids/data by function name. This results in a sorting priority of
518         // function name, then value. This one has to be a stable sort.
519         all_data.sort_by_key(|(id, _)| id.function_id.as_ref());
520 
521         self.generate_summary(
522             &BenchmarkId::new(group_id, None, None, None),
523             &*(all_data),
524             context,
525             formatter,
526             true,
527         );
528         self.plotter.borrow_mut().wait();
529     }
530 
final_summary(&self, report_context: &ReportContext)531     fn final_summary(&self, report_context: &ReportContext) {
532         let output_directory = &report_context.output_directory;
533         if !fs::is_dir(&output_directory) {
534             return;
535         }
536 
537         let mut found_ids = try_else_return!(fs::list_existing_benchmarks(&output_directory));
538         found_ids.sort_unstable_by_key(|id| id.id().to_owned());
539 
540         // Group IDs by group id
541         let mut id_groups: HashMap<&str, Vec<&BenchmarkId>> = HashMap::new();
542         for id in found_ids.iter() {
543             id_groups
544                 .entry(&id.group_id)
545                 .or_insert_with(Vec::new)
546                 .push(id);
547         }
548 
549         let mut groups = id_groups
550             .into_iter()
551             .map(|(_, group)| BenchmarkGroup::new(output_directory, &group))
552             .collect::<Vec<BenchmarkGroup<'_>>>();
553         groups.sort_unstable_by_key(|g| g.group_report.name);
554 
555         try_else_return!(fs::mkdirp(&output_directory.join("report")));
556 
557         let report_path = output_directory.join("report").join("index.html");
558 
559         let context = IndexContext { groups };
560 
561         debug_context(&report_path, &context);
562 
563         let text = self
564             .templates
565             .render("index", &context)
566             .expect("Failed to render index template");
567         try_else_return!(fs::save_string(&text, &report_path,));
568     }
569 }
570 impl Html {
comparison(&self, measurements: &MeasurementData<'_>) -> Option<Comparison>571     fn comparison(&self, measurements: &MeasurementData<'_>) -> Option<Comparison> {
572         if let Some(ref comp) = measurements.comparison {
573             let different_mean = comp.p_value < comp.significance_threshold;
574             let mean_est = &comp.relative_estimates.mean;
575             let explanation_str: String;
576 
577             if !different_mean {
578                 explanation_str = "No change in performance detected.".to_owned();
579             } else {
580                 let comparison = compare_to_threshold(&mean_est, comp.noise_threshold);
581                 match comparison {
582                     ComparisonResult::Improved => {
583                         explanation_str = "Performance has improved.".to_owned();
584                     }
585                     ComparisonResult::Regressed => {
586                         explanation_str = "Performance has regressed.".to_owned();
587                     }
588                     ComparisonResult::NonSignificant => {
589                         explanation_str = "Change within noise threshold.".to_owned();
590                     }
591                 }
592             }
593 
594             let comp = Comparison {
595                 p_value: format!("{:.2}", comp.p_value),
596                 inequality: (if different_mean { "<" } else { ">" }).to_owned(),
597                 significance_level: format!("{:.2}", comp.significance_threshold),
598                 explanation: explanation_str,
599 
600                 change: ConfidenceInterval {
601                     point: format::change(mean_est.point_estimate, true),
602                     lower: format::change(mean_est.confidence_interval.lower_bound, true),
603                     upper: format::change(mean_est.confidence_interval.upper_bound, true),
604                 },
605 
606                 thrpt_change: measurements.throughput.as_ref().map(|_| {
607                     let to_thrpt_estimate = |ratio: f64| 1.0 / (1.0 + ratio) - 1.0;
608                     ConfidenceInterval {
609                         point: format::change(to_thrpt_estimate(mean_est.point_estimate), true),
610                         lower: format::change(
611                             to_thrpt_estimate(mean_est.confidence_interval.lower_bound),
612                             true,
613                         ),
614                         upper: format::change(
615                             to_thrpt_estimate(mean_est.confidence_interval.upper_bound),
616                             true,
617                         ),
618                     }
619                 }),
620 
621                 additional_plots: vec![
622                     Plot::new("Change in mean", "change/mean.svg"),
623                     Plot::new("Change in median", "change/median.svg"),
624                     Plot::new("T-Test", "change/t-test.svg"),
625                 ],
626             };
627             Some(comp)
628         } else {
629             None
630         }
631     }
632 
generate_plots( &self, id: &BenchmarkId, context: &ReportContext, formatter: &dyn ValueFormatter, measurements: &MeasurementData<'_>, )633     fn generate_plots(
634         &self,
635         id: &BenchmarkId,
636         context: &ReportContext,
637         formatter: &dyn ValueFormatter,
638         measurements: &MeasurementData<'_>,
639     ) {
640         let plot_ctx = PlotContext {
641             id,
642             context,
643             size: None,
644             is_thumbnail: false,
645         };
646 
647         let plot_data = PlotData {
648             measurements,
649             formatter,
650             comparison: None,
651         };
652 
653         let plot_ctx_small = plot_ctx.thumbnail(true).size(THUMBNAIL_SIZE);
654 
655         self.plotter.borrow_mut().pdf(plot_ctx, plot_data);
656         self.plotter.borrow_mut().pdf(plot_ctx_small, plot_data);
657         if measurements.absolute_estimates.slope.is_some() {
658             self.plotter.borrow_mut().regression(plot_ctx, plot_data);
659             self.plotter
660                 .borrow_mut()
661                 .regression(plot_ctx_small, plot_data);
662         } else {
663             self.plotter
664                 .borrow_mut()
665                 .iteration_times(plot_ctx, plot_data);
666             self.plotter
667                 .borrow_mut()
668                 .iteration_times(plot_ctx_small, plot_data);
669         }
670 
671         self.plotter
672             .borrow_mut()
673             .abs_distributions(plot_ctx, plot_data);
674 
675         if let Some(ref comp) = measurements.comparison {
676             try_else_return!({
677                 let mut change_dir = context.output_directory.clone();
678                 change_dir.push(id.as_directory_name());
679                 change_dir.push("report");
680                 change_dir.push("change");
681                 fs::mkdirp(&change_dir)
682             });
683 
684             try_else_return!({
685                 let mut both_dir = context.output_directory.clone();
686                 both_dir.push(id.as_directory_name());
687                 both_dir.push("report");
688                 both_dir.push("both");
689                 fs::mkdirp(&both_dir)
690             });
691 
692             let comp_data = plot_data.comparison(&comp);
693 
694             self.plotter.borrow_mut().pdf(plot_ctx, comp_data);
695             self.plotter.borrow_mut().pdf(plot_ctx_small, comp_data);
696             if measurements.absolute_estimates.slope.is_some()
697                 && comp.base_estimates.slope.is_some()
698             {
699                 self.plotter.borrow_mut().regression(plot_ctx, comp_data);
700                 self.plotter
701                     .borrow_mut()
702                     .regression(plot_ctx_small, comp_data);
703             } else {
704                 self.plotter
705                     .borrow_mut()
706                     .iteration_times(plot_ctx, comp_data);
707                 self.plotter
708                     .borrow_mut()
709                     .iteration_times(plot_ctx_small, comp_data);
710             }
711             self.plotter.borrow_mut().t_test(plot_ctx, comp_data);
712             self.plotter
713                 .borrow_mut()
714                 .rel_distributions(plot_ctx, comp_data);
715         }
716 
717         self.plotter.borrow_mut().wait();
718     }
719 
load_summary_data<'a>( &self, output_directory: &Path, all_ids: &[&'a BenchmarkId], ) -> Vec<(&'a BenchmarkId, Vec<f64>)>720     fn load_summary_data<'a>(
721         &self,
722         output_directory: &Path,
723         all_ids: &[&'a BenchmarkId],
724     ) -> Vec<(&'a BenchmarkId, Vec<f64>)> {
725         all_ids
726             .iter()
727             .filter_map(|id| {
728                 let entry = output_directory.join(id.as_directory_name()).join("new");
729 
730                 let SavedSample { iters, times, .. } =
731                     try_else_return!(fs::load(&entry.join("sample.json")), || None);
732                 let avg_times = iters
733                     .into_iter()
734                     .zip(times.into_iter())
735                     .map(|(iters, time)| time / iters)
736                     .collect::<Vec<_>>();
737 
738                 Some((*id, avg_times))
739             })
740             .collect::<Vec<_>>()
741     }
742 
generate_summary( &self, id: &BenchmarkId, data: &[&(&BenchmarkId, Vec<f64>)], report_context: &ReportContext, formatter: &dyn ValueFormatter, full_summary: bool, )743     fn generate_summary(
744         &self,
745         id: &BenchmarkId,
746         data: &[&(&BenchmarkId, Vec<f64>)],
747         report_context: &ReportContext,
748         formatter: &dyn ValueFormatter,
749         full_summary: bool,
750     ) {
751         let plot_ctx = PlotContext {
752             id,
753             context: report_context,
754             size: None,
755             is_thumbnail: false,
756         };
757 
758         try_else_return!(
759             {
760                 let mut report_dir = report_context.output_directory.clone();
761                 report_dir.push(id.as_directory_name());
762                 report_dir.push("report");
763                 fs::mkdirp(&report_dir)
764             },
765             || {}
766         );
767 
768         self.plotter.borrow_mut().violin(plot_ctx, formatter, data);
769 
770         let value_types: Vec<_> = data.iter().map(|&&(ref id, _)| id.value_type()).collect();
771         let mut line_path = None;
772 
773         if value_types.iter().all(|x| x == &value_types[0]) {
774             if let Some(value_type) = value_types[0] {
775                 let values: Vec<_> = data.iter().map(|&&(ref id, _)| id.as_number()).collect();
776                 if values.iter().any(|x| x != &values[0]) {
777                     self.plotter
778                         .borrow_mut()
779                         .line_comparison(plot_ctx, formatter, data, value_type);
780                     line_path = Some(plot_ctx.line_comparison_path());
781                 }
782             }
783         }
784 
785         let path_prefix = if full_summary { "../.." } else { "../../.." };
786         let benchmarks = data
787             .iter()
788             .map(|&&(ref id, _)| {
789                 IndividualBenchmark::from_id(&report_context.output_directory, path_prefix, id)
790             })
791             .collect();
792 
793         let context = SummaryContext {
794             group_id: id.as_title().to_owned(),
795 
796             thumbnail_width: THUMBNAIL_SIZE.unwrap().0,
797             thumbnail_height: THUMBNAIL_SIZE.unwrap().1,
798 
799             violin_plot: Some(plot_ctx.violin_path().to_string_lossy().into_owned()),
800             line_chart: line_path.map(|p| p.to_string_lossy().into_owned()),
801 
802             benchmarks,
803         };
804 
805         let mut report_path = report_context.output_directory.clone();
806         report_path.push(id.as_directory_name());
807         report_path.push("report");
808         report_path.push("index.html");
809         debug_context(&report_path, &context);
810 
811         let text = self
812             .templates
813             .render("summary_report", &context)
814             .expect("Failed to render summary report template");
815         try_else_return!(fs::save_string(&text, &report_path,), || {});
816     }
817 }
818 
819 enum ComparisonResult {
820     Improved,
821     Regressed,
822     NonSignificant,
823 }
824 
compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult825 fn compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult {
826     let ci = &estimate.confidence_interval;
827     let lb = ci.lower_bound;
828     let ub = ci.upper_bound;
829 
830     if lb < -noise && ub < -noise {
831         ComparisonResult::Improved
832     } else if lb > noise && ub > noise {
833         ComparisonResult::Regressed
834     } else {
835         ComparisonResult::NonSignificant
836     }
837 }
838