1pub(crate) mod analysis;
2pub(crate) mod disk;
3pub(crate) mod ser;
4
5#[cfg(feature = "bench")]
6use crate::benching::SamplingData;
7#[cfg(feature = "bench")]
8use crate::output::analysis::criterion::{
9 calculate_p_value, calculate_t_value, resample, BenchmarkConfig, SamplingDataSimpleAnalysis,
10};
11#[cfg(feature = "bench")]
12use crate::output::analysis::sample_data::simple_analyze_sampling_data;
13#[cfg(feature = "timer")]
14use crate::timing::TimingData;
15
16#[cfg(feature = "timer")]
19const TIMING_NOISE_THRESHOLD: f64 = 5.0;
20
21#[cfg(feature = "bench")]
24const NOISE_THRESHOLD: f64 = 1.0;
25
26#[cfg(feature = "bench")]
29const SIGNIFICANCE_LEVEL: f64 = 0.05;
30
31#[cfg(feature = "timer")]
32pub(crate) struct LabeledOutput<Output> {
33 label: &'static str,
34 out: Output,
35}
36
37#[cfg(feature = "timer")]
38impl<O> LabeledOutput<O> {
39 pub(crate) fn new(label: &'static str, out: O) -> Self {
40 Self { label, out }
41 }
42}
43
44#[cfg(feature = "timer")]
45impl<O> LabeledOutput<O>
46where
47 O: Output,
48{
49 pub(crate) fn dump(&self, data: TimingData) {
50 self.out.dump_timing_data(self.label, data);
51 }
52}
53
54pub(crate) trait Output {
55 #[cfg(feature = "timer")]
56 fn dump_timing_data(&self, label: &'static str, data: TimingData);
57
58 #[cfg(feature = "bench")]
59 fn dump_sampling_data(
60 &self,
61 label: &'static str,
62 sampling_data: &SamplingData,
63 cfg: &BenchmarkConfig,
64 total_iters: u128,
65 );
66}
67
68pub struct SimpleStdout;
70
71impl Output for SimpleStdout {
72 #[cfg(feature = "timer")]
73 fn dump_timing_data(&self, label: &'static str, data: TimingData) {
74 print_timer_header(label, &data);
75 let mean = data.elapsed as f64 / data.iterations as f64;
76 timer_print_elapsed(data.min_nanos as f64, mean, data.max_nanos as f64);
77 }
78
79 #[cfg(feature = "bench")]
80 fn dump_sampling_data(
81 &self,
82 label: &'static str,
83 sampling_data: &SamplingData,
84 cfg: &BenchmarkConfig,
85 total_iters: u128,
86 ) {
87 let analysis = simple_analyze_sampling_data(sampling_data);
88 print_sample_header(label, total_iters, analysis.elapsed, cfg.num_samples as u64);
89 print_analysis(&analysis);
90 }
91}
92
93pub struct ComparedStdout;
95
96impl Output for ComparedStdout {
97 #[cfg(feature = "timer")]
98 fn dump_timing_data(&self, label: &'static str, data: TimingData) {
99 let mean = data.elapsed as f64 / data.iterations as f64;
100 let maybe_old = disk::try_read_last_results(label);
101 print_timer_header(label, &data);
102 timer_print_elapsed(data.min_nanos as f64, mean, data.max_nanos as f64);
103 match maybe_old {
104 Ok(Some(old)) => {
105 let min_change = (data.min_nanos as f64 / old.min_nanos as f64 - 1f64) * 100f64;
106 let max_change = (data.max_nanos as f64 / old.max_nanos as f64 - 1f64) * 100f64;
107 let mean_change =
108 (mean / (old.elapsed as f64 / old.iterations as f64) - 1f64) * 100f64;
109 let mean_comparison = if mean_change >= TIMING_NOISE_THRESHOLD {
110 MeanComparison::new(mean_change, Comparison::Better)
111 } else if mean_change <= -TIMING_NOISE_THRESHOLD {
112 MeanComparison::new(mean_change, Comparison::Worse)
113 } else {
114 MeanComparison::new(mean_change, Comparison::Same)
115 };
116 print_cmp(
117 min_change,
118 &mean_comparison,
119 max_change,
120 "p=? single sample",
121 );
122 }
123 Err(e) => {
124 println!(
125 "{}, cause {e}",
126 wrap_high_insensity_red("Failed to read last results")
127 );
128 }
129 _ => {}
130 }
131 disk::try_write_results(label, data);
132 }
133
134 #[cfg(feature = "bench")]
135 fn dump_sampling_data(
136 &self,
137 label: &'static str,
138 sampling_data: &SamplingData,
139 cfg: &BenchmarkConfig,
140 total_iters: u128,
141 ) {
142 let analysis = simple_analyze_sampling_data(sampling_data);
143 print_sample_header(label, total_iters, analysis.elapsed, cfg.num_samples as u64);
144 print_analysis(&analysis);
145 match disk::try_read_last_simpling(label) {
146 Ok(Some(last)) => {
147 let old_analysis = simple_analyze_sampling_data(&last);
148 let min_change = (analysis.min / old_analysis.min - 1f64) * 100f64;
149 let max_change = (analysis.max / old_analysis.max - 1f64) * 100f64;
150 let mean_change = (analysis.average / old_analysis.average - 1f64) * 100f64;
151 let t = calculate_t_value(
152 &analysis.per_sample_average,
153 &old_analysis.per_sample_average,
154 );
155 let t_distribution = resample(
156 &analysis.per_sample_average,
157 &old_analysis.per_sample_average,
158 cfg.num_resamples,
159 );
160 let p = calculate_p_value(t, &t_distribution);
161 let mean_change = if mean_change.abs() >= NOISE_THRESHOLD && p <= SIGNIFICANCE_LEVEL
162 {
163 if mean_change > 0.0 {
164 MeanComparison::new(mean_change, Comparison::Worse)
165 } else if mean_change < 0.0 {
166 MeanComparison::new(mean_change, Comparison::Better)
167 } else {
168 MeanComparison::new(mean_change, Comparison::Same)
169 }
170 } else {
171 MeanComparison::new(mean_change, Comparison::Same)
172 };
173 print_cmp(min_change, &mean_change, max_change, &format!("p = {p:.2}"));
174 }
175 Err(e) => {
176 println!(
177 "{}, cause {e}",
178 wrap_high_insensity_red("Failed to read last sample")
179 );
180 }
181 _ => {}
182 }
183
184 disk::try_write_last_simpling(label, sampling_data);
185 }
186}
187
188#[cfg(feature = "timer")]
189pub(crate) fn print_timer_header(label: &'static str, data: &TimingData) {
190 println!(
191 "{} [{} iterations in {}]:",
192 wrap_bold_green(label),
193 fmt_num(data.iterations as f64),
194 fmt_time(data.elapsed as f64)
195 );
196}
197
198#[cfg(feature = "bench")]
199pub(crate) fn print_sample_header(
200 label: &'static str,
201 total_iterations: u128,
202 total_elapsed: u128,
203 num_samples: u64,
204) {
205 println!(
206 "{} [{} iterations in {} with {} samples]:",
207 wrap_bold_green(label),
208 fmt_num(total_iterations as f64),
209 fmt_time(total_elapsed as f64),
210 fmt_num(num_samples as f64)
211 );
212}
213
214#[cfg(feature = "bench")]
215pub(crate) fn print_analysis(analysis: &SamplingDataSimpleAnalysis) {
216 println!(
218 "\telapsed\t[{} {} {}]:\t[{} {} {}] (sample data: med = {}, var = {}², stddev = {})",
219 wrap_gray("min"),
220 wrap_high_intensity_white("mean"),
221 wrap_gray("max"),
222 wrap_gray(&fmt_time(analysis.min)),
223 wrap_high_intensity_white(&fmt_time(analysis.average)),
224 wrap_gray(&fmt_time(analysis.max)),
225 fmt_time(analysis.median),
226 fmt_time(analysis.variance),
227 fmt_time(analysis.stddev),
228 );
229}
230
231#[cfg(feature = "timer")]
232pub(crate) fn timer_print_elapsed(min: f64, mean: f64, max: f64) {
233 println!(
235 "\telapsed\t[{} {} {}]:\t[{} {} {}]",
236 wrap_gray("min"),
237 wrap_high_intensity_white("mean"),
238 wrap_gray("max"),
239 wrap_gray(&fmt_time(min)),
240 wrap_high_intensity_white(&fmt_time(mean)),
241 wrap_gray(&fmt_time(max)),
242 );
243}
244
245pub(crate) struct MeanComparison {
246 mean: f64,
247 comparison: Comparison,
248}
249
250impl MeanComparison {
251 pub(crate) fn new(mean: f64, comparison: Comparison) -> Self {
252 Self { mean, comparison }
253 }
254
255 pub(crate) fn format(&self) -> String {
256 match self.comparison {
257 Comparison::Worse => wrap_high_insensity_red(&fmt_change(self.mean)),
258 Comparison::Same => wrap_high_intensity_white(&fmt_change(self.mean)),
259 Comparison::Better => wrap_high_intensity_green(&fmt_change(self.mean)),
260 }
261 }
262}
263
264pub enum Comparison {
265 Worse,
266 Same,
267 Better,
268}
269
270pub(crate) fn print_cmp(min: f64, mean: &MeanComparison, max: f64, reliability_comment: &str) {
271 println!(
272 "\tchange\t[{} {} {}]:\t[{} {} {}] ({reliability_comment})",
273 wrap_gray("min"),
274 wrap_high_intensity_white("mean"),
275 wrap_gray("max"),
276 wrap_gray(&fmt_change(min)),
277 mean.format(),
278 wrap_gray(&fmt_change(max)),
279 );
280}
281
282const NANO_LIMIT: f64 = 1000f64;
283const MICRO_LIMIT: f64 = NANO_LIMIT * 1000f64;
284const MILLI_LIMIT: f64 = MICRO_LIMIT * 1000f64;
285
286pub(crate) fn wrap_bold_green(text: &str) -> String {
287 format!("\x1b[1;32m{text}\x1b[0m")
288}
289
290pub(crate) fn wrap_high_intensity_green(text: &str) -> String {
291 format!("\x1b[0;92m{text}\x1b[0m")
292}
293
294pub(crate) fn wrap_yellow(text: &str) -> String {
295 format!("\x1b[0;93m{text}\x1b[0m")
296}
297
298pub(crate) fn wrap_high_insensity_red(text: &str) -> String {
299 format!("\x1b[0;91m{text}\x1b[0m")
300}
301
302pub(crate) fn wrap_gray(text: &str) -> String {
303 format!("\x1b[0;37m{text}\x1b[0m")
304}
305
306pub(crate) fn wrap_high_intensity_white(text: &str) -> String {
307 format!("\x1b[0;97m{text}\x1b[0m")
308}
309
310pub(crate) fn fmt_time(time: f64) -> String {
311 if time < NANO_LIMIT {
313 format!("{time:.2}ns")
314 } else if time < MICRO_LIMIT {
315 format!("{:.2}µs", time / NANO_LIMIT)
316 } else if time < MILLI_LIMIT {
317 format!("{:.2}ms", time / MICRO_LIMIT)
318 } else {
319 format!("{:.2}s", time / MILLI_LIMIT)
320 }
321}
322
323fn fmt_change(change: f64) -> String {
324 format!("{change:.4}%")
325}
326
327pub(crate) fn fmt_num(num: f64) -> String {
328 if num < NANO_LIMIT {
329 format!("{num:.1}")
330 } else if num < MICRO_LIMIT {
331 format!("{:.1} thousand", num / NANO_LIMIT)
332 } else if num < MILLI_LIMIT {
333 format!("{:.1}M", num / MICRO_LIMIT)
334 } else {
335 format!("{:.1}B", num / MILLI_LIMIT)
336 }
337}
338
339const ILLEGAL: [char; 10] = [
341 '/', '\0', ':', '<', '>', '"', '\\', '|', '?', '*',
344];
345
346#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
347pub(crate) enum LabelValidationResult {
348 Valid,
349 Invalid(&'static str),
350}
351
352pub(crate) fn fallback_to_anonymous_on_invalid_label(label: &'static str) -> &'static str {
353 if let LabelValidationResult::Invalid(reason) = validate_label(label) {
354 println!(
355 "{} falling back to 'anonymous'.",
356 wrap_high_insensity_red(reason)
357 );
358 "anonymous"
359 } else {
360 label
361 }
362}
363
364fn validate_label(label: &'static str) -> LabelValidationResult {
365 for ch in ILLEGAL {
366 if label.contains(ch) {
367 return LabelValidationResult::Invalid("Label contains illegal character {ch}");
368 }
369 }
370 for ch in 0..32 {
371 let ascii_ctrl = char::from(ch);
372 if label.contains(ascii_ctrl) {
373 return LabelValidationResult::Invalid(
374 "Label contains illegal ascii-control character number {ch}",
375 );
376 }
377 }
378 if label.ends_with('.') {
379 return LabelValidationResult::Invalid("Label cannot end with dot");
380 }
381 if label.ends_with(' ') {
382 return LabelValidationResult::Invalid("Label cannot end with a space");
383 }
384 LabelValidationResult::Valid
385}
386
387#[cfg(test)]
388mod tests {
389 use crate::output::{fmt_change, fmt_num, fmt_time, validate_label, LabelValidationResult};
390
391 #[test]
392 fn validates_label() {
393 assert_eq!(LabelValidationResult::Valid, validate_label("Hello!"));
394 assert_eq!(
395 LabelValidationResult::Valid,
396 validate_label("Some,weird_name_but.okay.png")
397 );
398 assert!(matches!(
399 validate_label("."),
400 LabelValidationResult::Invalid(_)
401 ));
402 assert!(matches!(
403 validate_label("hello!."),
404 LabelValidationResult::Invalid(_)
405 ));
406 assert!(matches!(
407 validate_label("hello! "),
408 LabelValidationResult::Invalid(_)
409 ));
410 assert!(matches!(
411 validate_label("bad/label"),
412 LabelValidationResult::Invalid(_)
413 ));
414 assert!(matches!(
415 validate_label("bad:label"),
416 LabelValidationResult::Invalid(_)
417 ));
418 assert!(matches!(
419 validate_label("bad>label"),
420 LabelValidationResult::Invalid(_)
421 ));
422 assert!(matches!(
423 validate_label("bad<label"),
424 LabelValidationResult::Invalid(_)
425 ));
426 assert!(matches!(
427 validate_label("bad\0label"),
428 LabelValidationResult::Invalid(_)
429 ));
430 assert!(matches!(
431 validate_label("bad\\label"),
432 LabelValidationResult::Invalid(_)
433 ));
434 assert!(matches!(
435 validate_label("bad\"label"),
436 LabelValidationResult::Invalid(_)
437 ));
438 assert!(matches!(
439 validate_label("bad|label"),
440 LabelValidationResult::Invalid(_)
441 ));
442 assert!(matches!(
443 validate_label("bad?label"),
444 LabelValidationResult::Invalid(_)
445 ));
446 assert!(matches!(
447 validate_label("bad*label"),
448 LabelValidationResult::Invalid(_)
449 ));
450 }
451
452 #[test]
453 fn formats_time() {
454 assert_eq!("5.15ns", &fmt_time(5.15));
455 assert_eq!("1.50µs", &fmt_time(1500.0));
456 assert_eq!("3.33ms", &fmt_time(3_330_000.0));
457 assert_eq!("5.79s", &fmt_time(5_790_000_000.0));
458 assert_eq!("68.00s", &fmt_time(68_000_000_000.0));
459 }
460
461 #[test]
462 fn formats_number() {
463 assert_eq!("5.1", &fmt_num(5.1));
464 assert_eq!("35.0 thousand", &fmt_num(35_000.0));
465 assert_eq!("97.0M", &fmt_num(97_000_000.0));
466 assert_eq!("7.9B", &fmt_num(7_900_000_000.0));
467 }
468
469 #[test]
470 fn formats_change() {
471 assert_eq!("5.1973%", &fmt_change(5.1973));
472 }
473}