diff --git a/src/benchmark/mod.rs b/src/benchmark/mod.rs index 3de2e75..c8a9f18 100644 --- a/src/benchmark/mod.rs +++ b/src/benchmark/mod.rs @@ -16,7 +16,6 @@ use crate::options::{ CmdFailureAction, CommandOutputPolicy, ExecutorKind, Options, OutputStyleOption, }; use crate::outlier_detection::OUTLIER_THRESHOLD; -use crate::output::format::{format_duration, format_duration_unit}; use crate::output::console_writeln; use crate::output::progress_bar::{ finish_initial_measurement, get_progress_bar, start_initial_measurement, @@ -331,8 +330,9 @@ impl<'a> Benchmark<'a> { let benchmark_iteration = BenchmarkIteration::Benchmark(i + 1); let msg = { - let mean = - format_duration(measurements.time_wall_clock_mean(), self.options.time_unit); + let mean = measurements + .time_wall_clock_mean() + .format_auto(self.options.time_unit); format!("Current estimate: {}", mean.to_string().green()) }; @@ -365,14 +365,15 @@ impl<'a> Benchmark<'a> { } // Formatting and console output - let (mean_str, time_unit) = - format_duration_unit(measurements.time_wall_clock_mean(), self.options.time_unit); - let min_str = format_duration(measurements.min(), Some(time_unit)); - let max_str = format_duration(measurements.max(), Some(time_unit)); + let (mean_str, time_unit) = measurements + .time_wall_clock_mean() + .format_auto_with_unit(self.options.time_unit); + let min_str = measurements.min().format_auto(Some(time_unit)); + let max_str = measurements.max().format_auto(Some(time_unit)); let num_str = format!("{num_runs} runs", num_runs = measurements.len()); - let user_str = format_duration(measurements.time_user_mean(), Some(time_unit)); - let system_str = format_duration(measurements.time_system_mean(), Some(time_unit)); + let user_str = measurements.time_user_mean().format_auto(Some(time_unit)); + let system_str = measurements.time_system_mean().format_auto(Some(time_unit)); if self.options.output_style != OutputStyleOption::Disabled { if measurements.len() == 1 { @@ -385,7 +386,7 @@ impl<'a> Benchmark<'a> { system_str.blue() )?; } else { - let stddev_str = format_duration(measurements.stddev().unwrap(), Some(time_unit)); + let stddev_str = measurements.stddev().unwrap().format_auto(Some(time_unit)); console_writeln!( stdout, diff --git a/src/export/csv.rs b/src/export/csv.rs index 02d44b7..bbb55ff 100644 --- a/src/export/csv.rs +++ b/src/export/csv.rs @@ -5,8 +5,7 @@ use csv::WriterBuilder; use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; -use crate::quantity::{second, TimeQuantity}; -use crate::util::units::TimeUnit; +use crate::quantity::{second, TimeQuantity, TimeUnit}; use anyhow::Result; diff --git a/src/export/json.rs b/src/export/json.rs index a3bf18b..358b419 100644 --- a/src/export/json.rs +++ b/src/export/json.rs @@ -4,7 +4,7 @@ use serde_json::to_vec_pretty; use super::Exporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; -use crate::util::units::TimeUnit; +use crate::quantity::TimeUnit; use anyhow::Result; diff --git a/src/export/markup.rs b/src/export/markup.rs index 3b8bc63..2331ac9 100644 --- a/src/export/markup.rs +++ b/src/export/markup.rs @@ -51,9 +51,12 @@ pub trait MarkupExporter { let result = &entry.result; // prepare data row strings let cmd_str = result.command_with_unused_parameters().replace('|', "\\|"); - let mean_str = format_duration_value(result.mean_wall_clock_time(), Some(time_unit)).0; + let mean_str = result + .mean_wall_clock_time() + .format_value_auto(Some(time_unit)) + .0; let stddev_str = if let Some(stddev) = result.measurements.stddev() { - format!(" ± {}", format_duration_value(stddev, Some(time_unit)).0) + format!(" ± {}", stddev.format_value_auto(Some(time_unit)).0) } else { "".into() }; @@ -114,7 +117,10 @@ pub trait MarkupExporter { fn determine_unit_from_results(results: &[BenchmarkResult]) -> TimeUnit { if let Some(first_result) = results.first() { // Use the first BenchmarkResult entry to determine the unit for all entries. - format_duration_value(first_result.mean_wall_clock_time(), None).1 + first_result + .mean_wall_clock_time() + .format_auto_with_unit(None) + .1 } else { // Default to `Second`. TimeUnit::Second diff --git a/src/export/mod.rs b/src/export/mod.rs index 87cb681..c82675e 100644 --- a/src/export/mod.rs +++ b/src/export/mod.rs @@ -18,7 +18,7 @@ use self::orgmode::OrgmodeExporter; use crate::benchmark::benchmark_result::BenchmarkResult; use crate::options::SortOrder; -use crate::util::units::TimeUnit; +use crate::quantity::TimeUnit; use anyhow::{Context, Result}; use clap::ArgMatches; diff --git a/src/export/tests.rs b/src/export/tests.rs index e67cf74..2263981 100644 --- a/src/export/tests.rs +++ b/src/export/tests.rs @@ -3,8 +3,7 @@ use crate::benchmark::benchmark_result::BenchmarkResult; use crate::benchmark::measurement::{Measurement, Measurements}; use crate::export::asciidoc::AsciidocExporter; use crate::export::orgmode::OrgmodeExporter; -use crate::quantity::{byte, second, Information, Time, TimeQuantity}; -use crate::util::units::TimeUnit; +use crate::quantity::{byte, second, Information, Time, TimeQuantity, TimeUnit}; use crate::{export::markdown::MarkdownExporter, options::SortOrder}; use std::collections::BTreeMap; use std::process::ExitStatus; diff --git a/src/options.rs b/src/options.rs index 290d870..f5a3285 100644 --- a/src/options.rs +++ b/src/options.rs @@ -9,8 +9,7 @@ use clap::ArgMatches; use crate::command::Commands; use crate::error::OptionsError; -use crate::quantity::{second, Time}; -use crate::util::units::TimeUnit; +use crate::quantity::{second, Time, TimeUnit}; use anyhow::Result; diff --git a/src/output/warnings.rs b/src/output/warnings.rs index bcbfcd3..a0d5698 100644 --- a/src/output/warnings.rs +++ b/src/output/warnings.rs @@ -1,8 +1,7 @@ use std::fmt; use crate::benchmark::MIN_EXECUTION_TIME; -use crate::output::format::format_duration; -use crate::quantity::{millisecond, Time, TimeQuantity}; +use crate::quantity::{Time, TimeQuantity, TimeUnit}; pub struct OutlierWarningOptions { pub warmup_in_use: bool, @@ -26,7 +25,7 @@ impl fmt::Display for Warnings { inaccurate because hyperfine can not calibrate the shell startup time much \ more precise than this limit. You can try to use the `-N`/`--shell=none` \ option to disable the shell completely.", - min_execution_time = MIN_EXECUTION_TIME.to_string(millisecond) + min_execution_time = MIN_EXECUTION_TIME.format_value_in(TimeUnit::MilliSecond, 0) ), Warnings::NonZeroExitCode => write!(f, "Ignoring non-zero exit code."), Warnings::SlowInitialRun(time_first_run, ref options) => write!( @@ -34,7 +33,7 @@ impl fmt::Display for Warnings { "The first benchmarking run for this command was significantly slower than the \ rest ({time}). This could be caused by (filesystem) caches that were not filled until \ after the first run. {hints}", - time=time_first_run.format_auto(), + time=time_first_run.format_auto(None), hints=match (options.warmup_in_use, options.prepare_in_use) { (true, true) => "You are already using both the '--warmup' option as well \ as the '--prepare' option. Consider re-running the benchmark on a quiet system. \ diff --git a/src/quantity/mod.rs b/src/quantity/mod.rs index 5844474..75f1442 100644 --- a/src/quantity/mod.rs +++ b/src/quantity/mod.rs @@ -1,6 +1,17 @@ +use core::f64; use std::marker::PhantomData; -use uom::si; +use serde::ser::SerializeStruct; +use serde::Serializer; +use uom::{si, Conversion}; + +pub use uom::si::information::{byte, kibibyte}; +pub use uom::si::ratio::ratio; +pub use uom::si::time::{hour, microsecond, millisecond, minute, nanosecond, second}; + +pub use si::f64::Ratio; +pub use si::f64::Time; +pub use si::u64::Information; pub use si::f64::{Information, Ratio, Time}; pub use si::information::{byte, gibibyte, kibibyte, mebibyte, tebibyte}; @@ -10,52 +21,67 @@ pub use uom::num_traits::Zero; pub use units::{InformationUnit, IsUnit, TimeUnit}; -pub mod statistics; mod units; -pub trait FormatQuantity { - type Unit; - - fn suitable_unit(&self) -> Self::Unit; - - fn format_with_precision(&self, unit: Self::Unit, precision: usize) -> String; - fn format(&self, unit: Self::Unit) -> String; - fn format_auto(&self) -> String; - fn format_value(&self, unit: Self::Unit) -> String; -} - -impl FormatQuantity for Time { - type Unit = TimeUnit; - - fn suitable_unit(&self) -> TimeUnit { - if *self < Time::new::(1.0) { - TimeUnit::MicroSecond - } else if *self < Time::new::(1.0) { - TimeUnit::MilliSecond - } else { - TimeUnit::Second - } +pub trait TimeQuantity { + fn zero() -> Time { + Time::new::(0.0) } - /// Format the time duration in the given unit with the given precision. - fn format_with_precision(&self, u: TimeUnit, precision: usize) -> String { + fn value_in(&self, u: U) -> f64 + where + U: si::time::Unit + Conversion; + + fn format_value_in(&self, time_unit: TimeUnit, precision: usize) -> String; + fn format_auto(&self, time_unit: Option) -> String; + fn format_auto_with_unit(&self, time_unit: Option) -> (String, TimeUnit); + fn format_value_auto(&self, time_unit: Option) -> (String, TimeUnit); +} + +impl TimeQuantity for Time { + fn value_in(&self, _u: U) -> f64 + where + U: si::time::Unit + Conversion, + { + self.get::() + } + + fn format_value_in(&self, u: TimeUnit, precision: usize) -> String { u.format(*self, precision) } - /// Format the time duration in the given unit. - fn format(&self, unit: TimeUnit) -> String { - let value = self.format_with_precision(unit, unit.preferred_precision()); - format!("{} {}", value, unit.short_name()) + /// Format the given duration as a string. The output-unit can be enforced by setting `unit` to + /// `Some(target_unit)`. If `unit` is `None`, it will be determined automatically. + fn format_auto(&self, time_unit: Option) -> String { + self.format_auto_with_unit(time_unit).0 } - /// Format the given time duration. The unit will be determined automatically. - fn format_auto(&self) -> String { - self.format(self.suitable_unit()) + /// Like `format_duration`, but returns the target unit as well. + fn format_auto_with_unit(&self, time_unit: Option) -> (String, TimeUnit) { + let (out_str, out_unit) = self.format_value_auto(time_unit); + + (format!("{} {}", out_str, out_unit.short_name()), out_unit) } - /// Like `format`, but without displaying the unit. - fn format_value(&self, unit: TimeUnit) -> String { - self.format_with_precision(unit, unit.preferred_precision()) + /// Like `format_duration`, but returns the target unit as well. + fn format_value_auto(&self, time_unit: Option) -> (String, TimeUnit) { + let (time_unit, precision) = if (*self < Time::new::(1.0) + && time_unit.is_none()) + || time_unit == Some(TimeUnit::MicroSecond) + { + (TimeUnit::MicroSecond, 1) + } else if (*self < Time::new::(1.0) && time_unit.is_none()) + || time_unit == Some(TimeUnit::MilliSecond) + { + (TimeUnit::MilliSecond, 1) + } else { + let time_unit = time_unit.unwrap_or(TimeUnit::Second); + let precision = if time_unit == TimeUnit::Second { 3 } else { 1 }; + + (time_unit, precision) + }; + + (self.format_value_in(time_unit, precision), time_unit) } } @@ -68,91 +94,126 @@ pub const fn const_time_from_seconds(value: f64) -> Time { } } -impl FormatQuantity for Information { - type Unit = InformationUnit; - - fn suitable_unit(&self) -> InformationUnit { - if *self < Information::new::(1.0) { - InformationUnit::Byte - } else if *self < Information::new::(1.0) { - InformationUnit::KibiByte - } else if *self < Information::new::(1.0) { - InformationUnit::MebiByte - } else { - InformationUnit::GibiByte - } +pub trait InformationQuantity { + fn zero() -> Information { + Information::new::(0) } - /// Format the information in the given unit with the given precision. - fn format_with_precision(&self, u: InformationUnit, precision: usize) -> String { - u.format(*self, precision) + fn value_in(&self, u: U) -> u64 + where + U: si::information::Unit + Conversion>; + + fn to_string(&self, u: U) -> String + where + U: si::information::Unit + Conversion>; +} + +impl InformationQuantity for Information { + fn value_in(&self, _u: U) -> u64 + where + U: si::information::Unit + Conversion>, + { + self.get::() } - /// Format the information in the given unit. - fn format(&self, unit: InformationUnit) -> String { - let value = self.format_with_precision(unit, unit.preferred_precision()); - format!("{} {}", value, unit.short_name()) - } - - /// Format the given information. The unit will be determined automatically. - fn format_auto(&self) -> String { - self.format(self.suitable_unit()) - } - - /// Like `format`, but without displaying the unit. - fn format_value(&self, unit: InformationUnit) -> String { - self.format_with_precision(unit, unit.preferred_precision()) + fn to_string(&self, u: U) -> String + where + U: si::information::Unit + Conversion>, + { + format!( + "{}", + self.into_format_args(u, uom::fmt::DisplayStyle::Abbreviation) + ) } } +pub fn serialize_time(t: &Time, s: S) -> Result +where + S: Serializer, +{ + let mut state = s.serialize_struct("Time", 3)?; + state.serialize_field("value", &t.value)?; + state.serialize_field("unit", "second")?; + state.end() +} + +pub fn serialize_information(i: &Information, s: S) -> Result +where + S: Serializer, +{ + let mut state = s.serialize_struct("Information", 3)?; + state.serialize_field("value", &i.value)?; + state.serialize_field("unit", "byte")?; + state.end() +} + +macro_rules! quantity_fn { + ($name:ident, $unwrapped_values:ident, $body:expr) => { + pub fn $name(values: &[Time]) -> Time { + let $unwrapped_values: Vec<_> = values.iter().map(|q| q.value_in(second)).collect(); + let result_value = $body; + + Time::new::(result_value) + } + }; +} + +quantity_fn!(mean, values, statistical::mean(&values)); +quantity_fn!(median, values, statistical::median(&values)); +quantity_fn!(min, values, crate::util::min_max::min(&values)); +quantity_fn!(max, values, crate::util::min_max::max(&values)); +quantity_fn!(standard_deviation, values, { + let mean_value = statistical::mean(&values); + statistical::standard_deviation(&values, Some(mean_value)) +}); + #[test] fn test_time() { let time = Time::new::(123.4); - assert_eq!(time.get::(), 123.4); + assert_eq!(time.value_in(millisecond), 123.4); - let time_s = time.get::(); + let time_s = time.value_in(second); approx::assert_relative_eq!(time_s, 0.1234); - let time_us = time.get::(); + let time_us = time.value_in(microsecond); approx::assert_relative_eq!(time_us, 123400.0); } #[test] fn test_information() { - use uom::si::information::pebibyte; + let information = Information::new::(8); + assert_eq!(information.value_in(byte), 8192); - let information = Information::new::(8.); - assert_eq!(information.get::(), 8192.); - - let information_kib = information.get::(); - assert_eq!(information_kib, 8.); - - let largest_exactly_representable = Information::new::(9_007_199_254_740_992.); - assert_eq!(largest_exactly_representable.get::(), 8.); + let information_kib = information.value_in(kibibyte); + assert_eq!(information_kib, 8); } #[test] -fn test_suiteable_unit_time() { - assert_eq!(Time::new::(1.3).suitable_unit(), TimeUnit::Second); - assert_eq!(Time::new::(1.0).suitable_unit(), TimeUnit::Second); - assert_eq!( - Time::new::(0.999).suitable_unit(), - TimeUnit::MilliSecond - ); - assert_eq!( - Time::new::(0.0005).suitable_unit(), - TimeUnit::MicroSecond - ); - assert_eq!( - Time::new::(0.).suitable_unit(), - TimeUnit::MicroSecond - ); - assert_eq!( - Time::new::(1000.0).suitable_unit(), - TimeUnit::Second - ); +fn test_format() { + let time = Time::new::(123.4); + assert_eq!(time.format_auto(Some(TimeUnit::Second)), "0.123 s"); + assert_eq!(time.format_auto(Some(TimeUnit::MilliSecond)), "123.4 ms"); + assert_eq!(time.format_auto(Some(TimeUnit::MicroSecond)), "123400.0 µs"); + + let peak_memory_usage = Information::new::(8); + assert_eq!(peak_memory_usage.to_string(byte), "8192 B"); + assert_eq!(peak_memory_usage.to_string(kibibyte), "8 KiB"); } +#[test] +fn test_mean() { + let values = vec![ + Time::new::(123.4), + Time::new::(234.5), + ]; + let result = mean(&values); + assert_eq!(result.format_auto(Some(TimeUnit::MilliSecond)), "178.9 ms"); +} + +#[test] +fn test_format_duration() { + let (out_str, out_unit) = Time::new::(1.3).format_auto_with_unit(None); + #[test] fn test_suitable_unit_information() { assert_eq!( @@ -173,31 +234,49 @@ fn test_suitable_unit_information() { ); } -#[test] -fn test_format() { - let time = Time::new::(123.4); - assert_eq!(time.format(TimeUnit::Second), "0.123 s"); - assert_eq!(time.format(TimeUnit::MilliSecond), "123.4 ms"); - assert_eq!(time.format_auto(), "123.4 ms"); - assert_eq!(time.format(TimeUnit::MicroSecond), "123400.0 µs"); + let (out_str, out_unit) = Time::new::(1.0).format_auto_with_unit(None); - let peak_memory_usage = Information::new::(8.); - assert_eq!(peak_memory_usage.format_auto(), "8.0 KiB"); - assert_eq!(peak_memory_usage.format(InformationUnit::Byte), "8192 B"); - assert_eq!( - peak_memory_usage.format(InformationUnit::KibiByte), - "8.0 KiB" - ); + assert_eq!("1.000 s", out_str); + assert_eq!(TimeUnit::Second, out_unit); + + let (out_str, out_unit) = Time::new::(0.999).format_auto_with_unit(None); + + assert_eq!("999.0 ms", out_str); + assert_eq!(TimeUnit::MilliSecond, out_unit); + + let (out_str, out_unit) = Time::new::(0.0005).format_auto_with_unit(None); + + assert_eq!("500.0 µs", out_str); + assert_eq!(TimeUnit::MicroSecond, out_unit); + + let (out_str, out_unit) = Time::new::(0.).format_auto_with_unit(None); + + assert_eq!("0.0 µs", out_str); + assert_eq!(TimeUnit::MicroSecond, out_unit); + + let (out_str, out_unit) = Time::new::(1000.0).format_auto_with_unit(None); + + assert_eq!("1000.000 s", out_str); + assert_eq!(TimeUnit::Second, out_unit); } #[test] fn test_format_duration_unit_with_unit() { - let out = Time::new::(1.3).format(TimeUnit::Second); - assert_eq!("1.300 s", out); + let (out_str, out_unit) = + Time::new::(1.3).format_auto_with_unit(Some(TimeUnit::Second)); let out = Time::new::(1.3).format(TimeUnit::MilliSecond); assert_eq!("1300.0 ms", out); - let out = Time::new::(1.3).format(TimeUnit::MicroSecond); - assert_eq!("1300000.0 µs", out); + let (out_str, out_unit) = + Time::new::(1.3).format_auto_with_unit(Some(TimeUnit::MilliSecond)); + + assert_eq!("1300.0 ms", out_str); + assert_eq!(TimeUnit::MilliSecond, out_unit); + + let (out_str, out_unit) = + Time::new::(1.3).format_auto_with_unit(Some(TimeUnit::MicroSecond)); + + assert_eq!("1300000.0 µs", out_str); + assert_eq!(TimeUnit::MicroSecond, out_unit); } diff --git a/src/quantity/units.rs b/src/quantity/units.rs index 0e457f5..5765ba0 100644 --- a/src/quantity/units.rs +++ b/src/quantity/units.rs @@ -23,6 +23,10 @@ pub trait IsUnit { } } +use std::marker::PhantomData; + +use crate::quantity::{hour, microsecond, millisecond, minute, second, Time}; + /// Supported time units #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeUnit { @@ -33,24 +37,50 @@ pub enum TimeUnit { Hour, } -impl IsUnit for TimeUnit { - type Quantity = Time; +struct Dispatcher> { + u: PhantomData, +} - fn dispatch(&self) -> Box> { +impl> Dispatcher { + fn new() -> Self { + Dispatcher { u: PhantomData } + } +} + +trait UnitImpl { + fn short_name(&self) -> &'static str; + fn format_value(&self, value: Time, precision: usize) -> String; +} + +impl> UnitImpl for Dispatcher { + fn short_name(&self) -> &'static str { + U::abbreviation() + } + + fn format_value(&self, value: Time, precision: usize) -> String { + format!("{value:.precision$}", value = value.get::()) + } +} + +impl TimeUnit { + fn dispatch(self) -> Box { match self { - TimeUnit::MicroSecond => Box::new(TimeUnitDispatcher::::new()), - TimeUnit::MilliSecond => Box::new(TimeUnitDispatcher::::new()), - TimeUnit::Second => Box::new(TimeUnitDispatcher::::new()), - TimeUnit::Minute => Box::new(TimeUnitDispatcher::::new()), - TimeUnit::Hour => Box::new(TimeUnitDispatcher::::new()), + TimeUnit::MicroSecond => Box::new(Dispatcher::::new()), + TimeUnit::MilliSecond => Box::new(Dispatcher::::new()), + TimeUnit::Second => Box::new(Dispatcher::::new()), + TimeUnit::Minute => Box::new(Dispatcher::::new()), + TimeUnit::Hour => Box::new(Dispatcher::::new()), } } - fn preferred_precision(&self) -> usize { - match self { - TimeUnit::Second => 3, - _ => 1, - } + /// A short abbreviation like `s`, `ms`, or `µs`. + pub fn short_name(self) -> &'static str { + self.dispatch().short_name() + } + + /// Formats the quantity as a string in the given Unit. + pub fn format(self, value: Time, precision: usize) -> String { + self.dispatch().format_value(value, precision) } } @@ -147,20 +177,3 @@ fn test_time_unit_short_name() { assert_eq!("min", TimeUnit::Minute.short_name()); assert_eq!("h", TimeUnit::Hour.short_name()); } - -// Note - the values are rounded when formatted. -#[test] -fn test_unit_format() { - use crate::quantity::FormatQuantity; - - let value = Time::new::(123.456789); - assert_eq!("123.457", value.format_value(TimeUnit::Second)); - assert_eq!("123456.8", value.format_value(TimeUnit::MilliSecond)); - - assert_eq!( - "1234.6", - Time::new::(0.00123456).format_value(TimeUnit::MicroSecond) - ); - assert_eq!("2.1", value.format_value(TimeUnit::Minute)); - assert_eq!("0.0", value.format_value(TimeUnit::Hour)); -}