Coverage Report

Created: 2026-04-14 06:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/criterion-0.5.1/src/report.rs
Line
Count
Source
1
#[cfg(feature = "csv_output")]
2
use crate::csv_report::FileCsvReport;
3
use crate::stats::bivariate::regression::Slope;
4
use crate::stats::univariate::outliers::tukey::LabeledSample;
5
use crate::{html::Html, stats::bivariate::Data};
6
7
use crate::estimate::{ChangeDistributions, ChangeEstimates, Distributions, Estimate, Estimates};
8
use crate::format;
9
use crate::measurement::ValueFormatter;
10
use crate::stats::univariate::Sample;
11
use crate::stats::Distribution;
12
use crate::{PlotConfiguration, Throughput};
13
use anes::{Attribute, ClearLine, Color, ResetAttributes, SetAttribute, SetForegroundColor};
14
use std::cmp;
15
use std::collections::HashSet;
16
use std::fmt;
17
use std::io::stderr;
18
use std::io::Write;
19
use std::path::{Path, PathBuf};
20
21
const MAX_DIRECTORY_NAME_LEN: usize = 64;
22
const MAX_TITLE_LEN: usize = 100;
23
24
pub(crate) struct ComparisonData {
25
    pub p_value: f64,
26
    pub t_distribution: Distribution<f64>,
27
    pub t_value: f64,
28
    pub relative_estimates: ChangeEstimates,
29
    pub relative_distributions: ChangeDistributions,
30
    pub significance_threshold: f64,
31
    pub noise_threshold: f64,
32
    pub base_iter_counts: Vec<f64>,
33
    pub base_sample_times: Vec<f64>,
34
    pub base_avg_times: Vec<f64>,
35
    pub base_estimates: Estimates,
36
}
37
38
pub(crate) struct MeasurementData<'a> {
39
    pub data: Data<'a, f64, f64>,
40
    pub avg_times: LabeledSample<'a, f64>,
41
    pub absolute_estimates: Estimates,
42
    pub distributions: Distributions,
43
    pub comparison: Option<ComparisonData>,
44
    pub throughput: Option<Throughput>,
