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