Compare commits

...

11 Commits

Author SHA1 Message Date
Richard
a421a2ab8d
Merge 539995aad1 into 981db9d102 2024-06-13 00:15:04 +00:00
Hamir Mahal
981db9d102 fix: formatting in src/command.rs 2024-06-01 11:07:23 +02:00
Hamir Mahal
ef1263279d style: simplify string interpolation 2024-06-01 11:07:23 +02:00
Richard Montoya
539995aad1 formtating 2023-07-28 19:39:41 -05:00
Richard Montoya
c8bfd59d81 Adds the import-json to the cli options 2023-07-28 19:30:33 -05:00
Richard Montoya
610c8c4a8a Add functionality to load a previous run 2023-07-28 19:26:30 -05:00
Richard Montoya
0033f23006 Make the JSON public for use in import
Modify the JSON export module so it can be used as a reference for
importing and deserializing.
2023-07-28 19:23:38 -05:00
Richard Montoya
b90fa23237 modify types for serialization and deserialization
The existing types need to be modified so they can be used for the
deserialization process.
2023-07-26 23:09:41 -05:00
Richard
6746cf120d
Merge branch 'sharkdp:master' into master 2023-07-25 12:09:28 -05:00
Richard Montoya
1cb474f452 bump the minimum supported rust version. 2023-07-24 15:10:52 -05:00
Richard Montoya
8c145f994d fix: update Cargo.lock to remove proc_macro_span 2023-07-22 00:35:49 -05:00
18 changed files with 98 additions and 50 deletions

View File

@ -1,20 +1,20 @@
use std::collections::BTreeMap;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use crate::util::units::Second;
/// Set of values that will be exported.
// NOTE: `serde` is used for JSON serialization, but not for CSV serialization due to the
// `parameters` map. Update `src/hyperfine/export/csv.rs` with new fields, as appropriate.
#[derive(Debug, Default, Clone, Serialize, PartialEq)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct BenchmarkResult {
/// The full command line of the program that is being benchmarked
pub command: String,
/// The full command line of the program that is being benchmarked, possibly including a list of
/// parameters that were not used in the command line template.
#[serde(skip_serializing)]
#[serde(skip)]
pub command_with_unused_parameters: String,
/// The average run time
@ -46,6 +46,6 @@ pub struct BenchmarkResult {
pub exit_codes: Vec<Option<i32>>,
/// Parameter values for this benchmark
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub parameters: BTreeMap<String, String>,
}

View File

