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