Skip to content

Commit

Permalink
Add scale_throughputs function.
Browse files Browse the repository at this point in the history
This is mostly future-proofing for #149, but it does allow format_throughput to be implemented in terms of scale_throughputs.
  • Loading branch information
bheisler committed Aug 17, 2019
1 parent 6d720ff commit 168e175
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 68 deletions.
26 changes: 25 additions & 1 deletion benches/benchmarks/custom_measurement.rs
Expand Up @@ -21,14 +21,38 @@ impl ValueFormatter for HalfSecFormatter {
}
}

fn scale_for_graph(&self, ns: f64, values: &mut [f64]) -> &'static str {
fn scale_values(&self, _typical: f64, values: &mut [f64]) -> &'static str {
for val in values {
*val *= 2f64 * 10f64.powi(-9);
}

"s/2"
}

fn scale_throughputs(
&self,
_typical: f64,
throughput: &Throughput,
values: &mut [f64],
) -> &'static str {
match *throughput {
Throughput::Bytes(bytes) => {
for val in values {
*val = (bytes as f64) / (*val * 2f64 * 10f64.powi(-9))
}

"b/s/2"
}
Throughput::Elements(elems) => {
for val in values {
*val = (elems as f64) / (*val * 2f64 * 10f64.powi(-9))
}

"elem/s/2"
}
}
}

