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