45
}
46
impl<'a> MeasurementData<'a> {
47
0
    pub fn iter_counts(&self) -> &Sample<f64> {
48
0
        self.data.x()
49
0
    }
50
51
    #[cfg(feature = "csv_output")]
52
    pub fn sample_times(&self) -> &Sample<f64> {
53
        self.data.y()
54
    }
55
}
56
57
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
58
pub enum ValueType {
59
    Bytes,
60
    Elements,
61
    Value,
62
}
63
64
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
65
pub struct BenchmarkId {
66
    pub group_id: String,
67
    pub function_id: Option<String>,
68
    pub value_str: Option<String>,
69
    pub throughput: Option<Throughput>,
70
    full_id: String,
71
    directory_name: String,
72
    title: String,
73
}
74
75
0
fn truncate_to_character_boundary(s: &mut String, max_len: usize) {
76
0
    let mut boundary = cmp::min(max_len, s.len());
77
0
    while !s.is_char_boundary(boundary) {
78
0
        boundary -= 1;
79
0
    }
80
0
    s.truncate(boundary);
81
0
}
82
83
0
pub fn make_filename_safe(string: &str) -> String {
84
0
    let mut string = string.replace(
85
0
        &['?', '"', '/', '\\', '*', '<', '>', ':', '|', '^'][..],
86
0
        "_",
87
    );
88
89
    // Truncate to last character boundary before max length...
90
0
    truncate_to_character_boundary(&mut string, MAX_DIRECTORY_NAME_LEN);
91
92
0
    if cfg!(target_os = "windows") {
93
0
        {
94
0
            string = string
95
0
                // On Windows, spaces in the end of the filename are ignored and will be trimmed.
96
0
                //
97
0
                // Without trimming ourselves, creating a directory `dir ` will silently create
98
0
                // `dir` instead, but then operations on files like `dir /file` will fail.
99
0
                //
100
0
                // Also note that it's important to do this *after* trimming to MAX_DIRECTORY_NAME_LEN,
101
0
                // otherwise it can trim again to a name with a trailing space.
102
0
                .trim_end()
103
0
                // On Windows, file names are not case-sensitive, so lowercase everything.
104
0
                .to_lowercase();
105
0
        }
106
0
    }
107
108
0
    string
109
0
}
110
111
impl BenchmarkId {
112
0
    pub fn new(
113
0
        group_id: String,
114
0
        function_id: Option<String>,
115
0
        value_str: Option<String>,
116
0
        throughput: Option<Throughput>,
117
0
    ) -> BenchmarkId {
118
0
        let full_id = match (&function_id, &value_str) {
119
0
            (Some(func), Some(val)) => format!("{}/{}/{}", group_id, func, val),
120
0
            (Some(func), &None) => format!("{}/{}", group_id, func),
121
0
            (&None, Some(val)) => format!("{}/{}", group_id, val),
122
0
            (&None, &None) => group_id.clone(),
123
        };
124
125
0
        let mut title = full_id.clone();
126
0
        truncate_to_character_boundary(&mut title, MAX_TITLE_LEN);
127
0
        if title != full_id {
128
0
            title.push_str("...");
129
0
        }
130
131
0
        let directory_name = match (&function_id, &value_str) {
132
0
            (Some(func), Some(val)) => format!(
133
0
                "{}/{}/{}",
134
0
                make_filename_safe(&group_id),
135
0
                make_filename_safe(func),
136
0
                make_filename_safe(val)
137
            ),
138
0
            (Some(func), &None) => format!(
139
0
                "{}/{}",
140
0
                make_filename_safe(&group_id),
141
0
                make_filename_safe(func)
142
            ),
143
0
            (&None, Some(val)) => format!(
144
0
                "{}/{}",
145
0
                make_filename_safe(&group_id),
146
0
                make_filename_safe(val)
147
            ),
148
0
            (&None, &None) => make_filename_safe(&group_id),
149
        };
150
151
0
        BenchmarkId {
152
0
            group_id,
153
0
            function_id,
154
0
            value_str,
155
0
            throughput,
156
0
            full_id,
157
0
            directory_name,
158
0
            title,
159
0
        }
160
0
    }
161
162
0
    pub fn id(&self) -> &str {
163
0
        &self.full_id
164
0
    }
165
166
0
    pub fn as_title(&self) -> &str {
167
0
        &self.title
168
0
    }
169
170
0
    pub fn as_directory_name(&self) -> &str {
171
0
        &self.directory_name
172
0
    }
173
174
0
    pub fn as_number(&self) -> Option<f64> {
175
0
        match self.throughput {
176
0
            Some(Throughput::Bytes(n))
177
0
            | Some(Throughput::Elements(n))
178
0
            | Some(Throughput::BytesDecimal(n)) => Some(n as f64),
179
0
            None => self
180
0
                .value_str
181
0
                .as_ref()
182
0
                .and_then(|string| string.parse::<f64>().ok()),
183
        }
184
0
    }
185
186
0
    pub fn value_type(&self) -> Option<ValueType> {
187
0
        match self.throughput {
188
0
            Some(Throughput::Bytes(_)) => Some(ValueType::Bytes),
189
0
            Some(Throughput::BytesDecimal(_)) => Some(ValueType::Bytes),
190
0
            Some(Throughput::Elements(_)) => Some(ValueType::Elements),
191
0
            None => self
192
0
                .value_str
193
0
                .as_ref()
194
0
                .and_then(|string| string.parse::<f64>().ok())
195
0
                .map(|_| ValueType::Value),
196
        }
197
0
    }
198
199
0
    pub fn ensure_directory_name_unique(&mut self, existing_directories: &HashSet<String>) {
200
0
        if !existing_directories.contains(self.as_directory_name()) {
201
0
            return;
202
0
        }
203
204
0
        let mut counter = 2;
205
        loop {
206
0
            let new_dir_name = format!("{}_{}", self.as_directory_name(), counter);
207
0
            if !existing_directories.contains(&new_dir_name) {
208
0
                self.directory_name = new_dir_name;
209
0
                return;
210
0
            }
211
0
            counter += 1;
212
        }
213
0
    }
214
215
0
    pub fn ensure_title_unique(&mut self, existing_titles: &HashSet<String>) {
216
0
        if !existing_titles.contains(self.as_title()) {
217
0
            return;
218
0
        }
219
220
0
        let mut counter = 2;
221
        loop {
222
0
            let new_title = format!("{} #{}", self.as_title(), counter);
223
0
            if !existing_titles.contains(&new_title) {
224
0
                self.title = new_title;
225
0
                return;
226
0
            }
227
0
            counter += 1;
228
        }
229
0
    }
230
}
231
impl fmt::Display for BenchmarkId {
232
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233
0
        f.write_str(self.as_title())
234
0
    }
