mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-11-01 08:50:19 +00:00
Change markdown export to use seconds, closes #71
The markdown results exporter was using milliseconds, but the other results exporters are using the default seconds unit. Apply some refactoring so the same logic used to select units for the CLI output can be used to select the units for the markdown exporter. Now the units of the markdown exporter match that of the CLI. When there are multiple benchmarks, the CLI will choose the appropriate unit for each benchmark separately. In this case, the markdown exporter will use the first benchmark to select the units for all the results. *This will change the markdown results for all users from milliseconds to seconds, a UX breaking change.* Add unit tests for the markdown exporter to verify the output. Issue #80 proposes a new option to choose the units used for both the CLI report and results export.
This commit is contained in:
1 parent
f1d4443416
commit
29804c54c9
2 files changed
+208
-20
No files matched your search
@@ -1,40 +1,157 @@
|
||||
use super::Exporter;
|
||||
|
||||
use hyperfine::format::{Unit, format_duration_value};
|
||||
use hyperfine::types::BenchmarkResult;
|
||||
|
||||
use std::io::Result;
|
||||
|
||||
const MULTIPLIER: f64 = 1e3;
|
||||
macro_rules! TABLE_HEADER {
|
||||
($unit:expr) => {
|
||||
format!("| Command | Mean [{unit}] | Min…Max [{unit}] |\n|:---|---:|---:|\n", unit=$unit) };
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MarkdownExporter {}
|
||||
|
||||
impl Exporter for MarkdownExporter {
|
||||
fn serialize(&self, results: &Vec<BenchmarkResult>) -> Result<Vec<u8>> {
|
||||
let mut destination = start_table();
|
||||
// Default to `Second`.
|
||||
let mut unit = Unit::Second;
|
||||
|
||||
if !results.is_empty() {
|
||||
// Use the first BenchmarkResult entry to determine the unit for all entries.
|
||||
unit = format_duration_value(results[0].mean, None).1;
|
||||
}
|
||||
|
||||
let mut destination = start_table(unit);
|
||||
|
||||
for result in results {
|
||||
add_table_row(&mut destination, result);
|
||||
add_table_row(&mut destination, result, unit);
|
||||
}
|
||||
|
||||
Ok(destination)
|
||||
}
|
||||
}
|
||||
|
||||
fn start_table() -> Vec<u8> {
|
||||
"| Command | Mean [ms] | Min…Max [ms] |\n|:---|---:|---:|\n"
|
||||
.bytes()
|
||||
.collect()
|
||||
fn start_table(unit: Unit) -> Vec<u8> {
|
||||
TABLE_HEADER!(unit.short_name()).bytes().collect()
|
||||
}
|
||||
fn add_table_row(dest: &mut Vec<u8>, entry: &BenchmarkResult) {
|
||||
|
||||
fn add_table_row(dest: &mut Vec<u8>, entry: &BenchmarkResult, unit: Unit) {
|
||||
let mean_str = format_duration_value(entry.mean, Some(unit)).0;
|
||||
let stddev_str = format_duration_value(entry.stddev, Some(unit)).0;
|
||||
let min_str = format_duration_value(entry.min, Some(unit)).0;
|
||||
let max_str = format_duration_value(entry.max, Some(unit)).0;
|
||||
|
||||
dest.extend(
|
||||
format!(
|
||||
"| `{}` | {:.1} ± {:.1} | {:.1}…{:.1} |\n",
|
||||
entry.command.replace("|", "\\|"),
|
||||
entry.mean * MULTIPLIER,
|
||||
entry.stddev * MULTIPLIER,
|
||||
entry.min * MULTIPLIER,
|
||||
entry.max * MULTIPLIER
|
||||
)
|
||||
.as_bytes(),
|
||||
"| `{command}` | {mean} ± {stddev} | {min}…{max} |\n",
|
||||
command=entry.command.replace("|", "\\|"),
|
||||
mean=mean_str,
|
||||
stddev=stddev_str,
|
||||
min=min_str,
|
||||
max=max_str,
|
||||
).as_bytes(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Ensure the markdown output includes the table header and the multiple
|
||||
/// benchmark results as a table. The list of actual times is not included
|
||||
/// in the output.
|
||||
///
|
||||
/// This also demonstrates that the first entry's units (ms) are used to set
|
||||
/// the units for all entries.
|
||||
#[test]
|
||||
fn test_markdown_format_ms() {
|
||||
let exporter = MarkdownExporter::default();
|
||||
|
||||
let mut timing_results = vec![];
|
||||
|
||||
timing_results.push(BenchmarkResult::new(
|
||||
String::from("sleep 0.1"),
|
||||
0.1057, // mean
|
||||
0.0016, // std dev
|
||||
0.0009, // user_mean
|
||||
0.0011, // system_mean
|
||||
0.1023, // min
|
||||
0.1080, // max
|
||||
vec![0.1, 0.1, 0.1], // times
|
||||
));
|
||||
|
||||
timing_results.push(BenchmarkResult::new(
|
||||
String::from("sleep 2"),
|
||||
2.0050, // mean
|
||||
0.0020, // std dev
|
||||
0.0009, // user_mean
|
||||
0.0012, // system_mean
|
||||
2.0020, // min
|
||||
2.0080, // max
|
||||
vec![2.0, 2.0, 2.0], // times
|
||||
));
|
||||
|
||||
let formatted = String::from_utf8(exporter.serialize(&timing_results).unwrap()).unwrap();
|
||||
|
||||
let formatted_expected = format!(
|
||||
"{}\
|
||||
| `sleep 0.1` | 105.7 ± 1.6 | 102.3…108.0 |
|
||||
| `sleep 2` | 2005.0 ± 2.0 | 2002.0…2008.0 |
|
||||
", TABLE_HEADER!("ms"));
|
||||
|
||||
assert_eq!(formatted_expected, formatted);
|
||||
}
|
||||
|
||||
/// This (again) demonstrates that the first entry's units (s) are used to set
|
||||
/// the units for all entries.
|
||||
#[test]
|
||||
fn test_markdown_format_s() {
|
||||
let exporter = MarkdownExporter::default();
|
||||
|
||||
let mut timing_results = vec![];
|
||||
|
||||
timing_results.push(BenchmarkResult::new(
|
||||
String::from("sleep 2"),
|
||||
2.0050, // mean
|
||||
0.0020, // std dev
|
||||
0.0009, // user_mean
|
||||
0.0012, // system_mean
|
||||
2.0020, // min
|
||||
2.0080, // max
|
||||
vec![2.0, 2.0, 2.0], // times
|
||||
));
|
||||
|
||||
timing_results.push(BenchmarkResult::new(
|
||||
String::from("sleep 0.1"),
|
||||
0.1057, // mean
|
||||
0.0016, // std dev
|
||||
0.0009, // user_mean
|
||||
0.0011, // system_mean
|
||||
0.1023, // min
|
||||
0.1080, // max
|
||||
vec![0.1, 0.1, 0.1], // times
|
||||
));
|
||||
|
||||
let formatted = String::from_utf8(exporter.serialize(&timing_results).unwrap()).unwrap();
|
||||
|
||||
let formatted_expected = format!(
|
||||
"{}\
|
||||
| `sleep 2` | 2.005 ± 0.002 | 2.002…2.008 |
|
||||
| `sleep 0.1` | 0.106 ± 0.002 | 0.102…0.108 |
|
||||
", TABLE_HEADER!("s"));
|
||||
|
||||
assert_eq!(formatted_expected, formatted);
|
||||
}
|
||||
|
||||
/// An empty list of benchmark results will only include the table header
|
||||
/// in the markdown output, using the default `Seconds` unit.
|
||||
#[test]
|
||||
fn test_markdown_format_empty_results() {
|
||||
let exporter = MarkdownExporter::default();
|
||||
|
||||
let timing_results = vec![];
|
||||
|
||||
let formatted = String::from_utf8(exporter.serialize(&timing_results).unwrap()).unwrap();
|
||||
|
||||
let formatted_expected = TABLE_HEADER!("s");
|
||||
|
||||
assert_eq!(formatted_expected, formatted);
|
||||
}
|
||||
+75
-4
@@ -6,6 +6,34 @@ pub enum Unit {
|
||||
MilliSecond,
|
||||
}
|
||||
|
||||
impl Unit {
|
||||
/// The abbreviation of the Unit.
|
||||
pub fn short_name(&self) -> String {
|
||||
match *self {
|
||||
Unit::Second => String::from("s"),
|
||||
Unit::MilliSecond => String::from("ms"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The multiplier value to convert from `from_unit` to the Unit.
|
||||
pub fn multiplier_from(&self, from_unit: Unit) -> f64 {
|
||||
match (*self, from_unit) {
|
||||
(Unit::Second, Unit::Second) => 1.0,
|
||||
(Unit::MilliSecond, Unit::MilliSecond) => 1.0,
|
||||
(Unit::Second, Unit::MilliSecond) => 1e-3,
|
||||
(Unit::MilliSecond, Unit::Second) => 1e3,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Second value formatted for the Unit.
|
||||
pub fn format(&self, value: Second) -> String {
|
||||
match *self {
|
||||
Unit::Second => format!("{:.3}", value * self.multiplier_from(Unit::Second)),
|
||||
Unit::MilliSecond => format!("{:.1}", value * self.multiplier_from(Unit::Second)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the given duration as a string. The output-unit can be enforced by setting `unit` to
|
||||
/// `Some(target_unit)`. If `unit` is `None`, it will be determined automatically.
|
||||
pub fn format_duration(duration: Second, unit: Option<Unit>) -> String {
|
||||
@@ -16,11 +44,54 @@ pub fn format_duration(duration: Second, unit: Option<Unit>) -> String {
|
||||
|
||||
/// Like `format_duration`, but returns the target unit as well.
|
||||
pub fn format_duration_unit(duration: Second, unit: Option<Unit>) -> (String, Unit) {
|
||||
if (duration < 1.0 && unit.is_none()) || unit == Some(Unit::MilliSecond) {
|
||||
(format!("{:.1} ms", duration * 1e3), Unit::MilliSecond)
|
||||
} else {
|
||||
(format!("{:.3} s", duration), Unit::Second)
|
||||
let (out_str, out_unit) = format_duration_value(duration, unit);
|
||||
|
||||
(format!("{} {}", out_str, out_unit.short_name()), out_unit)
|
||||
}
|
||||
|
||||
/// Like `format_duration`, but returns the target unit as well.
|
||||
pub fn format_duration_value(duration: Second, unit: Option<Unit>) -> (String, Unit) {
|
||||
|
||||
// Default to `Second` until proven otherwise.
|
||||
let mut duration_unit = Unit::Second;
|
||||
|
||||
match unit {
|
||||
Some(unit_option) => {
|
||||
// Use user-supplied unit.
|
||||
duration_unit = unit_option;
|
||||
},
|
||||
None => {
|
||||
if duration < 1.0 {
|
||||
// It's a small value, use `Millisecond` instead.
|
||||
duration_unit = Unit::MilliSecond;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
(duration_unit.format(duration), duration_unit)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unit_short_name() {
|
||||
assert_eq!("s", Unit::Second.short_name());
|
||||
assert_eq!("ms", Unit::MilliSecond.short_name());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unit_multiplier_from() {
|
||||
assert_eq!(1.0, Unit::Second.multiplier_from(Unit::Second));
|
||||
assert_eq!(1.0, Unit::MilliSecond.multiplier_from(Unit::MilliSecond));
|
||||
|
||||
assert_eq!(0.001, Unit::Second.multiplier_from(Unit::MilliSecond));
|
||||
assert_eq!(1000.0, Unit::MilliSecond.multiplier_from(Unit::Second));
|
||||
}
|
||||
|
||||
// Note - the values are rounded when formatted.
|
||||
#[test]
|
||||
fn test_unit_format() {
|
||||
let value: Second = 123.456789;
|
||||
assert_eq!("123.457", Unit::Second.format(value));
|
||||
assert_eq!("123456.8", Unit::MilliSecond.format(value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user