Add support for floats in --parameter-scan

This commit is contained in:
Piyush Rungta 2019-08-31 12:48:26 +05:30 committed by David Peter
parent 737403df35
commit b10b93f473
7 changed files with 244 additions and 49 deletions

12
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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",
}
}
}

View File

@ -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;

View File

@ -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");
}

View File

@ -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,

View File

@ -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 {