235
}
236
impl fmt::Debug for BenchmarkId {
237
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238
0
        fn format_opt(opt: &Option<String>) -> String {
239
0
            match *opt {
240
0
                Some(ref string) => format!("\"{}\"", string),
241
0
                None => "None".to_owned(),
242
            }
243
0
        }
244
245
0
        write!(
246
0
            f,
247
0
            "BenchmarkId {{ group_id: \"{}\", function_id: {}, value_str: {}, throughput: {:?} }}",
248
            self.group_id,
249
0
            format_opt(&self.function_id),
250
0
            format_opt(&self.value_str),
251
            self.throughput,
252
        )
253
0
    }
254
}
255
256
pub struct ReportContext {
257
    pub output_directory: PathBuf,
258
    pub plot_config: PlotConfiguration,
259
}
260
impl ReportContext {
261
0
    pub fn report_path<P: AsRef<Path> + ?Sized>(&self, id: &BenchmarkId, file_name: &P) -> PathBuf {
262
0
        let mut path = self.output_directory.clone();
263
0
        path.push(id.as_directory_name());
264
0
        path.push("report");
265
0
        path.push(file_name);
266
0
        path
267
0
    }
Unexecuted instantiation: <criterion::report::ReportContext>::report_path::<alloc::string::String>
Unexecuted instantiation: <criterion::report::ReportContext>::report_path::<str>
268
}
269
270
pub(crate) trait Report {
271
0
    fn test_start(&self, _id: &BenchmarkId, _context: &ReportContext) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::test_start
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::test_start
272
0
    fn test_pass(&self, _id: &BenchmarkId, _context: &ReportContext) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::test_pass
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::test_pass
273
274
0
    fn benchmark_start(&self, _id: &BenchmarkId, _context: &ReportContext) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::benchmark_start
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::benchmark_start
275
0
    fn profile(&self, _id: &BenchmarkId, _context: &ReportContext, _profile_ns: f64) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::profile
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::profile
276
0
    fn warmup(&self, _id: &BenchmarkId, _context: &ReportContext, _warmup_ns: f64) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::warmup
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::warmup
277
0
    fn terminated(&self, _id: &BenchmarkId, _context: &ReportContext) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::terminated
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::terminated
278
0
    fn analysis(&self, _id: &BenchmarkId, _context: &ReportContext) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::analysis
Unexecuted instantiation: <criterion::html::Html as criterion::report::Report>::analysis
279
0
    fn measurement_start(
280
0
        &self,
281
0
        _id: &BenchmarkId,
282
0
        _context: &ReportContext,
283
0
        _sample_count: u64,
284
0
        _estimate_ns: f64,
285
0
        _iter_count: u64,
286
0
    ) {
287
0
    }
288
0
    fn measurement_complete(
289
0
        &self,
290
0
        _id: &BenchmarkId,
291
0
        _context: &ReportContext,
292
0
        _measurements: &MeasurementData<'_>,
293
0
        _formatter: &dyn ValueFormatter,
294
0
    ) {
295
0
    }
296
0
    fn summarize(
297
0
        &self,
298
0
        _context: &ReportContext,
299
0
        _all_ids: &[BenchmarkId],
300
0
        _formatter: &dyn ValueFormatter,
301
0
    ) {
302
0
    }
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::summarize
Unexecuted instantiation: <criterion::report::CliReport as criterion::report::Report>::summarize
303
0
    fn final_summary(&self, _context: &ReportContext) {}
Unexecuted instantiation: <criterion::report::BencherReport as criterion::report::Report>::final_summary
Unexecuted instantiation: <criterion::report::CliReport as criterion::report::Report>::final_summary
304
0
    fn group_separator(&self) {}
305
}
306
307
pub(crate) struct Reports {
308
    pub(crate) cli_enabled: bool,
309
    pub(crate) cli: CliReport,
310
    pub(crate) bencher_enabled: bool,
311
    pub(crate) bencher: BencherReport,
312
    pub(crate) csv_enabled: bool,
313
    pub(crate) html: Option<Html>,
314
}
315
macro_rules! reports_impl {
316
    (fn $name:ident(&self, $($argn:ident: $argt:ty),*)) => {
317
0
        fn $name(&self, $($argn: $argt),* ) {
318
0
            if self.cli_enabled {
319
0
                self.cli.$name($($argn),*);
320
0
            }
321
0
            if self.bencher_enabled {
322
0
                self.bencher.$name($($argn),*);
323
0
            }
324
            #[cfg(feature = "csv_output")]
325
            if self.csv_enabled {
326
                FileCsvReport.$name($($argn),*);
327
            }
328
0
            if let Some(reporter) = &self.html {
329
0
                reporter.$name($($argn),*);
330
0
            }
331
0
        }
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::terminated
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::test_start
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::final_summary
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::benchmark_start
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::group_separator
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::measurement_start
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::measurement_complete
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::warmup
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::profile
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::analysis
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::summarize
Unexecuted instantiation: <criterion::report::Reports as criterion::report::Report>::test_pass
332
    };
333
}
334
335
impl Report for Reports {
336
    reports_impl!(fn test_start(&self, id: &BenchmarkId, context: &ReportContext));
337
    reports_impl!(fn test_pass(&self, id: &BenchmarkId, context: &ReportContext));
338
    reports_impl!(fn benchmark_start(&self, id: &BenchmarkId, context: &ReportContext));
339
    reports_impl!(fn profile(&self, id: &BenchmarkId, context: &ReportContext, profile_ns: f64));
340
    reports_impl!(fn warmup(&self, id: &BenchmarkId, context: &ReportContext, warmup_ns: f64));
341
    reports_impl!(fn terminated(&self, id: &BenchmarkId, context: &ReportContext));
342
    reports_impl!(fn analysis(&self, id: &BenchmarkId, context: &ReportContext));
343
    reports_impl!(fn measurement_start(
344
        &self,
345
        id: &BenchmarkId,
346
        context: &ReportContext,
347
        sample_count: u64,
348
        estimate_ns: f64,
349
        iter_count: u64
350
    ));
351
    reports_impl!(
352
    fn measurement_complete(
353
        &self,
354
        id: &BenchmarkId,
355
        context: &ReportContext,
356
        measurements: &MeasurementData<'_>,
357
        formatter: &dyn ValueFormatter
358
    ));
359
    reports_impl!(
360
    fn summarize(
361
        &self,
362
        context: &ReportContext,
363
        all_ids: &[BenchmarkId],
364
        formatter: &dyn ValueFormatter
365
    ));
366
367
    reports_impl!(fn final_summary(&self, context: &ReportContext));
368
    reports_impl!(fn group_separator(&self, ));
369
}
370
371
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
372
pub(crate) enum CliVerbosity {
373
    Quiet,
374
    Normal,
375
    Verbose,
376
}
377
378
pub(crate) struct CliReport {
379
    pub enable_text_overwrite: bool,
380
    pub enable_text_coloring: bool,
381
    pub verbosity: CliVerbosity,
382
}
383
impl CliReport {
384
0
    pub fn new(
385
0
        enable_text_overwrite: bool,
386
0
        enable_text_coloring: bool,
387
0
        verbosity: CliVerbosity,
388
0
    ) -> CliReport {
389
0
        CliReport {
390
0
            enable_text_overwrite,
391
0
            enable_text_coloring,
392
0
            verbosity,
393
0
        }
394
0
    }
395
396
0
    fn text_overwrite(&self) {
397
0
        if self.enable_text_overwrite {
398
0
            eprint!("\r{}", ClearLine::All)
399
0
        }
400
0
    }
401
402
    // Passing a String is the common case here.
403
    #[cfg_attr(feature = "cargo-clippy", allow(clippy::needless_pass_by_value))]
404
0
    fn print_overwritable(&self, s: String) {
405
0
        if self.enable_text_overwrite {
406
0
            eprint!("{}", s);
407
0
            stderr().flush().unwrap();
408
0
        } else {
409
0
            eprintln!("{}", s);
410
0
        }
411
0
    }
412
413
0
    fn with_color(&self, color: Color, s: &str) -> String {
414
0
        if self.enable_text_coloring {
415
0
            format!("{}{}{}", SetForegroundColor(color), s, ResetAttributes)
416
        } else {
417
0
            String::from(s)
418
        }
419
0
    }
420
421
0
    fn green(&self, s: &str) -> String {
422
0
        self.with_color(Color::DarkGreen, s)
423
0
    }
424
425
0
    fn yellow(&self, s: &str) -> String {
426
0
        self.with_color(Color::DarkYellow, s)
427
0
    }
428
429
0
    fn red(&self, s: &str) -> String {
430
0
        self.with_color(Color::DarkRed, s)
431
0
    }
432
433
0
    fn bold(&self, s: String) -> String {
434
0
        if self.enable_text_coloring {
435
0
            format!("{}{}{}", SetAttribute(Attribute::Bold), s, ResetAttributes)
436
        } else {
437
0
            s
438
        }
439
0
    }
440
441
0
    fn faint(&self, s: String) -> String {
442
0
        if self.enable_text_coloring {
443
0
            format!("{}{}{}", SetAttribute(Attribute::Faint), s, ResetAttributes)
444
        } else {
445
0
            s
446
        }
447
0
    }
448
449
0
    pub fn outliers(&self, sample: &LabeledSample<'_, f64>) {
450
0
        let (los, lom, _, him, his) = sample.count();
451
0
        let noutliers = los + lom + him + his;
452
0
        let sample_size = sample.len();
453
454
0
        if noutliers == 0 {
455
0
            return;
456
0
        }
457
458
0
        let percent = |n: usize| 100. * n as f64 / sample_size as f64;
459
460
0
        println!(
461
0
            "{}",
462
0
            self.yellow(&format!(
463
0
                "Found {} outliers among {} measurements ({:.2}%)",
464
0
                noutliers,
465
0
                sample_size,
466
0
                percent(noutliers)
467
0
            ))
468
        );
469
470
0
        let print = |n, label| {
471
0
            if n != 0 {
472
0
                println!("  {} ({:.2}%) {}", n, percent(n), label);
473
0
            }
474
0
        };
475
476
0
        print(los, "low severe");
477
0
        print(lom, "low mild");
478
0
        print(him, "high mild");
479
0
        print(his, "high severe");
480
0
    }
481
}
482
impl Report for CliReport {
483
0
    fn test_start(&self, id: &BenchmarkId, _: &ReportContext) {
484
0
        println!("Testing {}", id);
485
0
    }
486
0
    fn test_pass(&self, _: &BenchmarkId, _: &ReportContext) {
487
0
        println!("Success");
488
0
    }
489
490
0
    fn benchmark_start(&self, id: &BenchmarkId, _: &ReportContext) {
491
0
        self.print_overwritable(format!("Benchmarking {}", id));
492
0
    }
493
494
0
    fn profile(&self, id: &BenchmarkId, _: &ReportContext, warmup_ns: f64) {
495
0
        self.text_overwrite();
496
0
        self.print_overwritable(format!(
497
0
            "Benchmarking {}: Profiling for {}",
498
            id,
499
0
            format::time(warmup_ns)
500
        ));
501
0
    }
502
503
0
    fn warmup(&self, id: &BenchmarkId, _: &ReportContext, warmup_ns: f64) {
504
0
        self.text_overwrite();
505
0
        self.print_overwritable(format!(
506
0
            "Benchmarking {}: Warming up for {}",
507
            id,
508
0
            format::time(warmup_ns)
509
        ));
510
0
    }
511
512
0
    fn terminated(&self, id: &BenchmarkId, _: &ReportContext) {
513
0
        self.text_overwrite();
514
0
        println!("Benchmarking {}: Complete (Analysis Disabled)", id);
515
0
    }
516
517
0
    fn analysis(&self, id: &BenchmarkId, _: &ReportContext) {
518
0
        self.text_overwrite();
519
0
        self.print_overwritable(format!("Benchmarking {}: Analyzing", id));
520
0
    }
521
522
0
    fn measurement_start(
523
0
        &self,
524
0
        id: &BenchmarkId,
525
0
        _: &ReportContext,
526
0
        sample_count: u64,
527
0
        estimate_ns: f64,
528
0
        iter_count: u64,
529
0
    ) {
530
0
        self.text_overwrite();
531
0
        let iter_string = if matches!(self.verbosity, CliVerbosity::Verbose) {
532
0
            format!("{} iterations", iter_count)
533
        } else {
534
0
            format::iter_count(iter_count)
535
        };
536
537
0
        self.print_overwritable(format!(
538
0
            "Benchmarking {}: Collecting {} samples in estimated {} ({})",
539
            id,
540
            sample_count,
541
0
            format::time(estimate_ns),
542
            iter_string
543
        ));
544
0
    }
545
546
0
    fn measurement_complete(
547
0
        &self,
548
0
        id: &BenchmarkId,
549
0
        _: &ReportContext,
550
0
        meas: &MeasurementData<'_>,
551
0
        formatter: &dyn ValueFormatter,
552
0
    ) {
553
0
        self.text_overwrite();
554
555
0
        let typical_estimate = &meas.absolute_estimates.typical();
556
557
        {
558
0
            let mut id = id.as_title().to_owned();
559
560
0
            if id.len() > 23 {
561
0
                println!("{}", self.green(&id));
562
0
                id.clear();
563
0
            }
564
0
            let id_len = id.len();
565
566
0
            println!(
567
0
                "{}{}time:   [{} {} {}]",
568
0
                self.green(&id),
569
0
                " ".repeat(24 - id_len),
570
0
                self.faint(
571
0
                    formatter.format_value(typical_estimate.confidence_interval.lower_bound)
572
                ),
573
0
                self.bold(formatter.format_value(typical_estimate.point_estimate)),
574
0
                self.faint(
575
0
                    formatter.format_value(typical_estimate.confidence_interval.upper_bound)
576
                )
577
            );
578
        }
579
580
0
        if let Some(ref throughput) = meas.throughput {
581
0
            println!(
582
0
                "{}thrpt:  [{} {} {}]",
583
0
                " ".repeat(24),
584
0
                self.faint(formatter.format_throughput(
585
0
                    throughput,
586
0
                    typical_estimate.confidence_interval.upper_bound
587
                )),
588
0
                self.bold(formatter.format_throughput(throughput, typical_estimate.point_estimate)),
589
0
                self.faint(formatter.format_throughput(
590
0
                    throughput,
591
0
                    typical_estimate.confidence_interval.lower_bound
592
                )),
593
            )
594
0
        }
595
596
0
        if !matches!(self.verbosity, CliVerbosity::Quiet) {
597
0
            if let Some(ref comp) = meas.comparison {
598
0
                let different_mean = comp.p_value < comp.significance_threshold;
599
0
                let mean_est = &comp.relative_estimates.mean;
600
0
                let point_estimate = mean_est.point_estimate;
601
0
                let mut point_estimate_str = format::change(point_estimate, true);
602
                // The change in throughput is related to the change in timing. Reducing the timing by
603
                // 50% increases the throughput by 100%.
604
0
                let to_thrpt_estimate = |ratio: f64| 1.0 / (1.0 + ratio) - 1.0;
605
0
                let mut thrpt_point_estimate_str =
606
0
                    format::change(to_thrpt_estimate(point_estimate), true);
607
                let explanation_str: String;
608
609
0
                if !different_mean {
610
0
                    explanation_str = "No change in performance detected.".to_owned();
611
0
                } else {
612
0
                    let comparison = compare_to_threshold(mean_est, comp.noise_threshold);
613
0
                    match comparison {
614
0
                        ComparisonResult::Improved => {
615
0
                            point_estimate_str = self.green(&self.bold(point_estimate_str));
616
0
                            thrpt_point_estimate_str =
617
0
                                self.green(&self.bold(thrpt_point_estimate_str));
618
0
                            explanation_str =
619
0
                                format!("Performance has {}.", self.green("improved"));
620
0
                        }
621
0
                        ComparisonResult::Regressed => {
622
0
                            point_estimate_str = self.red(&self.bold(point_estimate_str));
623
0
                            thrpt_point_estimate_str =
624
0
                                self.red(&self.bold(thrpt_point_estimate_str));
625
0
                            explanation_str = format!("Performance has {}.", self.red("regressed"));
626
0
                        }
627
0
                        ComparisonResult::NonSignificant => {
628
0
                            explanation_str = "Change within noise threshold.".to_owned();
629
0
                        }
630
                    }
631
                }
632
633
0
                if meas.throughput.is_some() {
634
0
                    println!("{}change:", " ".repeat(17));
635
636
0
                    println!(
637
0
                        "{}time:   [{} {} {}] (p = {:.2} {} {:.2})",
638
0
                        " ".repeat(24),
639
0
                        self.faint(format::change(
640
0
                            mean_est.confidence_interval.lower_bound,
641
                            true
642
                        )),
643
                        point_estimate_str,
644
0
                        self.faint(format::change(
645
0
                            mean_est.confidence_interval.upper_bound,
646
                            true
647
                        )),
648
                        comp.p_value,
649
0
                        if different_mean { "<" } else { ">" },
650
                        comp.significance_threshold
651
                    );
652
0
                    println!(
653
0
                        "{}thrpt:  [{} {} {}]",
654
0
                        " ".repeat(24),
655
0
                        self.faint(format::change(
656
0
                            to_thrpt_estimate(mean_est.confidence_interval.upper_bound),
657
                            true
658
                        )),
659
                        thrpt_point_estimate_str,
660
0
                        self.faint(format::change(
661
0
                            to_thrpt_estimate(mean_est.confidence_interval.lower_bound),
662
                            true
663
                        )),
664
                    );
665
                } else {
666
0
                    println!(
667
0
                        "{}change: [{} {} {}] (p = {:.2} {} {:.2})",
668
0
                        " ".repeat(24),
669
0
                        self.faint(format::change(
670
0
                            mean_est.confidence_interval.lower_bound,
671
                            true
672
                        )),
673
                        point_estimate_str,
674
0
                        self.faint(format::change(
675
0
                            mean_est.confidence_interval.upper_bound,
676
                            true
677
                        )),
678
                        comp.p_value,
679
0
                        if different_mean { "<" } else { ">" },
680
                        comp.significance_threshold
681
                    );
682
                }
683
684
0
                println!("{}{}", " ".repeat(24), explanation_str);
685
0
            }
686
0
        }
687
688
0
        if !matches!(self.verbosity, CliVerbosity::Quiet) {
689
0
            self.outliers(&meas.avg_times);
690
0
        }
691
692
0
        if matches!(self.verbosity, CliVerbosity::Verbose) {
693
0
            let format_short_estimate = |estimate: &Estimate| -> String {
694
0
                format!(
695
0
                    "[{} {}]",
696
0
                    formatter.format_value(estimate.confidence_interval.lower_bound),
697
0
                    formatter.format_value(estimate.confidence_interval.upper_bound)
698
                )
699
0
            };
700
701
0
            let data = &meas.data;
702
0
            if let Some(slope_estimate) = meas.absolute_estimates.slope.as_ref() {
703
0
                println!(
704
0
                    "{:<7}{} {:<15}[{:0.7} {:0.7}]",
705
0
                    "slope",
706
0
                    format_short_estimate(slope_estimate),
707
0
                    "R^2",
708
0
                    Slope(slope_estimate.confidence_interval.lower_bound).r_squared(data),
709
0
                    Slope(slope_estimate.confidence_interval.upper_bound).r_squared(data),
710
0
                );
711
0
            }
712
0
            println!(
713
0
                "{:<7}{} {:<15}{}",
714
                "mean",
715
0
                format_short_estimate(&meas.absolute_estimates.mean),
716
                "std. dev.",
717
0
                format_short_estimate(&meas.absolute_estimates.std_dev),
718
            );
719
0
            println!(
720
0
                "{:<7}{} {:<15}{}",
721
                "median",
722
0
                format_short_estimate(&meas.absolute_estimates.median),
723
                "med. abs. dev.",
724
0
                format_short_estimate(&meas.absolute_estimates.median_abs_dev),
725
            );
726
0
        }
727
0
    }
728
729
0
    fn group_separator(&self) {
730
0
        println!();
731
0
    }
732
}
733
734
pub struct BencherReport;
735
impl Report for BencherReport {
736
0
    fn measurement_start(
737
0
        &self,
738
0
        id: &BenchmarkId,
739
0
        _context: &ReportContext,
740
0
        _sample_count: u64,
741
0
        _estimate_ns: f64,
742
0
        _iter_count: u64,
743
0
    ) {
744
0
        print!("test {} ... ", id);
745
0
    }
746
747
0
    fn measurement_complete(
748
0
        &self,
749
0
        _id: &BenchmarkId,
750
0
        _: &ReportContext,
751
0
        meas: &MeasurementData<'_>,
752
0
        formatter: &dyn ValueFormatter,
753
0
    ) {
754
0
        let mut values = [
755
0
            meas.absolute_estimates.median.point_estimate,
756
0
            meas.absolute_estimates.std_dev.point_estimate,
757
0
        ];
758
0
        let unit = formatter.scale_for_machines(&mut values);
759
760
0
        println!(
761
0
            "bench: {:>11} {}/iter (+/- {})",
762
0
            format::integer(values[0]),
763
            unit,
764
0
            format::integer(values[1])
765
        );
766
0
    }
767
768
0
    fn group_separator(&self) {
769
0
        println!();
770
0
    }
771
}
772
773
enum ComparisonResult {
774
    Improved,
775
    Regressed,
776
    NonSignificant,
777
}
778
779
0
fn compare_to_threshold(estimate: &Estimate, noise: f64) -> ComparisonResult {
780
0
    let ci = &estimate.confidence_interval;
781
0
    let lb = ci.lower_bound;
782
0
    let ub = ci.upper_bound;
783
784
0
    if lb < -noise && ub < -noise {
785
0
        ComparisonResult::Improved
786
0
    } else if lb > noise && ub > noise {
787
0
        ComparisonResult::Regressed
788
    } else {
789
0
        ComparisonResult::NonSignificant
790
    }
791
0
}
792
793
#[cfg(test)]
794
mod test {
795
    use super::*;
796
797
    #[test]
798
    fn test_make_filename_safe_replaces_characters() {
799
        let input = "?/\\*\"";
800
        let safe = make_filename_safe(input);
801
        assert_eq!("_____", &safe);
802
    }
803
804
    #[test]
805
    fn test_make_filename_safe_truncates_long_strings() {
806
        let input = "this is a very long string. it is too long to be safe as a directory name, and so it needs to be truncated. what a long string this is.";
807
        let safe = make_filename_safe(input);
808
        assert!(input.len() > MAX_DIRECTORY_NAME_LEN);
809
        assert_eq!(&input[0..MAX_DIRECTORY_NAME_LEN], &safe);
810
    }
811
812
    #[test]
813
    fn test_make_filename_safe_respects_character_boundaries() {
814
        let input = "✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓";
815
        let safe = make_filename_safe(input);
816
        assert!(safe.len() < MAX_DIRECTORY_NAME_LEN);
817
    }
818
819
    #[test]
820
    fn test_benchmark_id_make_directory_name_unique() {
821
        let existing_id = BenchmarkId::new(
822
            "group".to_owned(),
823
            Some("function".to_owned()),
824
            Some("value".to_owned()),
825
            None,
826
        );
827
        let mut directories = HashSet::new();
828
        directories.insert(existing_id.as_directory_name().to_owned());
829
830
        let mut new_id = existing_id.clone();
831
        new_id.ensure_directory_name_unique(&directories);
832
        assert_eq!("group/function/value_2", new_id.as_directory_name());
833
        directories.insert(new_id.as_directory_name().to_owned());
834
835
        new_id = existing_id;
836
        new_id.ensure_directory_name_unique(&directories);
837
        assert_eq!("group/function/value_3", new_id.as_directory_name());
838
        directories.insert(new_id.as_directory_name().to_owned());
839
    }
840
    #[test]
841
    fn test_benchmark_id_make_long_directory_name_unique() {
842
        let long_name = (0..MAX_DIRECTORY_NAME_LEN).map(|_| 'a').collect::<String>();
843
        let existing_id = BenchmarkId::new(long_name, None, None, None);
844
        let mut directories = HashSet::new();
845
        directories.insert(existing_id.as_directory_name().to_owned());
846
847
        let mut new_id = existing_id.clone();
848
        new_id.ensure_directory_name_unique(&directories);
849
        assert_ne!(existing_id.as_directory_name(), new_id.as_directory_name());
850
    }
851
}