Coverage Report

Created: 2025-10-29 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}