fn scale_for_machines(&self, values: &mut [f64]) -> &'static str {
for val in values {
*val *= 2f64 * 10f64.powi(-9);
Expand Down
62 changes: 45 additions & 17 deletions book/src/user_guide/custom_measurements.md
Expand Up @@ -94,9 +94,10 @@ The next trait is `ValueFormatter`, which defines how a measurement is displayed

```rust
pub trait ValueFormatter {
fn format_value(&self, value: f64) -> String;
fn format_throughput(&self, throughput: &Throughput, value: f64) -> String;
fn scale_for_graph(&self, typical_value: f64, values: &mut[f64]) -> (&'static str);
fn format_value(&self, value: f64) -> String {...}
fn format_throughput(&self, throughput: &Throughput, value: f64) -> String {...}
fn scale_values(&self, typical_value: f64, values: &mut [f64]) -> &'static str;
fn scale_throughputs(&self, typical_value: f64, throughput: &Throughput, values: &mut [f64]) -> &'static str;
fn scale_for_machines(&self, values: &mut [f64]) -> &'static str;
}
```
Expand All @@ -112,7 +113,7 @@ Implementors should try to format the values in a way that will make sense to hu
prefixes to simplify the numbers. An easy way to do this is to have a series of conditionals like so:

```rust
if ns < 1.0 { // ns = time in nanoseconds
if ns < 1.0 { // ns = time in nanoseconds per iteration
format!("{:>6} ps", ns * 1e3)
} else if ns < 10f64.powi(3) {
format!("{:>6} ns", ns)
Expand All @@ -128,24 +129,26 @@ if ns < 1.0 { // ns = time in nanoseconds
It's also a good idea to limit the amount of precision in floating-point output - after a few
digits the numbers don't matter much anymore but add a lot of visual noise and make the results
harder to interpret. For example, it's very unlikely that anyone cares about the difference between
`10.2896653s` and `10.2896654s` - it's much more salient that their function takes "about 10.3
`10.2896653s` and `10.2896654s` - it's much more salient that their function takes "about 10.290
seconds per iteration".

With that out of the way, `format_value` is pretty straightforward. `format_throughput` is also not
too difficult; match on `Throughput::Bytes` or `Throughput::Elements` and generate an appropriate
description. For wall-clock time, that would likely take the form of "bytes per second", but a
measurement that read CPU performance counters might want to display throughput in terms of "cycles
per byte".

`scale_for_graph` is a bit more complex. This is primarily used for plotting. This accepts a
"typical" value chosen by Criterion.rs, and a mutable slice of values to scale. This function
should choose an appropriate unit based on the typical value, and convert all values in the slice
to that unit. It should also return a string representing the chosen unit. So, for our wall-clock
times where the measured values are in nanoseconds, if we wanted to display plots in milliseconds
we would multiply all of the input values by `10.0f64.powi(-6)` and return `"ms"`, because
multiplying a value in nanoseconds by 10^-6 gives a value in milliseconds.

`scale_for_machines` is similar to `scale_for_graph`, except that it's used for generating
per byte". Note that default implementations of `format_value` and `format_throughput` are provided
which use `scale_values` and `scale_throughputs`, but you can override them if you wish.

`scale_values` is a bit more complex. This accepts a "typical" value chosen by Criterion.rs, and a
mutable slice of values to scale. This function should choose an appropriate unit based on the
typical value, and convert all values in the slice to that unit. It should also return a string
representing the chosen unit. So, for our wall-clock times where the measured values are in
nanoseconds, if we wanted to display plots in milliseconds we would multiply all of the input
values by `10.0f64.powi(-6)` and return `"ms"`, because multiplying a value in nanoseconds by 10^-6
gives a value in milliseconds. `scale_throughputs` does the same thing, only it converts a slice of
measured values to their corresponding scaled throughput values.

`scale_for_machines` is similar to `scale_values`, except that it's used for generating
machine-readable outputs. It does not accept a typical value, because this function should always
return values in the same unit.

Expand All @@ -172,14 +175,39 @@ impl ValueFormatter for HalfSecFormatter {
}
}

fn scale_for_graph(&self, ns: f64, values: &mut [f64]) -> &'static str {
fn scale_values(&self, ns: f64, values: &mut [f64]) -> &'static str {
for val in values {
*val *= 2f64 * 10f64.powi(-9);
}

"s/2"
}

fn scale_throughputs(
&self,
_typical: f64,
throughput: &Throughput,
values: &mut [f64],
) -> &'static str {
match *throughput {
Throughput::Bytes(bytes) => {
// Convert nanoseconds/iteration to bytes/half-second.
for val in values {
*val = (bytes as f64) / (*val * 2f64 * 10f64.powi(-9))
}

"b/s/2"
}
Throughput::Elements(elems) => {
for val in values {
*val = (elems as f64) / (*val * 2f64 * 10f64.powi(-9))
}

"elem/s/2"
}
}
}

fn scale_for_machines(&self, values: &mut [f64]) -> &'static str {
// Convert values in nanoseconds to half-seconds.
for val in values {
Expand Down
101 changes: 66 additions & 35 deletions src/measurement.rs
Expand Up @@ -20,18 +20,41 @@ use std::time::{Duration, Instant};
/// of the elapsed time in nanoseconds.
pub trait ValueFormatter {
/// Format the value (with appropriate unit) and return it as a string.
fn format_value(&self, value: f64) -> String;
fn format_value(&self, value: f64) -> String {
let mut values = [value];
let unit = self.scale_values(value, &mut values);
format!("{:>6} {}", short(values[0]), unit)
}

/// Format the value as a throughput measurement. The value represents the measurement value;
/// the implementor will have to calculate bytes per second, iterations per cycle, etc.
fn format_throughput(&self, throughput: &Throughput, value: f64) -> String;
fn format_throughput(&self, throughput: &Throughput, value: f64) -> String {
let mut values = [value];
let unit = self.scale_throughputs(value, throughput, &mut values);
format!("{:>6} {}", short(values[0]), unit)
}

/// Scale the given values to some appropriate unit and return the unit string.
///
/// The given typical value should be used to choose the unit. This function may be called
/// multiple times with different datasets; the typical value will remain the same to ensure
/// that the units remain consistent within a graph. The typical value will not be NaN.
/// Values will not contain NaN as input, and the transformed values must not contain NaN.
fn scale_values(&self, typical_value: f64, values: &mut [f64]) -> (&'static str);

/// Scale the given values and return an appropriate unit string.
/// Convert the given measured values into throughput numbers based on the given throughput
/// value, scale them to some appropriate unit, and return the unit string.
///
/// The given typical value should be used to choose the unit. This function may be called
/// multiple times with different datasets; the typical value will remain the same to ensure
/// that the units remain consistent within a graph. The typical value will not be NaN.
fn scale_for_graph(&self, typical_value: f64, values: &mut [f64]) -> (&'static str);
/// Values will not contain NaN as input, and the transformed values must not contain NaN.
fn scale_throughputs(
&self,
typical_value: f64,
throughput: &Throughput,
values: &mut [f64],
) -> (&'static str);

/// Scale the values and return a unit string designed for machines.
///
Expand Down Expand Up @@ -82,52 +105,60 @@ pub trait Measurement {

pub(crate) struct DurationFormatter;
impl DurationFormatter {
fn bytes_per_second(&self, bytes_per_second: f64) -> String {
if bytes_per_second < 1024.0 {
format!("{:>6} B/s", short(bytes_per_second))
fn bytes_per_second(&self, bytes: f64, typical: f64, values: &mut [f64]) -> &'static str {
let bytes_per_second = bytes * (1e9 / typical);
let (denominator, unit) = if bytes_per_second < 1024.0 {
(1.0, " B/s")
} else if bytes_per_second < 1024.0 * 1024.0 {
format!("{:>6} KiB/s", short(bytes_per_second / 1024.0))
(1024.0, "KiB/s")
} else if bytes_per_second < 1024.0 * 1024.0 * 1024.0 {
format!("{:>6} MiB/s", short(bytes_per_second / (1024.0 * 1024.0)))
(1024.0 * 1024.0, "MiB/s")
} else {
format!(
"{:>6} GiB/s",
short(bytes_per_second / (1024.0 * 1024.0 * 1024.0))
)
(1024.0 * 1024.0 * 1024.0, "GiB/s")
};

for val in values {
let bytes_per_second = bytes * (1e9 / *val);
*val = bytes_per_second / denominator;
}

unit
}

fn elements_per_second(&self, elements_per_second: f64) -> String {
if elements_per_second < 1000.0 {
format!("{:>6} elem/s", short(elements_per_second))
} else if elements_per_second < 1000.0 * 1000.0 {
format!("{:>6} Kelem/s", short(elements_per_second / 1000.0))
} else if elements_per_second < 1000.0 * 1000.0 * 1000.0 {
format!(
"{:>6} Melem/s",
short(elements_per_second / (1000.0 * 1000.0))
)
fn elements_per_second(&self, elems: f64, typical: f64, values: &mut [f64]) -> &'static str {
let elems_per_second = elems * (1e9 / typical);
let (denominator, unit) = if elems_per_second < 1000.0 {
(1.0, " elem/s")
} else if elems_per_second < 1000.0 * 1000.0 {
(1000.0, "Kelem/s")
} else if elems_per_second < 1000.0 * 1000.0 * 1000.0 {
(1000.0 * 1000.0, "Melem/s")
} else {
format!(
"{:>6} Gelem/s",
short(elements_per_second / (1000.0 * 1000.0 * 1000.0))
)
(1000.0 * 1000.0 * 1000.0, "Gelem/s")
};

for val in values {
let elems_per_second = elems * (1e9 / *val);
*val = elems_per_second / denominator;
}

unit
}
}
impl ValueFormatter for DurationFormatter {
fn format_value(&self, ns: f64) -> String {
crate::format::time(ns)
}

fn format_throughput(&self, throughput: &Throughput, ns: f64) -> String {
fn scale_throughputs(
&self,
typical: f64,
throughput: &Throughput,
values: &mut [f64],
) -> &'static str {
match *throughput {
Throughput::Bytes(bytes) => self.bytes_per_second((bytes as f64) * (1e9 / ns)),
Throughput::Elements(elems) => self.elements_per_second((elems as f64) * (1e9 / ns)),
Throughput::Bytes(bytes) => self.bytes_per_second(bytes as f64, typical, values),
Throughput::Elements(elems) => self.elements_per_second(elems as f64, typical, values),
}
}

fn scale_for_graph(&self, ns: f64, values: &mut [f64]) -> &'static str {
fn scale_values(&self, ns: f64, values: &mut [f64]) -> &'static str {
let (factor, unit) = if ns < 10f64.powi(0) {
(10f64.powi(3), "ps")
} else if ns < 10f64.powi(3) {
Expand Down
4 changes: 2 additions & 2 deletions src/plot/distributions.rs
Expand Up @@ -24,13 +24,13 @@ fn abs_distribution(
let ci = estimate.confidence_interval;
let typical = ci.upper_bound;
let mut ci_values = [ci.lower_bound, ci.upper_bound, estimate.point_estimate];
let unit = formatter.scale_for_graph(typical, &mut ci_values);
let unit = formatter.scale_values(typical, &mut ci_values);
let (lb, ub, p) = (ci_values[0], ci_values[1], ci_values[2]);

let start = lb - (ub - lb) / 9.;
let end = ub + (ub - lb) / 9.;
let mut scaled_xs: Vec<f64> = distribution.iter().cloned().collect();
let _ = formatter.scale_for_graph(typical, &mut scaled_xs);
let _ = formatter.scale_values(typical, &mut scaled_xs);
let scaled_xs_sample = Sample::new(&scaled_xs);
let (kde_xs, ys) = kde::sweep(scaled_xs_sample, KDE_POINTS, Some((start, end)));

Expand Down
10 changes: 5 additions & 5 deletions src/plot/pdf.rs
Expand Up @@ -14,7 +14,7 @@ pub(crate) fn pdf(
let avg_times = &measurements.avg_times;
let typical = avg_times.max();
let mut scaled_avg_times: Vec<f64> = (avg_times as &Sample<f64>).iter().cloned().collect();
let unit = formatter.scale_for_graph(typical, &mut scaled_avg_times);
let unit = formatter.scale_values(typical, &mut scaled_avg_times);
let scaled_avg_times = Sample::new(&scaled_avg_times);

let mean = scaled_avg_times.mean();
Expand All @@ -36,7 +36,7 @@ pub(crate) fn pdf(
let (xs, ys) = kde::sweep(&scaled_avg_times, KDE_POINTS, None);
let (lost, lomt, himt, hist) = avg_times.fences();
let mut fences = [lost, lomt, himt, hist];
let _ = formatter.scale_for_graph(typical, &mut fences);
let _ = formatter.scale_values(typical, &mut fences);
let [lost, lomt, himt, hist] = fences;

let vertical = &[0., max_iters];
Expand Down Expand Up @@ -234,7 +234,7 @@ pub(crate) fn pdf_small(
let avg_times = &*measurements.avg_times;
let typical = avg_times.max();
let mut scaled_avg_times: Vec<f64> = (avg_times as &Sample<f64>).iter().cloned().collect();
let unit = formatter.scale_for_graph(typical, &mut scaled_avg_times);
let unit = formatter.scale_values(typical, &mut scaled_avg_times);
let scaled_avg_times = Sample::new(&scaled_avg_times);
let mean = scaled_avg_times.mean();

Expand Down Expand Up @@ -294,14 +294,14 @@ fn pdf_comparison_figure(
let base_avg_times = Sample::new(&comparison.base_avg_times);
let typical = base_avg_times.max().max(measurements.avg_times.max());
let mut scaled_base_avg_times: Vec<f64> = comparison.base_avg_times.clone();
let unit = formatter.scale_for_graph(typical, &mut scaled_base_avg_times);
let unit = formatter.scale_values(typical, &mut scaled_base_avg_times);
let scaled_base_avg_times = Sample::new(&scaled_base_avg_times);

let mut scaled_new_avg_times: Vec<f64> = (&measurements.avg_times as &Sample<f64>)
.iter()
.cloned()
.collect();
let _ = formatter.scale_for_graph(typical, &mut scaled_new_avg_times);
let _ = formatter.scale_values(typical, &mut scaled_new_avg_times);
let scaled_new_avg_times = Sample::new(&scaled_new_avg_times);

let base_mean = scaled_base_avg_times.mean();
Expand Down
6 changes: 3 additions & 3 deletions src/plot/regression.rs
Expand Up @@ -25,12 +25,12 @@ fn regression_figure(
let data = &measurements.data;
let (max_iters, typical) = (data.x().max(), data.y().max());
let mut scaled_y: Vec<f64> = data.y().iter().cloned().collect();
let unit = formatter.scale_for_graph(typical, &mut scaled_y);
let unit = formatter.scale_values(typical, &mut scaled_y);
let scaled_y = Sample::new(&scaled_y);

let point_estimate = Slope::fit(&measurements.data).0;
let mut scaled_points = [point_estimate * max_iters, lb * max_iters, ub * max_iters];
let _ = formatter.scale_for_graph(typical, &mut scaled_points);
let _ = formatter.scale_values(typical, &mut scaled_points);
let [point, lb, ub] = scaled_points;

let exponent = (max_iters.log10() / 3.).floor() as i32 * 3;
Expand Down Expand Up @@ -179,7 +179,7 @@ fn regression_comparison_figure(
point * max_iters,
ub * max_iters,
];
let unit = formatter.scale_for_graph(typical, &mut points);
let unit = formatter.scale_values(typical, &mut points);
let [base_lb, base_point, base_ub, lb, point, ub] = points;

let mut figure = Figure::new();
Expand Down

0 comments on commit 168e175

Please sign in to comment.