/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 | | } |