criterion_inverted_throughput/
lib.rs

1//! # Criterion Inverted Throughput
2//! Custom [`criterion::measurement::Measurement`] to get throughputs in the format `[time]/[elements or bytes]`.
3//!
4//! ## Description
5//!
6//! With deafult criterion config, result of [throughput measurement](https://bheisler.github.io/criterion.rs/book/user_guide/advanced_configuration.html#throughput-measurements) is printed like:
7//!
8//! ```text
9//! time:   [2.8617 µs 2.8728 µs 2.8850 µs]
10//! thrpt:  [14.558 Melem/s 14.620 Melem/s 14.677 Melem/s]
11//! ```
12//!
13//! Throughput is got in the format `[elements or bytes]/s`.
14//! It is fine as a throughput, but sometimes we want to get how much time is
15//! cost per 1 element or byte.
16//!
17//! Using this crate, we can got it in the format `[time]/[element or byte]` without post-processing calculations, like:
18//!
19//! ```text
20//! time:   [2.8581 µs 2.8720 µs 2.8917 µs]
21//! thrpt:  [68.849 ns/elem 68.381 ns/elem 68.049 ns/elem]
22//! ```
23//!
24//! ## Usage
25//! Specify [`InvertedThroughput`] as your criterion measurement.
26//!
27//! ```
28//! use criterion::{criterion_group, criterion_main, Criterion, Throughput, measurement::Measurement};
29//! use criterion_inverted_throughput::InvertedThroughput;
30//!
31//! fn bench_foo<M: Measurement>(c: &mut Criterion<M>) {
32//!     let mut g = c.benchmark_group("foo");
33//!
34//!     // tell size of input to enable throughput
35//!     g.throughput(Throughput::Elements(42u64));
36//!
37//!     // add benchmarks to the group here like
38//!     // g.bench_function("foo", |b| b.iter(|| do_something()));
39//!
40//!     g.finish();
41//! }
42//!
43//! criterion_group!(
44//!     name = Foo;
45//!     // specify `InvertedThroughput` as measurement
46//!     config = Criterion::default().with_measurement(InvertedThroughput::new());
47//!     targets = bench_foo
48//! );
49//! criterion_main!(Foo);
50//! ```
51
52use criterion::measurement::{Measurement, ValueFormatter, WallTime};
53use criterion::Throughput;
54
55/// The custom measurement printing inverted throughputs instead of the throughputs
56///
57/// Specify it as custom measurement in your benchmarks like
58/// `Criterion::default().with_measurement(InvertedThroughput::new())`
59pub struct InvertedThroughput(WallTime);
60
61impl InvertedThroughput {
62    /// Returns a new `InvertedThroughput`
63    pub fn new() -> Self {
64        InvertedThroughput(WallTime)
65    }
66}
67
68impl Default for InvertedThroughput {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl Measurement for InvertedThroughput {
75    type Intermediate = <WallTime as Measurement>::Intermediate;
76    type Value = <WallTime as Measurement>::Value;
77    fn start(&self) -> Self::Intermediate {
78        self.0.start()
79    }
80    fn end(&self, i: Self::Intermediate) -> Self::Value {
81        self.0.end(i)
82    }
83
84    fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value {
85        self.0.add(v1, v2)
86    }
87    fn zero(&self) -> Self::Value {
88        self.0.zero()
89    }
90    fn to_f64(&self, val: &Self::Value) -> f64 {
91        self.0.to_f64(val)
92    }
93
94    fn formatter(&self) -> &dyn ValueFormatter {
95        self
96    }
97}
98
99impl InvertedThroughput {
100    fn time_per_unit(&self, units: f64, typical_value: f64, values: &mut [f64]) -> &'static str {
101        let typical_time = typical_value / units;
102        for val in &mut *values {
103            let val_per_unit = *val / units;
104            *val = val_per_unit;
105        }
106        self.0.formatter().scale_values(typical_time, values)
107    }
108
109    fn static_denom(&self, time_denom: &str, unit_denom: &str) -> &'static str {
110        match (unit_denom, time_denom) {
111            ("byte", "ps") => "ps/byte",
112            ("byte", "ns") => "ns/byte",
113            ("byte", "µs") => "µs/byte",
114            ("byte", "ms") => "ms/byte",
115            ("byte", "s") => "s/byte",
116            ("elem", "ps") => "ps/elem",
117            ("elem", "ns") => "ns/elem",
118            ("elem", "µs") => "µs/elem",
119            ("elem", "ms") => "ms/elem",
120            ("elem", "s") => "s/elem",
121            _ => "UNEXPECTED",
122        }
123    }
124}
125
126impl ValueFormatter for InvertedThroughput {
127    fn scale_values(&self, typical_value: f64, values: &mut [f64]) -> &'static str {
128        self.0.formatter().scale_values(typical_value, values)
129    }
130
131    fn scale_throughputs(
132        &self,
133        typical_value: f64,
134        throughput: &Throughput,
135        values: &mut [f64],
136    ) -> &'static str {
137        let (t_val, t_unit) = match *throughput {
138            Throughput::Bytes(v) => (v as f64, "byte"),
139            Throughput::BytesDecimal(v) => (v as f64, "byte"),
140            Throughput::Elements(v) => (v as f64, "elem"),
141        };
142        self.static_denom(self.time_per_unit(t_val, typical_value, values), t_unit)
143    }
144
145    fn scale_for_machines(&self, values: &mut [f64]) -> &'static str {
146        self.0.formatter().scale_for_machines(values)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use test_case::test_case;
154
155    #[derive(Clone)]
156    struct Data {
157        typical_value: f64,
158        values: Vec<f64>,
159        throughput: Throughput,
160    }
161
162    impl Data {
163        fn new(typical_value: f64, throughput: Throughput) -> Self {
164            let mut values: Vec<f64> = vec![];
165            for x in -5..5 {
166                // generate values in 90%-110% times of typical value
167                values.push(typical_value * (1f64 - (x as f64 * 0.02)))
168            }
169            Self {
170                typical_value,
171                values,
172                throughput,
173            }
174        }
175    }
176
177    enum Unit {
178        Element,
179        Byte,
180        ByteDecimal,
181    }
182
183    fn normalize_time(denom: &str, value: f64) -> f64 {
184        if denom.to_string().starts_with("ps") {
185            value / 1e12
186        } else if denom.to_string().starts_with("ns") {
187            value / 1e9
188        } else if denom.to_string().starts_with("µs") {
189            value / 1e6
190        } else if denom.to_string().starts_with("ms") {
191            value / 1e3
192        } else if denom.to_string().starts_with("s") {
193            value
194        } else {
195            panic!("Unexpected denom for time: {}", denom)
196        }
197    }
198
199    fn normalize_amount(denom: &str, value: f64) -> f64 {
200        if denom.to_string().starts_with("G") {
201            value * 1e9
202        } else if denom.to_string().starts_with("M") {
203            value * 1e6
204        } else if denom.to_string().starts_with("K") {
205            value * 1e3
206        } else {
207            value
208        }
209    }
210
211    fn assert_nearly_eq(a: Vec<f64>, b: Vec<f64>) {
212        assert_eq!(a.len(), b.len(), "left: {:?} !~= right: {:?}", a, b);
213        for i in 0..a.len() {
214            assert_ne!(a[i].abs(), 0.0, "left: {:?} !~= right: {:?}", a, b);
215            assert!(
216                (a[i] - b[i]).abs() < a[i].abs() * 1e-12,
217                "left: {:?} !~= right: {:?}",
218                a,
219                b
220            )
221        }
222    }
223
224    fn assert_nearly_inversion(a: Vec<f64>, b: Vec<f64>) {
225        assert_eq!(
226            a.len(),
227            b.len(),
228            "left: {:?} <not inversion> right: {:?}",
229            a,
230            b
231        );
232        for i in 0..a.len() {
233            assert_ne!(
234                a[i].abs(),
235                0.0,
236                "left: {:?} <not inversion> right: {:?}",
237                a,
238                b
239            );
240            assert!(
241                (a[i] * b[i] - 1f64).abs() < 0.075,
242                "left: {:?} <not inversion> right: {:?} (index: {}, abs(sub(1.0)): {})",
243                a,
244                b,
245                i,
246                (a[i] * b[i] - 1f64).abs(),
247            )
248        }
249    }
250
251    #[test_case(Unit::Element, 1, 1e3 ; "test 1 elements")]
252    #[test_case(Unit::Element, 10, 1e6 ; "test 10 elements")]
253    #[test_case(Unit::Byte, 100, 1e9 ; "test 100 bytes")]
254    #[test_case(Unit::ByteDecimal, 1000, 1e12 ; "test 1000 bytesdecimal")]
255    #[test_case(Unit::Element, 123, 1.234e15 ; "test 123 elements")]
256    #[test_case(Unit::Byte, 123_456_789, 1.234e6 ; "test big bytes")]
257    fn test_invert_throughput(unit: Unit, amount: u64, typical_value: f64) {
258        // generate test case
259        let throughput = match unit {
260            Unit::Element => Throughput::Elements(amount),
261            Unit::Byte => Throughput::Bytes(amount),
262            Unit::ByteDecimal => Throughput::BytesDecimal(amount),
263        };
264        let data = Data::new(typical_value, throughput.clone());
265
266        // measurements
267        let default_measure = WallTime;
268        let our_measure = InvertedThroughput(WallTime);
269
270        // compare value with intert throughput
271        let mut values_by_default = data.values.clone();
272        let mut throughputs_by_default = data.values.clone();
273        let mut inverted_throughputs = data.values.clone();
274
275        let unit_by_default = default_measure
276            .formatter()
277            .scale_values(data.typical_value, &mut values_by_default);
278        let unit_by_default_throughputs = default_measure.formatter().scale_throughputs(
279            data.typical_value,
280            &data.throughput,
281            &mut throughputs_by_default,
282        );
283        let unit_inverted_throughputs = our_measure.scale_throughputs(
284            data.typical_value,
285            &data.throughput,
286            &mut inverted_throughputs,
287        );
288
289        let expected_inverted_throuputs: Vec<f64> = values_by_default
290            .iter()
291            .map(|x| normalize_time(unit_by_default, *x) / amount as f64)
292            .collect();
293        let normalized_default_throuputs: Vec<f64> = throughputs_by_default
294            .iter()
295            .map(|x| normalize_amount(unit_by_default_throughputs, *x))
296            .collect();
297        let normalized_inverted_throuputs: Vec<f64> = inverted_throughputs
298            .iter()
299            .map(|x| normalize_time(unit_inverted_throughputs, *x))
300            .collect();
301
302        assert_nearly_eq(
303            expected_inverted_throuputs,
304            normalized_inverted_throuputs.clone(),
305        );
306        assert_nearly_inversion(normalized_inverted_throuputs, normalized_default_throuputs);
307    }
308}