Skip to content

Commit

Permalink
Change markdown export to use seconds, closes sharkdp#71
Browse files Browse the repository at this point in the history
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 sharkdp#80 proposes a new option to choose the units used for both the
CLI report and results export.
  • Loading branch information
jasonpeacock committed Sep 15, 2018
1 parent de74c21 commit e912e0f
Show file tree
Hide file tree
Showing 2 changed files with 207 additions and 18 deletions.
146 changes: 132 additions & 14 deletions src/hyperfine/export/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,39 +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
"| `{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);
}
79 changes: 75 additions & 4 deletions src/hyperfine/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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]
Expand Down

0 comments on commit e912e0f

Please sign in to comment.