mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-11-01 08:50:19 +00:00
Add support for floats in --parameter-scan
This commit is contained in:
1 parent
737403df35
commit
b10b93f473
7 files changed
+244
-49
No files matched your search
@@ -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<num::ParseIntError> for ParameterScanError {
|
||||
@@ -16,6 +19,12 @@ impl From<num::ParseIntError> for ParameterScanError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecimalError> 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Output = Self>
|
||||
+ Sub<Output = Self>
|
||||
+ Div<Output = Self>
|
||||
+ AddAssign
|
||||
+ PartialOrd
|
||||
+ Copy
|
||||
+ Clone
|
||||
+ From<i32>
|
||||
+ Into<NumericType>
|
||||
{
|
||||
}
|
||||
impl<
|
||||
T: Add<Output = Self>
|
||||
+ Sub<Output = Self>
|
||||
+ Div<Output = Self>
|
||||
+ AddAssign
|
||||
+ PartialOrd
|
||||
+ Copy
|
||||
+ Clone
|
||||
+ From<i32>
|
||||
+ Into<NumericType>,
|
||||
> Numeric for T
|
||||
{
|
||||
}
|
||||
|
||||
struct RangeStep<T> {
|
||||
state: T,
|
||||
end: T,
|
||||
step: T,
|
||||
}
|
||||
|
||||
impl<T: Numeric> RangeStep<T> {
|
||||
fn new(start: T, end: T, step: T) -> RangeStep<T> {
|
||||
RangeStep {
|
||||
state: start,
|
||||
end,
|
||||
step,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Numeric> Iterator for RangeStep<T> {
|
||||
type Item = T;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.state > self.end {
|
||||
return None;
|
||||
}
|
||||
let return_val = self.state;
|
||||
self.state += self.step;
|
||||
|
||||
Some(return_val)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_params<T: Numeric>(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<Vec<Command<'a>>, 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<Vec<Command<'a>>, ParameterScanError> {
|
||||
let command_strings = command_strings.collect::<Vec<&str>>();
|
||||
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::<i32>(), param_max.parse::<i32>()) {
|
||||
let step = step.unwrap_or("1").parse::<i32>()?;
|
||||
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<i32> = 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<Decimal> = 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");
|
||||
}
|
||||
+38
-5
@@ -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<NumericType> for i32 {
|
||||
fn into(self) -> NumericType {
|
||||
NumericType::Int(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<NumericType> 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<i32>,
|
||||
pub parameter: Option<NumericType>,
|
||||
}
|
||||
|
||||
impl BenchmarkResult {
|
||||
@@ -195,7 +228,7 @@ impl BenchmarkResult {
|
||||
min: Second,
|
||||
max: Second,
|
||||
times: Vec<Second>,
|
||||
parameter: Option<i32>,
|
||||
parameter: Option<NumericType>,
|
||||
) -> Self {
|
||||
BenchmarkResult {
|
||||
command,
|
||||
|
||||
+5
-44
@@ -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<Vec<B
|
||||
Ok(timing_results)
|
||||
}
|
||||
|
||||
/// A function to read the `--parameter-scan` arguments
|
||||
fn parse_parameter_scan_args<'a>(
|
||||
mut vals: clap::Values<'a>,
|
||||
step_size: &str,
|
||||
) -> Result<(&'a str, StepBy<Range<i32>>), 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<Command<'a>> {
|
||||
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::<Vec<&str>>();
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user