pub(crate) mod analysis;
pub(crate) mod disk;
pub(crate) mod ser;
#[cfg(feature = "bench")]
use crate::benching::SamplingData;
#[cfg(feature = "bench")]
use crate::output::analysis::criterion::{
calculate_p_value, calculate_t_value, resample, BenchmarkConfig, SamplingDataSimpleAnalysis,
};
#[cfg(feature = "bench")]
use crate::output::analysis::sample_data::simple_analyze_sampling_data;
#[cfg(feature = "timer")]
use crate::timing::TimingData;
#[cfg(feature = "timer")]
const TIMING_NOISE_THRESHOLD: f64 = 5.0;
#[cfg(feature = "bench")]
const NOISE_THRESHOLD: f64 = 1.0;
#[cfg(feature = "bench")]
const SIGNIFICANCE_LEVEL: f64 = 0.05;
#[cfg(feature = "timer")]
pub(crate) struct LabeledOutput<Output> {
label: &'static str,
out: Output,
}
#[cfg(feature = "timer")]
impl<O> LabeledOutput<O> {
pub(crate) fn new(label: &'static str, out: O) -> Self {
Self { label, out }
}
}
#[cfg(feature = "timer")]
impl<O> LabeledOutput<O>
where
O: Output,
{
pub(crate) fn dump(&self, data: TimingData) {
self.out.dump_timing_data(self.label, data);
}
}
pub(crate) trait Output {
#[cfg(feature = "timer")]
fn dump_timing_data(&self, label: &'static str, data: TimingData);
#[cfg(feature = "bench")]
fn dump_sampling_data(
&self,
label: &'static str,
sampling_data: &SamplingData,
cfg: &BenchmarkConfig,
total_iters: u128,
);
}
pub struct SimpleStdout;
impl Output for SimpleStdout {
#[cfg(feature = "timer")]
fn dump_timing_data(&self, label: &'static str, data: TimingData) {
print_timer_header(label, &data);
let mean = data.elapsed as f64 / data.iterations as f64;
timer_print_elapsed(data.min_nanos as f64, mean, data.max_nanos as f64);
}
#[cfg(feature = "bench")]
fn dump_sampling_data(
&self,
label: &'static str,
sampling_data: &SamplingData,
cfg: &BenchmarkConfig,
total_iters: u128,
) {
let analysis = simple_analyze_sampling_data(sampling_data);
print_sample_header(label, total_iters, analysis.elapsed, cfg.num_samples as u64);
print_analysis(&analysis);
}
}
pub struct ComparedStdout;
impl Output for ComparedStdout {
#[cfg(feature = "timer")]
fn dump_timing_data(&self, label: &'static str, data: TimingData) {
let mean = data.elapsed as f64 / data.iterations as f64;
let maybe_old = disk::try_read_last_results(label);
print_timer_header(label, &data);
timer_print_elapsed(data.min_nanos as f64, mean, data.max_nanos as f64);
match maybe_old {
Ok(Some(old)) => {
let min_change = (data.min_nanos as f64 / old.min_nanos as f64 - 1f64) * 100f64;
let max_change = (data.max_nanos as f64 / old.max_nanos as f64 - 1f64) * 100f64;
let mean_change =
(mean / (old.elapsed as f64 / old.iterations as f64) - 1f64) * 100f64;
let mean_comparison = if mean_change >= TIMING_NOISE_THRESHOLD {
MeanComparison::new(mean_change, Comparison::Better)
} else if mean_change <= -TIMING_NOISE_THRESHOLD {
MeanComparison::new(mean_change, Comparison::Worse)
} else {
MeanComparison::new(mean_change, Comparison::Same)
};
print_cmp(
min_change,
&mean_comparison,
max_change,
"p=? single sample",
);
}
Err(e) => {
println!(
"{}, cause {e}",
wrap_high_insensity_red("Failed to read last results")
);
}
_ => {}
}
disk::try_write_results(label, data);
}
#[cfg(feature = "bench")]
fn dump_sampling_data(
&self,
label: &'static str,
sampling_data: &SamplingData,
cfg: &BenchmarkConfig,
total_iters: u128,
) {
let analysis = simple_analyze_sampling_data(sampling_data);
print_sample_header(label, total_iters, analysis.elapsed, cfg.num_samples as u64);
print_analysis(&analysis);
match disk::try_read_last_simpling(label) {
Ok(Some(last)) => {
let old_analysis = simple_analyze_sampling_data(&last);
let min_change = (analysis.min / old_analysis.min - 1f64) * 100f64;
let max_change = (analysis.max / old_analysis.max - 1f64) * 100f64;
let mean_change = (analysis.average / old_analysis.average - 1f64) * 100f64;
let t = calculate_t_value(
&analysis.per_sample_average,
&old_analysis.per_sample_average,
);
let t_distribution = resample(
&analysis.per_sample_average,
&old_analysis.per_sample_average,
cfg.num_resamples,
);
let p = calculate_p_value(t, &t_distribution);
let mean_change = if mean_change.abs() >= NOISE_THRESHOLD && p <= SIGNIFICANCE_LEVEL
{
if mean_change > 0.0 {
MeanComparison::new(mean_change, Comparison::Worse)
} else if mean_change < 0.0 {
MeanComparison::new(mean_change, Comparison::Better)
} else {
MeanComparison::new(mean_change, Comparison::Same)
}
} else {
MeanComparison::new(mean_change, Comparison::Same)
};
print_cmp(min_change, &mean_change, max_change, &format!("p = {p:.2}"));
}
Err(e) => {
println!(
"{}, cause {e}",
wrap_high_insensity_red("Failed to read last sample")
);
}
_ => {}
}
disk::try_write_last_simpling(label, sampling_data);
}
}
#[cfg(feature = "timer")]
pub(crate) fn print_timer_header(label: &'static str, data: &TimingData) {
println!(
"{} [{} iterations in {}]:",
wrap_bold_green(label),
fmt_num(data.iterations as f64),
fmt_time(data.elapsed as f64)
);
}
#[cfg(feature = "bench")]
pub(crate) fn print_sample_header(
label: &'static str,
total_iterations: u128,
total_elapsed: u128,
num_samples: u64,
) {
println!(
"{} [{} iterations in {} with {} samples]:",
wrap_bold_green(label),
fmt_num(total_iterations as f64),
fmt_time(total_elapsed as f64),
fmt_num(num_samples as f64)
);
}
#[cfg(feature = "bench")]
pub(crate) fn print_analysis(analysis: &SamplingDataSimpleAnalysis) {
println!(
"\telapsed\t[{} {} {}]:\t[{} {} {}] (sample data: med = {}, var = {}², stddev = {})",
wrap_gray("min"),
wrap_high_intensity_white("mean"),
wrap_gray("max"),
wrap_gray(&fmt_time(analysis.min)),
wrap_high_intensity_white(&fmt_time(analysis.average)),
wrap_gray(&fmt_time(analysis.max)),
fmt_time(analysis.median),
fmt_time(analysis.variance),
fmt_time(analysis.stddev),
);
}
#[cfg(feature = "timer")]
pub(crate) fn timer_print_elapsed(min: f64, mean: f64, max: f64) {
println!(
"\telapsed\t[{} {} {}]:\t[{} {} {}]",
wrap_gray("min"),
wrap_high_intensity_white("mean"),
wrap_gray("max"),
wrap_gray(&fmt_time(min)),
wrap_high_intensity_white(&fmt_time(mean)),
wrap_gray(&fmt_time(max)),
);
}
pub(crate) struct MeanComparison {
mean: f64,
comparison: Comparison,
}
impl MeanComparison {
pub(crate) fn new(mean: f64, comparison: Comparison) -> Self {
Self { mean, comparison }
}
pub(crate) fn format(&self) -> String {
match self.comparison {
Comparison::Worse => wrap_high_insensity_red(&fmt_change(self.mean)),
Comparison::Same => wrap_high_intensity_white(&fmt_change(self.mean)),
Comparison::Better => wrap_high_intensity_green(&fmt_change(self.mean)),
}
}
}
pub enum Comparison {
Worse,
Same,
Better,
}
pub(crate) fn print_cmp(min: f64, mean: &MeanComparison, max: f64, reliability_comment: &str) {
println!(
"\tchange\t[{} {} {}]:\t[{} {} {}] ({reliability_comment})",
wrap_gray("min"),
wrap_high_intensity_white("mean"),
wrap_gray("max"),
wrap_gray(&fmt_change(min)),
mean.format(),
wrap_gray(&fmt_change(max)),
);
}
const NANO_LIMIT: f64 = 1000f64;
const MICRO_LIMIT: f64 = NANO_LIMIT * 1000f64;
const MILLI_LIMIT: f64 = MICRO_LIMIT * 1000f64;
pub(crate) fn wrap_bold_green(text: &str) -> String {
format!("\x1b[1;32m{text}\x1b[0m")
}
pub(crate) fn wrap_high_intensity_green(text: &str) -> String {
format!("\x1b[0;92m{text}\x1b[0m")
}
pub(crate) fn wrap_yellow(text: &str) -> String {
format!("\x1b[0;93m{text}\x1b[0m")
}
pub(crate) fn wrap_high_insensity_red(text: &str) -> String {
format!("\x1b[0;91m{text}\x1b[0m")
}
pub(crate) fn wrap_gray(text: &str) -> String {
format!("\x1b[0;37m{text}\x1b[0m")
}
pub(crate) fn wrap_high_intensity_white(text: &str) -> String {
format!("\x1b[0;97m{text}\x1b[0m")
}
pub(crate) fn fmt_time(time: f64) -> String {
if time < NANO_LIMIT {
format!("{time:.2}ns")
} else if time < MICRO_LIMIT {
format!("{:.2}µs", time / NANO_LIMIT)
} else if time < MILLI_LIMIT {
format!("{:.2}ms", time / MICRO_LIMIT)
} else {
format!("{:.2}s", time / MILLI_LIMIT)
}
}
fn fmt_change(change: f64) -> String {
format!("{change:.4}%")
}
pub(crate) fn fmt_num(num: f64) -> String {
if num < NANO_LIMIT {
format!("{num:.1}")
} else if num < MICRO_LIMIT {
format!("{:.1} thousand", num / NANO_LIMIT)
} else if num < MILLI_LIMIT {
format!("{:.1}M", num / MICRO_LIMIT)
} else {
format!("{:.1}B", num / MILLI_LIMIT)
}
}
const ILLEGAL: [char; 10] = [
'/', '\0', ':', '<', '>', '"', '\\', '|', '?', '*',
];
#[cfg_attr(test, derive(Eq, PartialEq, Debug))]
pub(crate) enum LabelValidationResult {
Valid,
Invalid(&'static str),
}
pub(crate) fn fallback_to_anonymous_on_invalid_label(label: &'static str) -> &'static str {
if let LabelValidationResult::Invalid(reason) = validate_label(label) {
println!(
"{} falling back to 'anonymous'.",
wrap_high_insensity_red(reason)
);
"anonymous"
} else {
label
}
}
fn validate_label(label: &'static str) -> LabelValidationResult {
for ch in ILLEGAL {
if label.contains(ch) {
return LabelValidationResult::Invalid("Label contains illegal character {ch}");
}
}
for ch in 0..32 {
let ascii_ctrl = char::from(ch);
if label.contains(ascii_ctrl) {
return LabelValidationResult::Invalid(
"Label contains illegal ascii-control character number {ch}",
);
}
}
if label.ends_with('.') {
return LabelValidationResult::Invalid("Label cannot end with dot");
}
if label.ends_with(' ') {
return LabelValidationResult::Invalid("Label cannot end with a space");
}
LabelValidationResult::Valid
}
#[cfg(test)]
mod tests {
use crate::output::{fmt_change, fmt_num, fmt_time, validate_label, LabelValidationResult};
#[test]
fn validates_label() {
assert_eq!(LabelValidationResult::Valid, validate_label("Hello!"));
assert_eq!(
LabelValidationResult::Valid,
validate_label("Some,weird_name_but.okay.png")
);
assert!(matches!(
validate_label("."),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("hello!."),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("hello! "),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad/label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad:label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad>label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad<label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad\0label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad\\label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad\"label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad|label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad?label"),
LabelValidationResult::Invalid(_)
));
assert!(matches!(
validate_label("bad*label"),
LabelValidationResult::Invalid(_)
));
}
#[test]
fn formats_time() {
assert_eq!("5.15ns", &fmt_time(5.15));
assert_eq!("1.50µs", &fmt_time(1500.0));
assert_eq!("3.33ms", &fmt_time(3_330_000.0));
assert_eq!("5.79s", &fmt_time(5_790_000_000.0));
assert_eq!("68.00s", &fmt_time(68_000_000_000.0));
}
#[test]
fn formats_number() {
assert_eq!("5.1", &fmt_num(5.1));
assert_eq!("35.0 thousand", &fmt_num(35_000.0));
assert_eq!("97.0M", &fmt_num(97_000_000.0));
assert_eq!("7.9B", &fmt_num(7_900_000_000.0));
}
#[test]
fn formats_change() {
assert_eq!("5.1973%", &fmt_change(5.1973));
}
}