From 6f557e0494cd083228d880bf91e81301ad3f8cb5 Mon Sep 17 00:00:00 2001 From: David Peter Date: Mon, 5 Oct 2026 11:46:49 +0200 Subject: [PATCH] Extract typed quantity formatting --- src/benchmark/mod.rs | 29 +++--- src/export/markup.rs | 12 +-- src/main.rs | 2 +- src/output/warnings.rs | 8 +- src/quantity/mod.rs | 222 ++++++++++++++++++++++++++--------------- src/quantity/units.rs | 160 ++++++++++++++++++++++++----- 6 files changed, 304 insertions(+), 129 deletions(-) diff --git a/src/benchmark/mod.rs b/src/benchmark/mod.rs index 18a861a..a45f9a0 100644 --- a/src/benchmark/mod.rs +++ b/src/benchmark/mod.rs @@ -17,9 +17,7 @@ use crate::output::console_writeln; use crate::output::progress_bar::get_progress_bar; use crate::output::warnings::{OutlierWarningOptions, Warnings}; use crate::parameter::ParameterNameAndValue; -use crate::quantity::{ - byte, const_time_from_seconds, format_duration, format_duration_unit, ratio, second, Time, Zero, -}; +use crate::quantity::{byte, const_time_from_seconds, ratio, second, FormatQuantity, Time, Zero}; use crate::util::exit_code::extract_exit_code; use benchmark_result::BenchmarkResult; use measurement::{Measurement, Measurements}; @@ -294,10 +292,12 @@ impl<'a> Benchmark<'a> { run_preparation_command()?; let msg = { - let mean = format_duration( - measurements.time_wall_clock_mean().get::(), - self.options.time_unit, - ); + let t_wall_clock_mean = measurements.time_wall_clock_mean(); + let time_unit = self + .options + .time_unit + .unwrap_or(t_wall_clock_mean.suitable_unit()); + let mean = t_wall_clock_mean.format(time_unit); format!("Current estimate: {}", mean.to_string().green()) }; @@ -340,14 +340,14 @@ impl<'a> Benchmark<'a> { let system_mean = measurements.time_system_mean(); // Formatting and console output - let (mean_str, time_unit) = - format_duration_unit(t_mean.get::(), self.options.time_unit); - let min_str = format_duration(t_min.get::(), Some(time_unit)); - let max_str = format_duration(t_max.get::(), Some(time_unit)); + let time_unit = self.options.time_unit.unwrap_or(t_mean.suitable_unit()); + let mean_str = t_mean.format(time_unit); + let min_str = t_min.format(time_unit); + let max_str = t_max.format(time_unit); let num_str = format!("{t_num} runs"); - let user_str = format_duration(user_mean.get::(), Some(time_unit)); - let system_str = format_duration(system_mean.get::(), Some(time_unit)); + let user_str = user_mean.format(time_unit); + let system_str = system_mean.format(time_unit); if self.options.output_style != OutputStyleOption::Disabled { let mut stdout = io::stdout().lock(); @@ -362,8 +362,7 @@ impl<'a> Benchmark<'a> { system_str.blue() )?; } else { - let stddev_str = - format_duration(t_stddev.unwrap().get::(), Some(time_unit)); + let stddev_str = t_stddev.unwrap().format(time_unit); console_writeln!( stdout, diff --git a/src/export/markup.rs b/src/export/markup.rs index f63bb80..e14ffe4 100644 --- a/src/export/markup.rs +++ b/src/export/markup.rs @@ -1,7 +1,7 @@ use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed; use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed}; use crate::options::SortOrder; -use crate::quantity::{format_duration_value, TimeUnit}; +use crate::quantity::{second, FormatQuantity, IsUnit, Time, TimeUnit}; use super::Exporter; use anyhow::Result; @@ -50,14 +50,14 @@ pub trait MarkupExporter { let cmd_str = measurement .command_with_unused_parameters .replace('|', "\\|"); - let mean_str = format_duration_value(measurement.mean, Some(unit)).0; + let mean_str = Time::new::(measurement.mean).format_value(unit); let stddev_str = if let Some(stddev) = measurement.stddev { - format!(" ± {}", format_duration_value(stddev, Some(unit)).0) + format!(" ± {}", Time::new::(stddev).format_value(unit)) } else { "".into() }; - let min_str = format_duration_value(measurement.min, Some(unit)).0; - let max_str = format_duration_value(measurement.max, Some(unit)).0; + let min_str = Time::new::(measurement.min).format_value(unit); + let max_str = Time::new::(measurement.max).format_value(unit); let rel_str = format!("{:.2}", entry.relative_speed); let rel_stddev_str = if entry.is_reference { "".into() @@ -101,7 +101,7 @@ 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, None).1 + Time::new::(first_result.mean).suitable_unit() } else { // Default to `Second`. TimeUnit::Second diff --git a/src/main.rs b/src/main.rs index b846790..806136e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ pub mod options; pub mod outlier_detection; pub mod output; pub mod parameter; -mod quantity; +pub mod quantity; pub mod timer; pub mod util; diff --git a/src/output/warnings.rs b/src/output/warnings.rs index 5ae7794..98ebdb7 100644 --- a/src/output/warnings.rs +++ b/src/output/warnings.rs @@ -1,7 +1,7 @@ use std::fmt; use crate::benchmark::MIN_EXECUTION_TIME; -use crate::quantity::{format_duration, second, Time}; +use crate::quantity::{FormatQuantity, Time, TimeUnit}; pub struct OutlierWarningOptions { pub warmup_in_use: bool, @@ -21,11 +21,11 @@ impl fmt::Display for Warnings { match *self { Warnings::FastExecutionTime => write!( f, - "Command took less than {:.0} ms to complete. Note that the results might be \ + "Command took less than {} ms to complete. Note that the results might be \ 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.get::() * 1e3 + MIN_EXECUTION_TIME.format_with_precision(TimeUnit::MilliSecond, 0) ), Warnings::NonZeroExitCode => write!(f, "Ignoring non-zero exit code."), Warnings::SlowInitialRun(time_first_run, ref options) => write!( @@ -33,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=format_duration(time_first_run.get::(), None), + time=time_first_run.format_auto(), 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 a2eae03..5844474 100644 --- a/src/quantity/mod.rs +++ b/src/quantity/mod.rs @@ -1,20 +1,64 @@ use std::marker::PhantomData; -pub use uom::num_traits::Zero; -pub use uom::si::f64::{Information, Ratio, Time}; -pub use uom::si::information::byte; -#[cfg(any(not(windows), test))] -pub use uom::si::information::kibibyte; -pub use uom::si::ratio::ratio; -#[cfg(any(not(windows), test))] -pub use uom::si::time::microsecond; -pub use uom::si::time::{nanosecond, second}; +use uom::si; -pub use units::TimeUnit; +pub use si::f64::{Information, Ratio, Time}; +pub use si::information::{byte, gibibyte, kibibyte, mebibyte, tebibyte}; +pub use si::ratio::ratio; +pub use si::time::{hour, microsecond, millisecond, minute, nanosecond, second}; +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 + } + } + + /// Format the time duration in the given unit with the given precision. + fn format_with_precision(&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 time duration. 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: TimeUnit) -> String { + self.format_with_precision(unit, unit.preferred_precision()) + } +} + pub const fn const_time_from_seconds(value: f64) -> Time { // Quantity::new in uom is not yet const: https://docs.rs/uom/0.36.0/uom/si/struct.Quantity.html Time { @@ -24,41 +68,43 @@ pub const fn const_time_from_seconds(value: f64) -> Time { } } -/// 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. -pub fn format_duration(duration: f64, unit: Option) -> String { - let (duration_fmt, _) = format_duration_unit(duration, unit); - duration_fmt -} +impl FormatQuantity for Information { + type Unit = InformationUnit; -/// Like `format_duration`, but returns the target unit as well. -pub fn format_duration_unit(duration: f64, unit: Option) -> (String, TimeUnit) { - let (out_str, out_unit) = format_duration_value(duration, unit); + 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 + } + } - (format!("{} {}", out_str, out_unit.short_name()), out_unit) -} + /// 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) + } -/// Like `format_duration`, but returns the target unit as well. -pub fn format_duration_value(duration: f64, unit: Option) -> (String, TimeUnit) { - if (duration < 0.001 && unit.is_none()) || unit == Some(TimeUnit::MicroSecond) { - ( - TimeUnit::MicroSecond.format(duration), - TimeUnit::MicroSecond, - ) - } else if (duration < 1.0 && unit.is_none()) || unit == Some(TimeUnit::MilliSecond) { - ( - TimeUnit::MilliSecond.format(duration), - TimeUnit::MilliSecond, - ) - } else { - let unit = unit.unwrap_or(TimeUnit::Second); - (unit.format(duration), unit) + /// 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()) } } -#[cfg(test)] -use uom::si::time::millisecond; - #[test] fn test_time() { let time = Time::new::(123.4); @@ -86,52 +132,72 @@ fn test_information() { } #[test] -fn test_format_duration_unit_basic() { - let (out_str, out_unit) = format_duration_unit(1.3, None); +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 + ); +} - assert_eq!("1.300 s", out_str); - assert_eq!(TimeUnit::Second, out_unit); +#[test] +fn test_suitable_unit_information() { + assert_eq!( + Information::new::(512.0).suitable_unit(), + InformationUnit::Byte + ); + assert_eq!( + Information::new::(2048.0).suitable_unit(), + InformationUnit::KibiByte + ); + assert_eq!( + Information::new::(2_097_152.0).suitable_unit(), + InformationUnit::MebiByte + ); + assert_eq!( + Information::new::(2_147_483_648.0).suitable_unit(), + InformationUnit::GibiByte + ); +} - let (out_str, out_unit) = format_duration_unit(1.0, None); +#[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"); - assert_eq!("1.000 s", out_str); - assert_eq!(TimeUnit::Second, out_unit); - - let (out_str, out_unit) = format_duration_unit(0.999, None); - - assert_eq!("999.0 ms", out_str); - assert_eq!(TimeUnit::MilliSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(0.0005, None); - - assert_eq!("500.0 µs", out_str); - assert_eq!(TimeUnit::MicroSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(0.0, None); - - assert_eq!("0.0 µs", out_str); - assert_eq!(TimeUnit::MicroSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(1000.0, None); - - assert_eq!("1000.000 s", out_str); - assert_eq!(TimeUnit::Second, out_unit); + 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" + ); } #[test] fn test_format_duration_unit_with_unit() { - let (out_str, out_unit) = format_duration_unit(1.3, Some(TimeUnit::Second)); + let out = Time::new::(1.3).format(TimeUnit::Second); + assert_eq!("1.300 s", out); - assert_eq!("1.300 s", out_str); - assert_eq!(TimeUnit::Second, out_unit); + let out = Time::new::(1.3).format(TimeUnit::MilliSecond); + assert_eq!("1300.0 ms", out); - let (out_str, out_unit) = format_duration_unit(1.3, Some(TimeUnit::MilliSecond)); - - assert_eq!("1300.0 ms", out_str); - assert_eq!(TimeUnit::MilliSecond, out_unit); - - let (out_str, out_unit) = format_duration_unit(1.3, Some(TimeUnit::MicroSecond)); - - assert_eq!("1300000.0 µs", out_str); - assert_eq!(TimeUnit::MicroSecond, out_unit); + let out = Time::new::(1.3).format(TimeUnit::MicroSecond); + assert_eq!("1300000.0 µs", out); } diff --git a/src/quantity/units.rs b/src/quantity/units.rs index 9a7ad35..0e457f5 100644 --- a/src/quantity/units.rs +++ b/src/quantity/units.rs @@ -1,41 +1,146 @@ -//! This module contains common units. +//! This module contains common units for time and information quantities. + +use std::marker::PhantomData; + +use crate::quantity::{ + byte, gibibyte, hour, kibibyte, mebibyte, microsecond, millisecond, minute, second, tebibyte, + Information, Time, +}; + +pub trait IsUnit { + type Quantity; + + fn dispatch(&self) -> Box>; + + fn preferred_precision(&self) -> usize; + + fn short_name(&self) -> &'static str { + self.dispatch().short_name() + } + + fn format(&self, value: Self::Quantity, precision: usize) -> String { + self.dispatch().format_value(value, precision) + } +} /// Supported time units #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TimeUnit { - Second, - MilliSecond, MicroSecond, + MilliSecond, + Second, Minute, Hour, } -impl TimeUnit { - /// The abbreviation of the TimeUnit. - pub fn short_name(self) -> String { +impl IsUnit for TimeUnit { + type Quantity = Time; + + fn dispatch(&self) -> Box> { match self { - TimeUnit::Second => String::from("s"), - TimeUnit::MilliSecond => String::from("ms"), - TimeUnit::MicroSecond => String::from("µs"), - TimeUnit::Minute => String::from("min"), - TimeUnit::Hour => String::from("h"), + 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()), } } - /// Returns the value in seconds formatted for the TimeUnit. - pub fn format(self, value: f64) -> String { + fn preferred_precision(&self) -> usize { match self { - TimeUnit::Second => format!("{value:.3}"), - TimeUnit::MilliSecond => format!("{:.1}", value * 1e3), - TimeUnit::MicroSecond => format!("{:.1}", value * 1e6), - TimeUnit::Minute => format!("{:.1}", value / 60.0), - TimeUnit::Hour => format!("{:.1}", value / 3600.0), + TimeUnit::Second => 3, + _ => 1, } } } +/// Supported information units +#[derive(Debug, Clone, Copy, PartialEq, Eq)] + +pub enum InformationUnit { + Byte, + KibiByte, + MebiByte, + GibiByte, + TebiByte, +} + +impl IsUnit for InformationUnit { + type Quantity = Information; + + fn dispatch(&self) -> Box> { + match self { + InformationUnit::Byte => Box::new(InformationUnitDispatcher::::new()), + InformationUnit::KibiByte => Box::new(InformationUnitDispatcher::::new()), + InformationUnit::MebiByte => Box::new(InformationUnitDispatcher::::new()), + InformationUnit::GibiByte => Box::new(InformationUnitDispatcher::::new()), + InformationUnit::TebiByte => Box::new(InformationUnitDispatcher::::new()), + } + } + + fn preferred_precision(&self) -> usize { + match self { + InformationUnit::Byte => 0, + _ => 1, + } + } +} + +pub trait UnitImpl { + type Quantity; + + fn short_name(&self) -> &'static str; + fn format_value(&self, value: Self::Quantity, precision: usize) -> String; +} + +struct TimeUnitDispatcher> { + u: PhantomData, +} + +impl> TimeUnitDispatcher { + fn new() -> Self { + TimeUnitDispatcher { u: PhantomData } + } +} + +impl> UnitImpl for TimeUnitDispatcher { + type Quantity = Time; + + fn short_name(&self) -> &'static str { + U::abbreviation() + } + + fn format_value(&self, value: Time, precision: usize) -> String { + format!("{value:.precision$}", value = value.get::()) + } +} + +struct InformationUnitDispatcher> { + u: PhantomData, +} + +impl> InformationUnitDispatcher { + fn new() -> Self { + InformationUnitDispatcher { u: PhantomData } + } +} + +impl> UnitImpl + for InformationUnitDispatcher +{ + type Quantity = Information; + + fn short_name(&self) -> &'static str { + U::abbreviation() + } + + fn format_value(&self, value: Information, precision: usize) -> String { + format!("{value:.precision$}", value = value.get::()) + } +} + #[test] -fn test_unit_short_name() { +fn test_time_unit_short_name() { assert_eq!("s", TimeUnit::Second.short_name()); assert_eq!("ms", TimeUnit::MilliSecond.short_name()); assert_eq!("µs", TimeUnit::MicroSecond.short_name()); @@ -46,11 +151,16 @@ fn test_unit_short_name() { // Note - the values are rounded when formatted. #[test] fn test_unit_format() { - let value: f64 = 123.456789; - assert_eq!("123.457", TimeUnit::Second.format(value)); - assert_eq!("123456.8", TimeUnit::MilliSecond.format(value)); + use crate::quantity::FormatQuantity; - assert_eq!("1234.6", TimeUnit::MicroSecond.format(0.00123456)); - assert_eq!("2.1", TimeUnit::Minute.format(value)); - assert_eq!("0.0", TimeUnit::Hour.format(value)); + 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)); }