@ -54,7 +54,7 @@ fn run_command_and_measure_common(
);
let result = execute_and_measure(command)
.with_context(|| format!("Failed to run command '{}'", command_name))?;
.with_context(|| format!("Failed to run command '{command_name}'"))?;
if command_failure_action == CmdFailureAction::RaiseError && !result.status.success() {
bail!(
@ -62,7 +62,7 @@ fn run_command_and_measure_common(
Alternatively, use the '--show-output' option to debug what went wrong.",
result.status.code().map_or(
"The process has been terminated by a signal".into(),
|c| format!("Command terminated with non-zero exit code: {}", c)
|c| format!("Command terminated with non-zero exit code: {c}")
)
);
}

View File

@ -312,7 +312,7 @@ impl<'a> Benchmark<'a> {
let (mean_str, time_unit) = format_duration_unit(t_mean, self.options.time_unit);
let min_str = format_duration(t_min, Some(time_unit));
let max_str = format_duration(t_max, Some(time_unit));
let num_str = format!("{} runs", t_num);
let num_str = format!("{t_num} runs");
let user_str = format_duration(user_mean, Some(time_unit));
let system_str = format_duration(system_mean, Some(time_unit));

View File

@ -22,12 +22,13 @@ impl<'a> Scheduler<'a> {
commands: &'a Commands,
options: &'a Options,
export_manager: &'a ExportManager,
results: &'a Vec<BenchmarkResult>,
) -> Self {
Self {
commands,
options,
export_manager,
results: vec![],
results: results.to_vec(),
}
}
@ -69,6 +70,14 @@ impl<'a> Scheduler<'a> {
&self.results,
self.options.sort_order_speed_comparison,
) {
fn get_command_from_result<'b>(result: &'b BenchmarkResult) -> &'b str {
if !result.command_with_unused_parameters.is_empty() {
&result.command_with_unused_parameters
} else {
&result.command
}
}
match self.options.sort_order_speed_comparison {
SortOrder::MeanTime => {
println!("{}", "Summary".bold());
@ -78,7 +87,7 @@ impl<'a> Scheduler<'a> {
println!(
" {} ran",
fastest.result.command_with_unused_parameters.cyan()
(get_command_from_result(&fastest.result)).cyan()
);
for item in others {
@ -86,11 +95,12 @@ impl<'a> Scheduler<'a> {
"{}{} times faster than {}",
format!("{:8.2}", item.relative_speed).bold().green(),
if let Some(stddev) = item.relative_speed_stddev {
format!(" ± {}", format!("{:.2}", stddev).green())
format!(" ± {}", format!("{stddev:.2}").green())
} else {
"".into()
},
&item.result.command_with_unused_parameters.magenta()
// &item.result.command_with_unused_parameters.magenta()
&(get_command_from_result(&item.result)).magenta()
);
}
}
@ -104,11 +114,11 @@ impl<'a> Scheduler<'a> {
if item.is_fastest {
" ".into()
} else if let Some(stddev) = item.relative_speed_stddev {
format!(" ± {}", format!("{:5.2}", stddev).green())
format!(" ± {}", format!("{stddev:5.2}").green())
} else {
" ".into()
},
&item.result.command_with_unused_parameters,
&(get_command_from_result(item.result)),
);
}
}

View File

@ -283,6 +283,13 @@ fn build_command() -> Command {
.help("Export the timing summary statistics and timings of individual runs as JSON to the given FILE. \
The output time unit is always seconds"),
)
.arg(
Arg::new("import-json")
.long("import-json")
.action(ArgAction::Set)
.value_name("FILE")
.help("Import the timing summary statistics and timings of individual runs from a JSON FILE.")
)
.arg(
Arg::new("export-markdown")
.long("export-markdown")

View File

@ -62,13 +62,13 @@ impl<'a> Command<'a> {
let parameters = self
.get_unused_parameters()
.fold(String::new(), |output, (parameter, value)| {
output + &format!("{} = {}, ", parameter, value)
output + &format!("{parameter} = {value}, ")
});
let parameters = parameters.trim_end_matches(", ");
let parameters = if parameters.is_empty() {
"".into()
} else {
format!(" ({})", parameters)
format!(" ({parameters})")
};
format!("{}{}", self.get_name(), parameters)
@ -81,7 +81,7 @@ impl<'a> Command<'a> {
pub fn get_command(&self) -> Result<std::process::Command> {
let command_line = self.get_command_line();
let mut tokens = shell_words::split(&command_line)
.with_context(|| format!("Failed to parse command '{}'", command_line))?
.with_context(|| format!("Failed to parse command '{command_line}'"))?
.into_iter();
if let Some(program_name) = tokens.next() {
@ -100,17 +100,14 @@ impl<'a> Command<'a> {
pub fn get_unused_parameters(&self) -> impl Iterator<Item = &(&'a str, ParameterValue)> {
self.parameters
.iter()
.filter(move |(parameter, _)| !self.expression.contains(&format!("{{{}}}", parameter)))
.filter(move |(parameter, _)| !self.expression.contains(&format!("{{{parameter}}}")))
}
fn replace_parameters_in(&self, original: &str) -> String {
let mut result = String::new();
let mut replacements = BTreeMap::<String, String>::new();
for (param_name, param_value) in &self.parameters {
replacements.insert(
format!("{{{param_name}}}", param_name = param_name),
param_value.to_string(),
);
replacements.insert(format!("{{{param_name}}}"), param_value.to_string());
}
let mut remaining = original;
// Manually replace consecutive occurrences to avoid double-replacing: e.g.,

View File

@ -32,7 +32,7 @@ impl MarkupExporter for AsciidocExporter {
}
fn command(&self, cmd: &str) -> String {
format!("`{}`", cmd)
format!("`{cmd}`")
}
}
@ -71,8 +71,7 @@ fn test_asciidoc_exporter_table_header() {
#[cfg(test)]
fn cfg_test_table_header(unit_short_name: &str) -> String {
format!(
"[cols=\"<,>,>,>,>\"]\n|===\n| Command \n| Mean [{unit}] \n| Min [{unit}] \n| Max [{unit}] \n| Relative \n",
unit = unit_short_name
"[cols=\"<,>,>,>,>\"]\n|===\n| Command \n| Mean [{unit_short_name}] \n| Min [{unit_short_name}] \n| Max [{unit_short_name}] \n| Relative \n"
)
}

View File

@ -31,7 +31,7 @@ impl Exporter for CsvExporter {
.collect();
if let Some(res) = results.first() {
for param_name in res.parameters.keys() {
headers.push(Cow::Owned(format!("parameter_{}", param_name).into_bytes()));
headers.push(Cow::Owned(format!("parameter_{param_name}").into_bytes()));
}
}
writer.write_record(headers)?;

View File

@ -8,9 +8,9 @@ use crate::util::units::Unit;
use anyhow::Result;
#[derive(Serialize, Debug)]
struct HyperfineSummary<'a> {
results: &'a [BenchmarkResult],
#[derive(Serialize, Deserialize, Debug)]
pub struct HyperfineSummary {
pub results: Vec<BenchmarkResult>,
}
#[derive(Default)]
@ -23,7 +23,9 @@ impl Exporter for JsonExporter {
_unit: Option<Unit>,
_sort_order: SortOrder,
) -> Result<Vec<u8>> {
let mut output = to_vec_pretty(&HyperfineSummary { results });
let mut output = to_vec_pretty(&HyperfineSummary {
results: results.to_vec(),
});
if let Ok(ref mut content) = output {
content.push(b'\n');
}

View File

@ -24,7 +24,7 @@ impl MarkupExporter for MarkdownExporter {
}
fn command(&self, cmd: &str) -> String {
format!("`{}`", cmd)
format!("`{cmd}`")
}
}
@ -53,8 +53,7 @@ fn test_markdown_formatter_table_divider() {
#[cfg(test)]
fn cfg_test_table_header(unit_short_name: String) -> String {
format!(
"| Command | Mean [{unit}] | Min [{unit}] | Max [{unit}] | Relative |\n|:---|---:|---:|---:|---:|\n",
unit = unit_short_name
"| Command | Mean [{unit_short_name}] | Min [{unit_short_name}] | Max [{unit_short_name}] | Relative |\n|:---|---:|---:|---:|---:|\n"
)
}

View File

@ -32,9 +32,9 @@ pub trait MarkupExporter {
// emit table header data
table.push_str(&self.table_row(&[
"Command",
&format!("Mean {}", notation),
&format!("Min {}", notation),
&format!("Max {}", notation),
&format!("Mean {notation}"),
&format!("Min {notation}"),
&format!("Max {notation}"),
"Relative",
]));
@ -59,7 +59,7 @@ pub trait MarkupExporter {
let rel_stddev_str = if entry.is_fastest {
"".into()
} else if let Some(stddev) = entry.relative_speed_stddev {
format!(" ± {:.2}", stddev)
format!(" ± {stddev:.2}")
} else {
"".into()
};
@ -67,10 +67,10 @@ pub trait MarkupExporter {
// prepare table row entries
table.push_str(&self.table_row(&[
&self.command(&cmd_str),
&format!("{}{}", mean_str, stddev_str),
&format!("{mean_str}{stddev_str}"),
&min_str,
&max_str,
&format!("{}{}", rel_str, rel_stddev_str),
&format!("{rel_str}{rel_stddev_str}"),
]))
}

View File

@ -3,7 +3,7 @@ use std::io::Write;
mod asciidoc;
mod csv;
mod json;
pub mod json;
mod markdown;
mod markup;
mod orgmode;
@ -108,7 +108,7 @@ impl ExportManager {
ExportTarget::Stdout
} else {
let _ = File::create(filename)
.with_context(|| format!("Could not create export file '{}'", filename))?;
.with_context(|| format!("Could not create export file '{filename}'"))?;
ExportTarget::File(filename.to_string())
},
});
@ -153,5 +153,5 @@ impl ExportManager {
fn write_to_file(filename: &str, content: &[u8]) -> Result<()> {
let mut file = OpenOptions::new().write(true).open(filename)?;
file.write_all(content)
.with_context(|| format!("Failed to export results to '{}'", filename))
.with_context(|| format!("Failed to export results to '{filename}'"))
}

View File

@ -18,7 +18,7 @@ impl MarkupExporter for OrgmodeExporter {
}
fn command(&self, cmd: &str) -> String {
format!("={}=", cmd)
format!("={cmd}=")
}
}
@ -58,8 +58,7 @@ fn test_orgmode_formatter_table_line() {
#[cfg(test)]
fn cfg_test_table_header(unit_short_name: String) -> String {
format!(
"| Command | Mean [{unit}] | Min [{unit}] | Max [{unit}] | Relative |\n|--+--+--+--+--|\n",
unit = unit_short_name
"| Command | Mean [{unit_short_name}] | Min [{unit_short_name}] | Max [{unit_short_name}] | Relative |\n|--+--+--+--+--|\n"
)
}

31
src/import.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::{benchmark::benchmark_result::BenchmarkResult, export::json::HyperfineSummary};
use clap::ArgMatches;
use std::fs;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Importer {}
impl Importer {
pub fn from_cli_arguments(matches: &ArgMatches) -> Option<Vec<BenchmarkResult>> {
match matches.get_one::<String>("import-json") {
Some(file_name) => read_summary_from_file(file_name),
None => None,
}
}
}
fn read_summary_from_file(file_name: &str) -> Option<Vec<BenchmarkResult>> {
let file_content = match fs::read_to_string(file_name) {
Ok(content) => content,
Err(_) => {
eprintln!("Unable to load previous run from file {}", file_name);
return None;
}
};
let hyperfine_summary = serde_json::from_str::<HyperfineSummary>(&file_content);
match hyperfine_summary {
Ok(summary) => Some(summary.results),
Err(_) => None,
}
}

View File

@ -9,6 +9,7 @@ use benchmark::scheduler::Scheduler;
use cli::get_cli_arguments;
use command::Commands;
use export::ExportManager;
use import::Importer;
use options::Options;
use anyhow::Result;
@ -19,6 +20,7 @@ pub mod cli;
pub mod command;
pub mod error;
pub mod export;
pub mod import;
pub mod options;
pub mod outlier_detection;
pub mod output;
@ -32,13 +34,15 @@ fn run() -> Result<()> {
colored::control::set_virtual_terminal(true).unwrap();
let cli_arguments = get_cli_arguments(env::args_os());
let previous_results = Importer::from_cli_arguments(&cli_arguments).unwrap_or(Vec::new());
let options = Options::from_cli_arguments(&cli_arguments)?;
let commands = Commands::from_cli_arguments(&cli_arguments)?;
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, options.time_unit)?;
options.validate_against_command_list(&commands)?;
let mut scheduler = Scheduler::new(&commands, &options, &export_manager);
let mut scheduler = Scheduler::new(&commands, &options, &export_manager, &previous_results);
scheduler.run_benchmarks()?;
scheduler.print_relative_speed_comparison();
scheduler.final_export()?;

View File

@ -38,7 +38,7 @@ impl Default for Shell {
impl fmt::Display for Shell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Shell::Default(cmd) => write!(f, "{}", cmd),
Shell::Default(cmd) => write!(f, "{cmd}"),
Shell::Custom(cmdline) => write!(f, "{}", shell_words::join(cmdline)),
}
}
@ -457,7 +457,7 @@ impl Options {
fn test_default_shell() {
let shell = Shell::default();
let s = format!("{}", shell);
let s = format!("{shell}");
assert_eq!(&s, DEFAULT_SHELL);
let cmd = shell.command();
@ -468,7 +468,7 @@ fn test_default_shell() {
fn test_can_parse_shell_command_line_from_str() {
let shell = Shell::parse_from_str("shell -x 'aaa bbb'").unwrap();
let s = format!("{}", shell);
let s = format!("{shell}");
assert_eq!(&s, "shell -x 'aaa bbb'");
let cmd = shell.command();

View File

@ -16,7 +16,7 @@ impl Display for ParameterValue {
ParameterValue::Text(ref value) => value.clone(),
ParameterValue::Numeric(value) => value.to_string(),
};
write!(f, "{}", str)
write!(f, "{str}")
}
}

View File

@ -26,7 +26,7 @@ impl Unit {
/// Returns the Second value formatted for the Unit.
pub fn format(self, value: Second) -> String {
match self {
Unit::Second => format!("{:.3}", value),
Unit::Second => format!("{value:.3}"),
Unit::MilliSecond => format!("{:.1}", value * 1e3),
Unit::MicroSecond => format!("{:.1}", value * 1e6),
}