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_deref();
203             let value = id.value_str.as_deref();
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(|&&(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(|&&(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
501             .iter()
502             .all(|(id, _)| id.value_str.as_deref().and_then(try_parse).is_some());
503         if all_values_numeric {
504             all_data.sort_unstable_by(|(a, _), (b, _)| {
505                 let num1 = a.value_str.as_deref().and_then(try_parse);
506                 let num2 = b.value_str.as_deref().and_then(try_parse);
507 
508                 num1.partial_cmp(&num2).unwrap_or(Ordering::Less)
509             });
510         } else {
511             all_data.sort_unstable_by_key(|(id, _)| id.value_str.as_ref());
512         }
513         // Next, sort the ids/data by function name. This results in a sorting priority of
514         // function name, then value. This one has to be a stable sort.
515         all_data.sort_by_key(|(id, _)| id.function_id.as_ref());
516 
517         self.generate_summary(
518             &BenchmarkId::new(group_id, None, None, None),
519             &*(all_data),
520             context,
521             formatter,
522             true,
523         );
524         self.plotter.borrow_mut().wait();
525     }
526 
final_summary(&self, report_context: &ReportContext)527     fn final_summary(&self, report_context: &ReportContext) {
528         let output_directory = &report_context.output_directory;
529         if !fs::is_dir(&output_directory) {
530             return;
531         }
532 
533         let mut found_ids = try_else_return!(fs::list_existing_benchmarks(&output_directory));
534         found_ids.sort_unstable_by_key(|id| id.id().to_owned());
535 
536         // Group IDs by group id
537         let mut id_groups: HashMap<&str, Vec<&BenchmarkId>> = HashMap::new();
538         for id in found_ids.iter() {
539             id_groups
540                 .entry(&id.group_id)
541                 .or_insert_with(Vec::new)
542                 .push(id);
543         }
544 
545         let mut groups = id_groups
546             .into_iter()
547             .map(|(_, group)| BenchmarkGroup::new(output_directory, &group))
548             .collect::<Vec<BenchmarkGroup<'_>>>();
549         groups.sort_unstable_by_key(|g| g.group_report.name);
550 
551         try_else_return!(fs::mkdirp(&output_directory.join("report")));
552 
553         let report_path = output_directory.join("report").join("index.html");
554 
555         let context = IndexContext { groups };
556 
557         debug_context(&report_path, &context);
558 
559         let text = self
560             .templates
561             .render("index", &context)
562             .expect("Failed to render index template");
563         try_else_return!(fs::save_string(&text, &report_path,));
564     }
565 }
566 impl Html {
comparison(&self, measurements: &MeasurementData<'_>) -> Option<Comparison>567     fn comparison(&self, measurements: &MeasurementData<'_>) -> Option<Comparison> {
568         if let Some(ref comp) = measurements.comparison {
569             let different_mean = comp.p_value < comp.significance_threshold;
570             let mean_est = &comp.relative_estimates.mean;
571             let explanation_str: String;
572 
573             if !different_mean {
574                 explanation_str = "No change in performance detected.".to_owned();
575             } else {
576                 let comparison = compare_to_threshold(mean_est, comp.noise_threshold);
577                 match comparison {
578                     ComparisonResult::Improved => {
579                         explanation_str = "Performance has improved.".to_owned();
580                     }
581                     ComparisonResult::Regressed => {
582                         explanation_str = "Performance has regressed.".to_owned();
583                     }
584                     ComparisonResult::NonSignificant => {
585                         explanation_str = "Change within noise threshold.".to_owned();
586                     }
587                 }
588             }
589 
590             let comp = Comparison {
591                 p_value: format!("{:.2}", comp.p_value),
592                 inequality: (if different_mean { "<" } else { ">" }).to_owned(),
593                 significance_level: format!("{:.2}", comp.significance_threshold),
594                 explanation: explanation_str,
595 
596                 change: ConfidenceInterval {
597                     point: format::change(mean_est.point_estimate, true),
598                     lower: format::change(mean_est.confidence_interval.lower_bound, true),
599                     upper: format::change(mean_est.confidence_interval.upper_bound, true),
600                 },
601 
602                 thrpt_change: measurements.throughput.as_ref().map(|_| {
603                     let to_thrpt_estimate = |ratio: f64| 1.0 / (1.0 + ratio) - 1.0;
604                     ConfidenceInterval {
605                         point: format::change(to_thrpt_estimate(mean_est.point_estimate), true),
606                         lower: format::change(
607                             to_thrpt_estimate(mean_est.confidence_interval.lower_bound),
608                             true,
609                         ),
610                         upper: format::change(
611                             to_thrpt_estimate(mean_est.confidence_interval.upper_bound),
612                             true,
613                         ),
614                     }
615                 }),
616 
617                 additional_plots: vec![
618                     Plot::new("Change in mean", "change/mean.svg"),
619                     Plot::new("Change in median", "change/median.svg"),
620                     Plot::new("T-Test", "change/t-test.svg"),
621                 ],
622             };
623             Some(comp)
624         } else {
625             None
626         }
627     }
628 
generate_plots( &self, id: &BenchmarkId, context: &ReportContext, formatter: &dyn ValueFormatter, measurements: &MeasurementData<'_>, )629     fn generate_plots(
630         &self,
631         id: &BenchmarkId,
632         context: &ReportContext,
633         formatter: &dyn ValueFormatter,
634         measurements: &MeasurementData<'_>,
635     ) {
636         let plot_ctx = PlotContext {
637             id,
638             context,
639             size: None,
640             is_thumbnail: false,
641         };
642 
643         let plot_data = PlotData {
644             measurements,
645             formatter,
646             comparison: None,
647         };
648 
649         let plot_ctx_small = plot_ctx.thumbnail(true).size(THUMBNAIL_SIZE);
650 
651         self.plotter.borrow_mut().pdf(plot_ctx, plot_data);
652         self.plotter.borrow_mut().pdf(plot_ctx_small, plot_data);
653         if measurements.absolute_estimates.slope.is_some() {
654             self.plotter.borrow_mut().regression(plot_ctx, plot_data);
655             self.plotter
656                 .borrow_mut()
657                 .regression(plot_ctx_small, plot_data);
658         } else {
659             self.plotter
660                 .borrow_mut()
661                 .iteration_times(plot_ctx, plot_data);
662             self.plotter
663                 .borrow_mut()
664                 .iteration_times(plot_ctx_small, plot_data);
665         }
666 
667         self.plotter
668             .borrow_mut()
669             .abs_distributions(plot_ctx, plot_data);
670 
671         if let Some(ref comp) = measurements.comparison {
672             try_else_return!({
673                 let mut change_dir = context.output_directory.clone();
674                 change_dir.push(id.as_directory_name());
675                 change_dir.push("report");
676                 change_dir.push("change");
677                 fs::mkdirp(&change_dir)
678             });
679 
680             try_else_return!({
681                 let mut both_dir = context.output_directory.clone();
682                 both_dir.push(id.as_directory_name());
683                 both_dir.push("report");
684                 both_dir.push("both");
685                 fs::mkdirp(&both_dir)
686             });
687 
688             let comp_data = plot_data.comparison(comp);
689 
690             self.plotter.borrow_mut().pdf(plot_ctx, comp_data);
691             self.plotter.borrow_mut().pdf(plot_ctx_small, comp_data);
692             if measurements.absolute_estimates.slope.is_some()
693                 && comp.base_estimates.slope.is_some()
694             {
695                 self.plotter.borrow_mut().regression(plot_ctx, comp_data);
696                 self.plotter
697                     .borrow_mut()
698                     .regression(plot_ctx_small, comp_data);
699             } else {
700                 self.plotter
701                     .borrow_mut()
702                     .iteration_times(plot_ctx, comp_data);
703                 self.plotter
704                     .borrow_mut()
705                     .iteration_times(plot_ctx_small, comp_data);
706             }
707             self.plotter.borrow_mut().t_test(plot_ctx, comp_data);
708             self.plotter
709                 .borrow_mut()
710                 .rel_distributions(plot_ctx, comp_data);
711         }
712 
713         self.plotter.borrow_mut().wait();
714     }
715 
load_summary_data<'a>( &self, output_directory: &Path, all_ids: &[&'a BenchmarkId], ) -> Vec<(&'a BenchmarkId, Vec<f64>)>716     fn load_summary_data<'a>(
717         &self,
718         output_directory: &Path,
719         all_ids: &[&'a BenchmarkId],
720     ) -> Vec<(&'a BenchmarkId, Vec<f64>)> {
721         all_ids
722             .iter()
723             .filter_map(|id| {
724                 let entry = output_directory.join(id.as_directory_name()).join("new");
725 
726                 let SavedSample { iters, times, .. } =
727                     try_else_return!(fs::load(&entry.join("sample.json")), || None);
728                 let avg_times = iters
729                     .into_iter()
730                     .zip(times.into_iter())
731                     .map(|(iters, time)| time / iters)
732                     .collect::<Vec<_>>();
733 
734                 Some((*id, avg_times))
735             })
736             .collect::<Vec<_>>()
737     }
738 
generate_summary( &self, id: &BenchmarkId, data: &[&(&BenchmarkId, Vec<f64>)], report_context: &ReportContext, formatter: &dyn ValueFormatter, full_summary: bool, )739     fn generate_summary(
740         &self,
741         id: &BenchmarkId,
742         data: &[&(&BenchmarkId, Vec<f64>)],
743         report_context: &ReportContext,
744         formatter: &dyn ValueFormatter,
745         full_summary: bool,
746     ) {
747         let plot_ctx = PlotContext {
748             id,
749             context: report_context,
750             size: None,
751             is_thumbnail: false,
752         };
753 
754         try_else_return!(
755             {
756                 let mut report_dir = report_context.output_directory.clone();
757                 report_dir.push(id.as_directory_name());
758                 report_dir.push("report");
759                 fs::mkdirp(&report_dir)
760             },
761             || {}
762         );
763 
764         self.plotter.borrow_mut().violin(plot_ctx, formatter, data);
765 
766         let value_types: Vec<_> = data.iter().map(|&&(id, _)| id.value_type()).collect();
767         let mut line_path = None;
768 
769         if value_types.iter().all(|x| x == &value_types[0]) {
770             if let Some(value_type) = value_types[0] {
771                 let values: Vec<_> = data.iter().map(|&&(id, _)| id.as_number()).collect();
772                 if values.iter().any(|x| x != &values[0]) {
773                     self.plotter
774                         .borrow_mut()
775                         .line_comparison(plot_ctx, formatter, data, value_type);
776                     line_path = Some(plot_ctx.line_comparison_path());
777                 }
778             }
779         }
780 
781         let path_prefix = if full_summary { "../.." } else { "../../.." };
782         let benchmarks = data
783             .iter()
784             .map(|&&(id, _)| {
785                 IndividualBenchmark::from_id(&report_context.output_directory, path_prefix, id)
786             })
787             .collect();
788 
789         let context = SummaryContext {
790             group_id: id.as_title().to_owned(),
791 
792             thumbnail_width: THUMBNAIL_SIZE.unwrap().0,
793             thumbnail_height: THUMBNAIL_SIZE.unwrap().1,
794 
795             violin_plot: Some(plot_ctx.violin_path().to_string_lossy().into_owned()),
796             line_chart: line_path.map(|p| p.to_string_lossy().into_owned()),
797 
798             benchmarks,
799         };
800 
801         let mut report_path = report_context.output_directory.clone();
802         report_path.push(id.as_directory_name());
803         report_path.push("report");
804         report_path.push("index.html");
805         debug_context(&report_path, &context);
806 
807         let text = self
808             .templates
809             .render("summary_report", &context)
810             .expect("Failed to render summary report template");
811         try_else_return!(fs::save_string(&text, &report_path,), || {});
812     }
813 }
814 
815 enum ComparisonResult {
816     Improved,
817     Regressed,
818     NonSignificant,
819 }
820 
compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult821 fn compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult {
822     let ci = &estimate.confidence_interval;
823     let lb = ci.lower_bound;
824     let ub = ci.upper_bound;
825 
826     if lb < -noise && ub < -noise {
827         ComparisonResult::Improved
828     } else if lb > noise && ub > noise {
829         ComparisonResult::Regressed
830     } else {
831         ComparisonResult::NonSignificant
832     }
833 }
834