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