mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-11-01 08:50:19 +00:00
Add support for showing other metrics in the terminal
This commit is contained in:
1 parent
7395dde05d
commit
908b33cc1d
31 files changed
+1514
-1898
No files matched your search
@@ -24,7 +24,7 @@ impl MarkupExporter for AsciidocExporter {
|
||||
}
|
||||
|
||||
fn table_row(&self, cells: &[&str]) -> String {
|
||||
format!("\n| {} \n", cells.join(" \n| "))
|
||||
format!("\n| {}\n", cells.join("\n| "))
|
||||
}
|
||||
|
||||
fn table_divider(&self, _cell_aligmnents: &[Alignment]) -> String {
|
||||
@@ -43,7 +43,7 @@ fn test_asciidoc_exporter_table_data() {
|
||||
let data = vec!["a", "b", "c"];
|
||||
|
||||
let actual = exporter.table_row(&data);
|
||||
let expect = "\n| a \n| b \n| c \n";
|
||||
let expect = "\n| a\n| b\n| c\n";
|
||||
|
||||
assert_eq!(expect, actual);
|
||||
}
|
||||
|
||||
+75
-53
@@ -1,65 +1,73 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use csv::WriterBuilder;
|
||||
|
||||
use super::Exporter;
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::options::SortOrder;
|
||||
use crate::quantity::{FormatQuantity, TimeUnit};
|
||||
use crate::metric::MetricSelection;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CsvExporter {}
|
||||
|
||||
impl Exporter for CsvExporter {
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
_time_unit: Option<TimeUnit>,
|
||||
_sort_order: SortOrder,
|
||||
_reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
const CSV_UNIT: TimeUnit = TimeUnit::Second;
|
||||
const CSV_PRECISION: usize = 6;
|
||||
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
|
||||
// CSV never chooses units automatically: an omitted suffix means base units.
|
||||
let unit = primary.csv_unit();
|
||||
let mut writer = WriterBuilder::new().from_writer(vec![]);
|
||||
|
||||
{
|
||||
let mut headers: Vec<Cow<[u8]>> = [
|
||||
// The list of times and exit codes cannot be exported to the CSV file - omit them.
|
||||
"command", "mean", "stddev", "median", "user", "system", "min", "max",
|
||||
]
|
||||
.iter()
|
||||
.map(|x| Cow::Borrowed(x.as_bytes()))
|
||||
.collect();
|
||||
if let Some(res) = results.first() {
|
||||
for param_name in res.parameters.keys() {
|
||||
headers.push(Cow::Owned(format!("parameter_{param_name}").into_bytes()));
|
||||
}
|
||||
}
|
||||
writer.write_record(headers)?;
|
||||
let mut headers: Vec<String> = [
|
||||
"command", "metric", "unit", "mean", "stddev", "median", "min", "max",
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
if let Some(result) = results.first() {
|
||||
headers.extend(
|
||||
result
|
||||
.parameters
|
||||
.keys()
|
||||
.map(|name| format!("parameter_{name}")),
|
||||
);
|
||||
}
|
||||
writer.write_record(headers)?;
|
||||
|
||||
for res in results {
|
||||
let mut fields = vec![Cow::Borrowed(res.get_name().as_bytes())];
|
||||
for f in &[
|
||||
res.mean_wall_clock_time(),
|
||||
res.measurements.stddev().unwrap_or_default(),
|
||||
res.measurements.median(),
|
||||
res.measurements.time_user_mean(),
|
||||
res.measurements.time_system_mean(),
|
||||
res.measurements.min(),
|
||||
res.measurements.max(),
|
||||
] {
|
||||
fields.push(Cow::Owned(
|
||||
f.format_with_precision(CSV_UNIT, CSV_PRECISION)
|
||||
.into_bytes(),
|
||||
))
|
||||
}
|
||||
for v in res.parameters.values() {
|
||||
fields.push(Cow::Borrowed(v.value.as_bytes()))
|
||||
}
|
||||
for result in results {
|
||||
let stats = primary
|
||||
.metric
|
||||
.summarize(&result.measurements)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Metric '{}' is unavailable for '{}'",
|
||||
primary.metric.name(),
|
||||
result.get_name()
|
||||
)
|
||||
})?;
|
||||
let mut fields = vec![
|
||||
result.get_name().to_owned(),
|
||||
primary.metric.name().to_owned(),
|
||||
unit.symbol.to_owned(),
|
||||
];
|
||||
// Preserve numeric precision rather than using rounded display values.
|
||||
fields.extend(
|
||||
[
|
||||
Some(stats.mean),
|
||||
stats.stddev,
|
||||
Some(stats.median),
|
||||
Some(stats.min),
|
||||
Some(stats.max),
|
||||
]
|
||||
.map(|value| {
|
||||
value
|
||||
.map(|value| (value / unit.scale).to_string())
|
||||
.unwrap_or_default()
|
||||
}),
|
||||
);
|
||||
fields.extend(
|
||||
result
|
||||
.parameters
|
||||
.values()
|
||||
.map(|parameter| parameter.value.clone()),
|
||||
);
|
||||
writer.write_record(fields)?;
|
||||
}
|
||||
|
||||
@@ -177,14 +185,28 @@ fn test_csv() {
|
||||
|
||||
let actual = String::from_utf8(
|
||||
exporter
|
||||
.serialize(&results, Some(TimeUnit::Second), SortOrder::Command, None)
|
||||
.serialize(&results, MetricSelection::default())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
insta::assert_snapshot!(actual, @r#"
|
||||
command,mean,stddev,median,user,system,min,max,parameter_bar,parameter_foo
|
||||
command_a,9.000000,2.645751,8.000000,9.000000,0.000000,7.000000,12.000000,two,one
|
||||
command_b,18.000000,1.000000,18.000000,18.000000,0.000000,17.000000,19.000000,seven,one
|
||||
command,metric,unit,mean,stddev,median,min,max,parameter_bar,parameter_foo
|
||||
command_a,time_wall_clock,s,9,2.6457513110645907,8,7,12,two,one
|
||||
command_b,time_wall_clock,s,18,1,18,17,19,seven,one
|
||||
"#);
|
||||
|
||||
for (selection, unit, mean) in [
|
||||
("memory_peak_resident", "B", "1024"),
|
||||
("memory_peak_resident:KiB", "KiB", "1"),
|
||||
] {
|
||||
let output = exporter
|
||||
.serialize(&results, MetricSelection::parse_list(selection).unwrap()[0])
|
||||
.unwrap();
|
||||
let mut reader = csv::Reader::from_reader(output.as_slice());
|
||||
let row = reader.records().next().unwrap().unwrap();
|
||||
assert_eq!(&row[1], "memory_peak_resident");
|
||||
assert_eq!(&row[2], unit);
|
||||
assert_eq!(&row[3], mean);
|
||||
}
|
||||
}
|
||||
+33
-178
@@ -1,14 +1,11 @@
|
||||
use std::ops::{Add, AddAssign, Div};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::*;
|
||||
use serde::Serialize;
|
||||
use serde_json::to_vec_pretty;
|
||||
|
||||
use super::Exporter;
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::benchmark::measurement::Measurements;
|
||||
use crate::options::SortOrder;
|
||||
use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue};
|
||||
use crate::quantity::{byte, ratio, second, QuantityInUnit, Ratio, TimeUnit, Zero};
|
||||
use crate::metric::{Metric, MetricSelection, Stats};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -21,6 +18,7 @@ const JSON_SCHEMA_VERSION: u32 = 2;
|
||||
struct HyperfineSummary<'a> {
|
||||
schema_version: u32,
|
||||
metadata: &'a Metadata,
|
||||
primary_metric: &'static str,
|
||||
results: Vec<JsonBenchmarkResult<'a>>,
|
||||
}
|
||||
|
||||
@@ -28,98 +26,7 @@ struct HyperfineSummary<'a> {
|
||||
struct JsonBenchmarkResult<'a> {
|
||||
#[serde(flatten)]
|
||||
result: &'a BenchmarkResult,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
is_reference: bool,
|
||||
summary: BenchmarkSummary,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BenchmarkSummary {
|
||||
time_wall_clock: StatisticalSummary,
|
||||
time_user: StatisticalSummary,
|
||||
time_system: StatisticalSummary,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
memory_peak_resident: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cpu_cycles: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_references: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_misses: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
branch_misses: Option<StatisticalSummary>,
|
||||
}
|
||||
|
||||
impl BenchmarkSummary {
|
||||
fn from_measurements(measurements: &Measurements) -> Self {
|
||||
let values = &measurements.measurements;
|
||||
Self {
|
||||
time_wall_clock: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| Some(m.time_wall_clock)),
|
||||
Some(second),
|
||||
)
|
||||
.expect("Benchmarks have at least one measurement"),
|
||||
time_user: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| Some(m.time_user)),
|
||||
Some(second),
|
||||
)
|
||||
.expect("Benchmarks have at least one measurement"),
|
||||
time_system: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| Some(m.time_system)),
|
||||
Some(second),
|
||||
)
|
||||
.expect("Benchmarks have at least one measurement"),
|
||||
memory_peak_resident: StatisticalSummary::from_values(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|m| m.memory_peak_resident)
|
||||
.map(Some),
|
||||
Some(byte),
|
||||
),
|
||||
cpu_cycles: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.cpu_cycles
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
instructions: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.instructions
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
cache_references: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.cache_references
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
cache_misses: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.cache_misses
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
branch_misses: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.branch_misses
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
}
|
||||
}
|
||||
summary: BTreeMap<&'static str, StatisticalSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -135,37 +42,20 @@ struct StatisticalSummary {
|
||||
}
|
||||
|
||||
impl StatisticalSummary {
|
||||
fn from_values<Q, P, U>(
|
||||
values: impl IntoIterator<Item = Option<Q>>,
|
||||
unit: Option<U>,
|
||||
) -> Option<Self>
|
||||
where
|
||||
Q: Copy
|
||||
+ PartialOrd
|
||||
+ Add<Output = Q>
|
||||
+ AddAssign
|
||||
+ Zero
|
||||
+ Div<Ratio, Output = P>
|
||||
+ UnsafeRawValue
|
||||
+ QuantityInUnit<U>,
|
||||
P: Into<Q>,
|
||||
U: uom::si::Unit,
|
||||
{
|
||||
// Only summarize a metric when it is available for every supplied measurement.
|
||||
let values: Vec<_> = values.into_iter().collect::<Option<_>>()?;
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
fn new(metric: Metric, stats: Stats) -> Self {
|
||||
Self {
|
||||
unit: match metric.base_unit().symbol {
|
||||
"s" => Some("second"),
|
||||
"B" => Some("byte"),
|
||||
_ => None,
|
||||
},
|
||||
count: stats.count,
|
||||
mean: stats.mean,
|
||||
stddev: stats.stddev,
|
||||
median: stats.median,
|
||||
min: stats.min,
|
||||
max: stats.max,
|
||||
}
|
||||
Some(Self {
|
||||
unit: unit.map(|_| U::singular()),
|
||||
count: values.len(),
|
||||
mean: mean(values.iter().copied()).value_in_unit(),
|
||||
stddev: (values.len() > 1)
|
||||
.then(|| standard_deviation(values.iter().copied()).value_in_unit()),
|
||||
median: median(values.iter().copied()).value_in_unit(),
|
||||
min: min(values.iter().copied()).value_in_unit(),
|
||||
max: max(values.iter().copied()).value_in_unit(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,63 +65,28 @@ pub struct JsonExporter {
|
||||
}
|
||||
|
||||
impl Exporter for JsonExporter {
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
_time_unit: Option<TimeUnit>,
|
||||
_sort_order: SortOrder,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
|
||||
let mut output = to_vec_pretty(&HyperfineSummary {
|
||||
schema_version: JSON_SCHEMA_VERSION,
|
||||
metadata: &self.metadata,
|
||||
primary_metric: primary.metric.name(),
|
||||
results: results
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, result)| JsonBenchmarkResult {
|
||||
.map(|result| JsonBenchmarkResult {
|
||||
result,
|
||||
is_reference: reference_index == Some(index),
|
||||
summary: BenchmarkSummary::from_measurements(&result.measurements),
|
||||
summary: Metric::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|metric| {
|
||||
metric.summarize(&result.measurements).map(|stats| {
|
||||
(metric.name(), StatisticalSummary::new(metric, stats))
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
if let Ok(ref mut content) = output {
|
||||
content.push(b'\n');
|
||||
}
|
||||
|
||||
Ok(output?)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_statistical_summary_unit_conversion() {
|
||||
use crate::quantity::{kibibyte, millisecond, Information, Time};
|
||||
|
||||
let time = StatisticalSummary::from_values(
|
||||
[Time::new::<second>(1.0), Time::new::<second>(3.0)].map(Some),
|
||||
Some(millisecond),
|
||||
)
|
||||
.unwrap();
|
||||
let information = StatisticalSummary::from_values(
|
||||
[
|
||||
Information::new::<byte>(1024.0),
|
||||
Information::new::<byte>(3072.0),
|
||||
]
|
||||
.map(Some),
|
||||
Some(kibibyte),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for (summary, unit, scale) in [
|
||||
(time, "millisecond", 1000.0),
|
||||
(information, "kibibyte", 1.0),
|
||||
] {
|
||||
assert_eq!(summary.unit, Some(unit));
|
||||
assert_eq!(summary.count, 2);
|
||||
approx::assert_relative_eq!(summary.mean, 2.0 * scale);
|
||||
approx::assert_relative_eq!(summary.median, 2.0 * scale);
|
||||
approx::assert_relative_eq!(summary.min, scale);
|
||||
approx::assert_relative_eq!(summary.max, 3.0 * scale);
|
||||
approx::assert_relative_eq!(summary.stddev.unwrap(), 2.0_f64.sqrt() * scale);
|
||||
})?;
|
||||
output.push(b'\n');
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
+57
-99
@@ -1,11 +1,8 @@
|
||||
use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed;
|
||||
use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed};
|
||||
use crate::options::SortOrder;
|
||||
use crate::quantity::{FormatQuantity, IsUnit, Time, TimeUnit, Zero};
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::metric::{MetricSelection, Stats, Unit};
|
||||
|
||||
use super::Exporter;
|
||||
use anyhow::Result;
|
||||
use std::cmp::Ordering;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
@@ -15,15 +12,12 @@ pub enum Alignment {
|
||||
pub trait MarkupExporter {
|
||||
fn table_results(
|
||||
&self,
|
||||
entries: &[BenchmarkResultWithRelativeSpeed],
|
||||
time_unit: TimeUnit,
|
||||
reference_pending: bool,
|
||||
explicit_reference: bool,
|
||||
results: &[BenchmarkResult],
|
||||
summaries: &[Stats],
|
||||
primary: MetricSelection,
|
||||
unit: Unit,
|
||||
) -> String {
|
||||
// prepare table header strings
|
||||
let notation = format!("[{}]", time_unit.short_name());
|
||||
|
||||
// prepare table cells alignment
|
||||
let notation = format!("[{}]", unit.symbol);
|
||||
let cells_alignment = [
|
||||
Alignment::Left,
|
||||
Alignment::Right,
|
||||
@@ -31,116 +25,80 @@ pub trait MarkupExporter {
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
];
|
||||
|
||||
// emit table header format
|
||||
let mut table = self.table_header(&cells_alignment);
|
||||
|
||||
// emit table header data
|
||||
table.push_str(&self.table_row(&[
|
||||
"Command",
|
||||
&format!("Mean {notation}"),
|
||||
&format!("Mean {} {notation}", primary.metric.label()),
|
||||
&format!("Min {notation}"),
|
||||
&format!("Max {notation}"),
|
||||
"Relative",
|
||||
"Change",
|
||||
]));
|
||||
|
||||
// emit horizontal line
|
||||
table.push_str(&self.table_divider(&cells_alignment));
|
||||
|
||||
for entry in entries {
|
||||
let result = &entry.result;
|
||||
// prepare data row strings
|
||||
let cmd_str = result.display_name.replace('|', "\\|");
|
||||
let mean_str = result.mean_wall_clock_time().format_value(time_unit);
|
||||
let stddev_str = if let Some(stddev) = result.measurements.stddev() {
|
||||
format!(" ± {}", stddev.format_value(time_unit))
|
||||
for (index, (result, stats)) in results.iter().zip(summaries).enumerate() {
|
||||
let command = result.display_name.replace('|', "\\|");
|
||||
let mean = unit.format_value(stats.mean);
|
||||
let stddev = stats
|
||||
.stddev
|
||||
.map(|value| format!(" ± {}", unit.format_value(value)))
|
||||
.unwrap_or_default();
|
||||
let change = if index == 0 {
|
||||
"reference".to_owned()
|
||||
} else {
|
||||
"".into()
|
||||
stats.format_change_from(&summaries[0])
|
||||
};
|
||||
let min_str = result.measurements.min().format_value(time_unit);
|
||||
let max_str = result.measurements.max().format_value(time_unit);
|
||||
// The ratio of two zero times is undefined, even if they compare equal.
|
||||
let relative_unavailable = reference_pending
|
||||
|| (explicit_reference
|
||||
&& !entry.is_reference
|
||||
&& entry.relative_ordering == Ordering::Equal
|
||||
&& result.mean_wall_clock_time() == Time::zero());
|
||||
let relative = if relative_unavailable {
|
||||
"N/A".to_string()
|
||||
} else {
|
||||
let stddev = match (entry.is_reference, entry.relative_speed_stddev) {
|
||||
(false, Some(stddev)) => format!(" ± {stddev:.2}"),
|
||||
_ => String::new(),
|
||||
};
|
||||
let label = if explicit_reference {
|
||||
entry.reference_label()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{:.2}{stddev}{label}", entry.relative_speed)
|
||||
};
|
||||
|
||||
// prepare table row entries
|
||||
table.push_str(&self.table_row(&[
|
||||
&self.command(&cmd_str),
|
||||
&format!("{mean_str}{stddev_str}"),
|
||||
&min_str,
|
||||
&max_str,
|
||||
&relative,
|
||||
]))
|
||||
&self.command(&command),
|
||||
&format!("{mean}{stddev}"),
|
||||
&unit.format_value(stats.min),
|
||||
&unit.format_value(stats.max),
|
||||
&change,
|
||||
]));
|
||||
}
|
||||
|
||||
// emit table footer format
|
||||
table.push_str(&self.table_footer(&cells_alignment));
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
fn table_row(&self, cells: &[&str]) -> String;
|
||||
|
||||
fn table_divider(&self, cell_aligmnents: &[Alignment]) -> String;
|
||||
fn table_divider(&self, cell_alignments: &[Alignment]) -> String;
|
||||
|
||||
fn table_header(&self, _cell_aligmnents: &[Alignment]) -> String {
|
||||
"".to_string()
|
||||
fn table_header(&self, _cell_alignments: &[Alignment]) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn table_footer(&self, _cell_aligmnents: &[Alignment]) -> String {
|
||||
"".to_string()
|
||||
fn table_footer(&self, _cell_alignments: &[Alignment]) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn command(&self, size: &str) -> String;
|
||||
}
|
||||
|
||||
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.
|
||||
first_result.mean_wall_clock_time().suitable_unit()
|
||||
} else {
|
||||
// Default to `Second`.
|
||||
TimeUnit::Second
|
||||
}
|
||||
fn command(&self, command: &str) -> String;
|
||||
}
|
||||
|
||||
impl<T: MarkupExporter> Exporter for T {
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let unit = time_unit.unwrap_or_else(|| determine_unit_from_results(results));
|
||||
// Do not report ratios against another benchmark while the selected
|
||||
// reference is still pending in an intermediate export.
|
||||
let reference_pending = reference_index.is_some_and(|i| i >= results.len());
|
||||
let entries = relative_speed::compute(
|
||||
results,
|
||||
sort_order,
|
||||
reference_index.and_then(|i| results.get(i)),
|
||||
);
|
||||
|
||||
let table =
|
||||
self.table_results(&entries, unit, reference_pending, reference_index.is_some());
|
||||
Ok(table.as_bytes().to_vec())
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
|
||||
let stats: Vec<_> = results
|
||||
.iter()
|
||||
.map(|result| {
|
||||
primary
|
||||
.metric
|
||||
.summarize(&result.measurements)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Metric '{}' is unavailable for '{}'",
|
||||
primary.metric.name(),
|
||||
result.get_name()
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
// Use one common unit, avoiding an unscaled column if the reference is zero.
|
||||
let mean = stats
|
||||
.iter()
|
||||
.find(|stats| stats.mean != 0.0)
|
||||
.map_or(0.0, |stats| stats.mean);
|
||||
let unit = primary.display_unit(mean);
|
||||
Ok(self
|
||||
.table_results(results, &stats, primary, unit)
|
||||
.into_bytes())
|
||||
}
|
||||
}
|
||||
+7
-30
@@ -17,9 +17,8 @@ use self::markdown::MarkdownExporter;
|
||||
use self::orgmode::OrgmodeExporter;
|
||||
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::options::SortOrder;
|
||||
use crate::metric::MetricSelection;
|
||||
use crate::output::console_writeln;
|
||||
use crate::quantity::TimeUnit;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::ArgMatches;
|
||||
@@ -46,13 +45,7 @@ pub enum ExportType {
|
||||
/// Interface for different exporters.
|
||||
trait Exporter {
|
||||
/// Export the given entries in the serialized form.
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>>;
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub enum ExportTarget {
|
||||
@@ -68,22 +61,16 @@ struct ExporterWithTarget {
|
||||
/// Handles the management of multiple file exporters.
|
||||
pub struct ExportManager {
|
||||
exporters: Vec<ExporterWithTarget>,
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
primary: MetricSelection,
|
||||
}
|
||||
|
||||
impl ExportManager {
|
||||
/// Build the ExportManager that will export the results specified
|
||||
/// in the given ArgMatches
|
||||
pub fn from_cli_arguments(
|
||||
matches: &ArgMatches,
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
) -> Result<Self> {
|
||||
pub fn from_cli_arguments(matches: &ArgMatches, primary: MetricSelection) -> Result<Self> {
|
||||
let mut export_manager = Self {
|
||||
exporters: vec![],
|
||||
time_unit,
|
||||
sort_order,
|
||||
primary,
|
||||
};
|
||||
{
|
||||
let mut add_exporter = |flag, exporttype| -> Result<()> {
|
||||
@@ -131,19 +118,9 @@ impl ExportManager {
|
||||
/// results are written to all file targets (to always have them up to date, even
|
||||
/// if a benchmark fails). In the latter case, we only print to stdout targets (in
|
||||
/// order not to clutter the output of hyperfine with intermediate results).
|
||||
/// `reference_index` refers to the complete benchmark sequence, so it can
|
||||
/// point past `results` while that benchmark is still pending.
|
||||
pub fn write_results(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
intermediate: bool,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<()> {
|
||||
pub fn write_results(&self, results: &[BenchmarkResult], intermediate: bool) -> Result<()> {
|
||||
for e in &self.exporters {
|
||||
let content = || {
|
||||
e.exporter
|
||||
.serialize(results, self.time_unit, self.sort_order, reference_index)
|
||||
};
|
||||
let content = || e.exporter.serialize(results, self.primary);
|
||||
|
||||
match e.target {
|
||||
ExportTarget::File(ref filename) => {
|
||||
|
||||
+108
-107
@@ -2,19 +2,27 @@ use super::Exporter;
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::benchmark::measurement::{Measurement, Measurements};
|
||||
use crate::export::asciidoc::AsciidocExporter;
|
||||
use crate::export::markdown::MarkdownExporter;
|
||||
use crate::export::orgmode::OrgmodeExporter;
|
||||
use crate::quantity::{byte, second, Information, Time, TimeUnit, Zero};
|
||||
use crate::{export::markdown::MarkdownExporter, options::SortOrder};
|
||||
use crate::metric::{MetricSelection, Unit};
|
||||
use crate::quantity::{byte, second, Information, Time, Zero};
|
||||
use std::collections::BTreeMap;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
fn get_output<E: Exporter + Default>(
|
||||
results: &[BenchmarkResult],
|
||||
unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
) -> String {
|
||||
fn get_output<E: Exporter + Default>(results: &[BenchmarkResult], unit: Option<Unit>) -> String {
|
||||
let exporter = E::default();
|
||||
String::from_utf8(exporter.serialize(results, unit, sort_order, None).unwrap()).unwrap()
|
||||
String::from_utf8(
|
||||
exporter
|
||||
.serialize(
|
||||
results,
|
||||
MetricSelection {
|
||||
unit,
|
||||
..MetricSelection::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Ensure the makrup output includes the table header and the multiple
|
||||
@@ -92,41 +100,41 @@ fn test_markup_export_auto_ms() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None), @r#"
|
||||
| Command | Mean Time [ms] | Min [ms] | Max [ms] | Change |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 0.1` | 110.0 ± 26.5 | 90.0 | 140.0 | 1.00 |
|
||||
| `sleep 2` | 3000.0 ± 1000.0 | 2000.0 | 4000.0 | 27.27 ± 11.21 |
|
||||
| `sleep 0.1` | 110.0 ± 26.5 | 90.0 | 140.0 | reference |
|
||||
| `sleep 2` | 3000.0 ± 1000.0 | 2000.0 | 4000.0 | +2627.3% |
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None, SortOrder::Command), @r#"
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [ms]
|
||||
| Min [ms]
|
||||
| Max [ms]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Time [ms]
|
||||
| Min [ms]
|
||||
| Max [ms]
|
||||
| Change
|
||||
|
||||
| `sleep 0.1`
|
||||
| 110.0 ± 26.5
|
||||
| 90.0
|
||||
| 140.0
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 110.0 ± 26.5
|
||||
| 90.0
|
||||
| 140.0
|
||||
| reference
|
||||
|
||||
| `sleep 2`
|
||||
| 3000.0 ± 1000.0
|
||||
| 2000.0
|
||||
| 4000.0
|
||||
| 27.27 ± 11.21
|
||||
| `sleep 2`
|
||||
| 3000.0 ± 1000.0
|
||||
| 2000.0
|
||||
| 4000.0
|
||||
| +2627.3%
|
||||
|===
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None), @r#"
|
||||
| Command | Mean Time [ms] | Min [ms] | Max [ms] | Change |
|
||||
|--+--+--+--+--|
|
||||
| =sleep 0.1= | 110.0 ± 26.5 | 90.0 | 140.0 | 1.00 |
|
||||
| =sleep 2= | 3000.0 ± 1000.0 | 2000.0 | 4000.0 | 27.27 ± 11.21 |
|
||||
| =sleep 0.1= | 110.0 ± 26.5 | 90.0 | 140.0 | reference |
|
||||
| =sleep 2= | 3000.0 ± 1000.0 | 2000.0 | 4000.0 | +2627.3% |
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -201,41 +209,41 @@ fn test_markup_export_auto_s() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None), @r#"
|
||||
| Command | Mean Time [s] | Min [s] | Max [s] | Change |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 2` | 2.200 ± 0.100 | 2.100 | 2.300 | 11.00 ± 5.52 |
|
||||
| `sleep 0.1` | 0.200 ± 0.100 | 0.100 | 0.300 | 1.00 |
|
||||
| `sleep 2` | 2.200 ± 0.100 | 2.100 | 2.300 | reference |
|
||||
| `sleep 0.1` | 0.200 ± 0.100 | 0.100 | 0.300 | -90.9% |
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None, SortOrder::Command), @r#"
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [s]
|
||||
| Min [s]
|
||||
| Max [s]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Time [s]
|
||||
| Min [s]
|
||||
| Max [s]
|
||||
| Change
|
||||
|
||||
| `sleep 2`
|
||||
| 2.200 ± 0.100
|
||||
| 2.100
|
||||
| 2.300
|
||||
| 11.00 ± 5.52
|
||||
| `sleep 2`
|
||||
| 2.200 ± 0.100
|
||||
| 2.100
|
||||
| 2.300
|
||||
| reference
|
||||
|
||||
| `sleep 0.1`
|
||||
| 0.200 ± 0.100
|
||||
| 0.100
|
||||
| 0.300
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 0.200 ± 0.100
|
||||
| 0.100
|
||||
| 0.300
|
||||
| -90.9%
|
||||
|===
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None), @r#"
|
||||
| Command | Mean Time [s] | Min [s] | Max [s] | Change |
|
||||
|--+--+--+--+--|
|
||||
| =sleep 2= | 2.200 ± 0.100 | 2.100 | 2.300 | 11.00 ± 5.52 |
|
||||
| =sleep 0.1= | 0.200 ± 0.100 | 0.100 | 0.300 | 1.00 |
|
||||
| =sleep 2= | 2.200 ± 0.100 | 2.100 | 2.300 | reference |
|
||||
| =sleep 0.1= | 0.200 ± 0.100 | 0.100 | 0.300 | -90.9% |
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -310,41 +318,41 @@ fn test_markup_export_manual_ms() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&timing_results, Some(Unit { symbol: "ms", scale: 0.001 })), @r#"
|
||||
| Command | Mean Time [ms] | Min [ms] | Max [ms] | Change |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 2` | 2200.0 ± 100.0 | 2100.0 | 2300.0 | 11.00 ± 5.52 |
|
||||
| `sleep 0.1` | 200.0 ± 100.0 | 100.0 | 300.0 | 1.00 |
|
||||
| `sleep 2` | 2200.0 ± 100.0 | 2100.0 | 2300.0 | reference |
|
||||
| `sleep 0.1` | 200.0 ± 100.0 | 100.0 | 300.0 | -90.9% |
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#"
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&timing_results, Some(Unit { symbol: "ms", scale: 0.001 })), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [ms]
|
||||
| Min [ms]
|
||||
| Max [ms]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Time [ms]
|
||||
| Min [ms]
|
||||
| Max [ms]
|
||||
| Change
|
||||
|
||||
| `sleep 2`
|
||||
| 2200.0 ± 100.0
|
||||
| 2100.0
|
||||
| 2300.0
|
||||
| 11.00 ± 5.52
|
||||
| `sleep 2`
|
||||
| 2200.0 ± 100.0
|
||||
| 2100.0
|
||||
| 2300.0
|
||||
| reference
|
||||
|
||||
| `sleep 0.1`
|
||||
| 200.0 ± 100.0
|
||||
| 100.0
|
||||
| 300.0
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 200.0 ± 100.0
|
||||
| 100.0
|
||||
| 300.0
|
||||
| -90.9%
|
||||
|===
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&timing_results, Some(Unit { symbol: "ms", scale: 0.001 })), @r#"
|
||||
| Command | Mean Time [ms] | Min [ms] | Max [ms] | Change |
|
||||
|--+--+--+--+--|
|
||||
| =sleep 2= | 2200.0 ± 100.0 | 2100.0 | 2300.0 | 11.00 ± 5.52 |
|
||||
| =sleep 0.1= | 200.0 ± 100.0 | 100.0 | 300.0 | 1.00 |
|
||||
| =sleep 2= | 2200.0 ± 100.0 | 2100.0 | 2300.0 | reference |
|
||||
| =sleep 0.1= | 200.0 ± 100.0 | 100.0 | 300.0 | -90.9% |
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -418,40 +426,33 @@ fn test_markup_export_manual_s() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, Some(TimeUnit::Second), SortOrder::Command), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, Some(Unit { symbol: "s", scale: 1.0 })), @r#"
|
||||
| Command | Mean Time [s] | Min [s] | Max [s] | Change |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 2` | 2.020 ± 0.010 | 2.010 | 2.030 | 16.83 ± 1.41 |
|
||||
| `sleep 0.1` | 0.120 ± 0.010 | 0.110 | 0.130 | 1.00 |
|
||||
| `sleep 2` | 2.020 ± 0.010 | 2.010 | 2.030 | reference |
|
||||
| `sleep 0.1` | 0.120 ± 0.010 | 0.110 | 0.130 | -94.1% |
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, Some(TimeUnit::Second), SortOrder::MeanTime), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 0.1` | 0.120 ± 0.010 | 0.110 | 0.130 | 1.00 |
|
||||
| `sleep 2` | 2.020 ± 0.010 | 2.010 | 2.030 | 16.83 ± 1.41 |
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, Some(TimeUnit::Second), SortOrder::Command), @r#"
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, Some(Unit { symbol: "s", scale: 1.0 })), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [s]
|
||||
| Min [s]
|
||||
| Max [s]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Time [s]
|
||||
| Min [s]
|
||||
| Max [s]
|
||||
| Change
|
||||
|
||||
| `sleep 2`
|
||||
| 2.020 ± 0.010
|
||||
| 2.010
|
||||
| 2.030
|
||||
| 16.83 ± 1.41
|
||||
| `sleep 2`
|
||||
| 2.020 ± 0.010
|
||||
| 2.010
|
||||
| 2.030
|
||||
| reference
|
||||
|
||||
| `sleep 0.1`
|
||||
| 0.120 ± 0.010
|
||||
| 0.110
|
||||
| 0.130
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 0.120 ± 0.010
|
||||
| 0.110
|
||||
| 0.130
|
||||
| -94.1%
|
||||
|===
|
||||
"#);
|
||||
}
|
||||
Reference in new issue
Block a user