diff --git a/Cargo.lock b/Cargo.lock index d416c89..d92135e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,7 @@ dependencies = [ "csv 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "indicatif 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.62 (registry+https://github.com/rust-lang/crates.io-index)", + "rust_decimal 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.40 (registry+https://github.com/rust-lang/crates.io-index)", "statistical 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -516,6 +517,16 @@ name = "rgb" version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rust_decimal" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", + "num 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.99 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -757,6 +768,7 @@ dependencies = [ "checksum regex-automata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ed09217220c272b29ef237a974ad58515bde75f194e3ffa7e6d0bf0f3b01f86" "checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96" "checksum rgb 0.8.13 (registry+https://github.com/rust-lang/crates.io-index)" = "4f089652ca87f5a82a62935ec6172a534066c7b97be003cc8f702ee9a7a59c92" +"checksum rust_decimal 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f7a28ded8f10361cefb69a8d8e1d195acf59344150534c165c401d6611cf013d" "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" "checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" diff --git a/Cargo.toml b/Cargo.toml index eeb8f35..0dab2d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ cfg-if = "0.1.9" csv = "1.1.1" serde = { version = "1.0.99", features = ["derive"] } serde_json = "1.0.40" +rust_decimal = "1.0" [target.'cfg(not(windows))'.dependencies] libc = "0.2" diff --git a/src/hyperfine/error.rs b/src/hyperfine/error.rs index cc0df70..95060fd 100644 --- a/src/hyperfine/error.rs +++ b/src/hyperfine/error.rs @@ -1,3 +1,4 @@ +use rust_decimal::Error as DecimalError; use std::error::Error; use std::fmt; use std::num; @@ -5,9 +6,11 @@ use std::num; #[derive(Debug)] pub enum ParameterScanError { ParseIntError(num::ParseIntError), + ParseDecimalError(DecimalError), EmptyRange, TooLarge, ZeroStep, + StepRequired, } impl From for ParameterScanError { @@ -16,6 +19,12 @@ impl From for ParameterScanError { } } +impl From for ParameterScanError { + fn from(e: DecimalError) -> ParameterScanError { + ParameterScanError::ParseDecimalError(e) + } +} + impl fmt::Display for ParameterScanError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.description()) @@ -26,9 +35,11 @@ impl Error for ParameterScanError { fn description(&self) -> &str { match *self { ParameterScanError::ParseIntError(ref e) => e.description(), + ParameterScanError::ParseDecimalError(ref e) => e.description(), ParameterScanError::EmptyRange => "Empty parameter range", ParameterScanError::TooLarge => "Parameter range is too large", ParameterScanError::ZeroStep => "Zero is not a valid parameter step", + ParameterScanError::StepRequired => "Step is required when range bounds are floats", } } } diff --git a/src/hyperfine/mod.rs b/src/hyperfine/mod.rs index e6af195..ceafc92 100644 --- a/src/hyperfine/mod.rs +++ b/src/hyperfine/mod.rs @@ -5,6 +5,7 @@ pub mod export; pub mod format; pub mod internal; pub mod outlier_detection; +pub mod parameter_range; pub mod shell; pub mod timer; pub mod types; diff --git a/src/hyperfine/parameter_range.rs b/src/hyperfine/parameter_range.rs new file mode 100644 index 0000000..e7d4cbd --- /dev/null +++ b/src/hyperfine/parameter_range.rs @@ -0,0 +1,176 @@ +use crate::hyperfine::error::ParameterScanError; +use crate::hyperfine::types::{Command, NumericType}; +use clap::Values; +use rust_decimal::Decimal; +use std::ops::{Add, AddAssign, Div, Sub}; +use std::str::FromStr; + +trait Numeric: + Add + + Sub + + Div + + AddAssign + + PartialOrd + + Copy + + Clone + + From + + Into +{ +} +impl< + T: Add + + Sub + + Div + + AddAssign + + PartialOrd + + Copy + + Clone + + From + + Into, + > Numeric for T +{ +} + +struct RangeStep { + state: T, + end: T, + step: T, +} + +impl RangeStep { + fn new(start: T, end: T, step: T) -> RangeStep { + RangeStep { + state: start, + end, + step, + } + } +} + +impl Iterator for RangeStep { + type Item = T; + + fn next(&mut self) -> Option { + if self.state > self.end { + return None; + } + let return_val = self.state; + self.state += self.step; + + Some(return_val) + } +} + +fn validate_params(start: T, end: T, step: T) -> Result<(), ParameterScanError> { + if end < start { + return Err(ParameterScanError::EmptyRange); + } + + if step == T::from(0) { + return Err(ParameterScanError::ZeroStep); + } + + const MAX_PARAMETERS: i32 = 100_000; + let steps = (end - start + T::from(1)) / step; + if steps > T::from(MAX_PARAMETERS) { + return Err(ParameterScanError::TooLarge); + } + + Ok(()) +} + +fn build_parameterized_commands<'a, T: Numeric>( + param_min: T, + param_max: T, + step: T, + command_strings: Vec<&'a str>, + param_name: &'a str, +) -> Result>, ParameterScanError> { + validate_params(param_min, param_max, step)?; + let param_range = RangeStep::new(param_min, param_max, step); + let mut commands = vec![]; + + for value in param_range { + for cmd in &command_strings { + commands.push(Command::new_parametrized(cmd, param_name, value.into())); + } + } + Ok(commands) +} + +pub fn get_parameterized_commands<'a>( + command_strings: Values<'a>, + mut vals: clap::Values<'a>, + step: Option<&str>, +) -> Result>, ParameterScanError> { + let command_strings = command_strings.collect::>(); + let param_name = vals.next().unwrap(); + let param_min = vals.next().unwrap(); + let param_max = vals.next().unwrap(); + + // attempt to parse as integers + if let (Ok(param_min), Ok(param_max)) = (param_min.parse::(), param_max.parse::()) { + let step = step.unwrap_or("1").parse::()?; + return build_parameterized_commands( + param_min, + param_max, + step, + command_strings, + param_name, + ); + } + + // try parsing them as decimals + let param_min = Decimal::from_str(param_min)?; + let param_max = Decimal::from_str(param_max)?; + + if step.is_none() { + return Err(ParameterScanError::StepRequired); + } + + let step = Decimal::from_str(step.unwrap())?; + build_parameterized_commands(param_min, param_max, step, command_strings, param_name) +} + +#[test] +fn test_integer_range() { + let param_range: Vec = RangeStep::new(0, 10, 3).collect(); + + assert_eq!(param_range.len(), 4); + assert_eq!(param_range[0], 0); + assert_eq!(param_range[3], 9); +} + +#[test] +fn test_decimal_range() { + let param_min = Decimal::from(0); + let param_max = Decimal::from(1); + let step = Decimal::from_str("0.1").unwrap(); + + let param_range: Vec = RangeStep::new(param_min, param_max, step).collect(); + + assert_eq!(param_range.len(), 11); + assert_eq!(param_range[0], Decimal::from(0)); + assert_eq!(param_range[10], Decimal::from(1)); +} + +#[test] +fn test_get_parameterized_commands_int() { + let commands = + build_parameterized_commands(1i32, 7i32, 3i32, vec!["echo {val}"], "val").unwrap(); + assert_eq!(commands.len(), 3); + assert_eq!(commands[2].get_shell_command(), "echo 7"); +} + +#[test] +fn test_get_parameterized_commands_decimal() { + let param_min = Decimal::from_str("0").unwrap(); + let param_max = Decimal::from_str("1").unwrap(); + let step = Decimal::from_str("0.33").unwrap(); + + let commands = + build_parameterized_commands(param_min, param_max, step, vec!["echo {val}"], "val") + .unwrap(); + assert_eq!(commands.len(), 4); + assert_eq!(commands[3].get_shell_command(), "echo 0.99"); +} diff --git a/src/hyperfine/types.rs b/src/hyperfine/types.rs index b300b78..d7d0038 100644 --- a/src/hyperfine/types.rs +++ b/src/hyperfine/types.rs @@ -1,3 +1,4 @@ +use rust_decimal::Decimal; /// This module contains common internal types. use serde::*; use std::fmt; @@ -10,6 +11,34 @@ pub const DEFAULT_SHELL: &str = "sh"; #[cfg(windows)] pub const DEFAULT_SHELL: &str = "cmd.exe"; +#[derive(Debug, Clone, Serialize, Copy)] +#[serde(untagged)] +pub enum NumericType { + Int(i32), + Decimal(Decimal), +} + +impl fmt::Display for NumericType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + NumericType::Int(i) => fmt::Display::fmt(&i, f), + NumericType::Decimal(i) => fmt::Display::fmt(&i, f), + } + } +} + +impl Into for i32 { + fn into(self) -> NumericType { + NumericType::Int(self) + } +} + +impl Into for Decimal { + fn into(self) -> NumericType { + NumericType::Decimal(self) + } +} + /// A command that should be benchmarked. #[derive(Debug, Clone)] pub struct Command<'a> { @@ -17,7 +46,7 @@ pub struct Command<'a> { expression: &'a str, /// A possible parameter value. - parameter: Option<(&'a str, i32)>, + parameter: Option<(&'a str, NumericType)>, } impl<'a> Command<'a> { @@ -28,7 +57,11 @@ impl<'a> Command<'a> { } } - pub fn new_parametrized(expression: &'a str, parameter: &'a str, value: i32) -> Command<'a> { + pub fn new_parametrized( + expression: &'a str, + parameter: &'a str, + value: NumericType, + ) -> Command<'a> { Command { expression, parameter: Some((parameter, value)), @@ -45,7 +78,7 @@ impl<'a> Command<'a> { } } - pub fn get_parameter(&self) -> Option<(&'a str, i32)> { + pub fn get_parameter(&self) -> Option<(&'a str, NumericType)> { self.parameter } } @@ -180,7 +213,7 @@ pub struct BenchmarkResult { /// Any parameter used #[serde(skip_serializing_if = "Option::is_none")] - pub parameter: Option, + pub parameter: Option, } impl BenchmarkResult { @@ -195,7 +228,7 @@ impl BenchmarkResult { min: Second, max: Second, times: Vec, - parameter: Option, + parameter: Option, ) -> Self { BenchmarkResult { command, diff --git a/src/main.rs b/src/main.rs index 6dd4b1b..8c520bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,8 +2,6 @@ use std::cmp; use std::env; use std::error::Error; use std::io; -use std::iter::StepBy; -use std::ops::Range; use atty::Stream; use clap::ArgMatches; @@ -13,9 +11,10 @@ mod hyperfine; use crate::hyperfine::app::get_arg_matches; use crate::hyperfine::benchmark::{mean_shell_spawning_time, run_benchmark}; -use crate::hyperfine::error::{OptionsError, ParameterScanError}; +use crate::hyperfine::error::OptionsError; use crate::hyperfine::export::{ExportManager, ExportType}; use crate::hyperfine::internal::write_benchmark_comparison; +use crate::hyperfine::parameter_range::get_parameterized_commands; use crate::hyperfine::types::{ BenchmarkResult, CmdFailureAction, Command, HyperfineOptions, OutputStyleOption, }; @@ -42,35 +41,6 @@ fn run(commands: &[Command<'_>], options: &HyperfineOptions) -> io::Result( - mut vals: clap::Values<'a>, - step_size: &str, -) -> Result<(&'a str, StepBy>), ParameterScanError> { - let param_name = vals.next().unwrap(); - let param_min: i32 = vals.next().unwrap().parse()?; - let param_max: i32 = vals.next().unwrap().parse()?; - let step_size = step_size.parse()?; - - const MAX_PARAMETERS: i32 = 100_000; - if param_max - param_min > MAX_PARAMETERS { - return Err(ParameterScanError::TooLarge); - } - - if param_max < param_min { - return Err(ParameterScanError::EmptyRange); - } - - if step_size == 0 { - // Iterator::step_by panics for zero step - return Err(ParameterScanError::ZeroStep); - } - - let param_range = (param_min..(param_max + 1)).step_by(step_size); - - Ok((param_name, param_range)) -} - fn main() { let matches = get_arg_matches(env::args_os()); let options = build_hyperfine_options(&matches); @@ -216,18 +186,9 @@ fn build_commands<'a>(matches: &'a ArgMatches<'_>) -> Vec> { let command_strings = matches.values_of("command").unwrap(); if let Some(args) = matches.values_of("parameter-scan") { - let step_size = matches.value_of("parameter-step-size").unwrap_or("1"); - match parse_parameter_scan_args(args, step_size) { - Ok((param_name, param_range)) => { - let mut commands = vec![]; - let command_strings = command_strings.collect::>(); - for value in param_range { - for cmd in &command_strings { - commands.push(Command::new_parametrized(cmd, param_name, value)); - } - } - commands - } + let step_size = matches.value_of("parameter-step-size"); + match get_parameterized_commands(command_strings, args, step_size) { + Ok(commands) => commands, Err(e) => error(e.description()), } } else {