criterion_decimal_throughput/
lib.rs

1//! Throughput measurement for criterion.rs using decimal multiple-byte units.
2//!
3//! By default, using [criterion.rs throughput measurement](https://bheisler.github.io/criterion.rs/book/user_guide/advanced_configuration.html#throughput-measurements)
4//! gives results in binary multiple-byte units, so KiB/s, MiB/s, etc. Some people, like me, prefer
5//! to use the more intuitive decimal multiple-byte units of KB/s, MB/s, and so on. This crate enables that.
6//!
7//! ## Usage
8//!
9//! You need to:
10//!
11//! 1. Use the custom measurement type [`criterion_decimal_throughput::Criterion`](Criterion) from this crate,
12//! exposed with the [`decimal_byte_measurement`] function.
13//! 2. Enable throughput measurement in the benchmark group with [`criterion::BenchmarkGroup::throughput`].
14//!
15//! ### Example
16//!
17//! ```
18//! use criterion::{criterion_group, criterion_main};
19//! use criterion_decimal_throughput::{Criterion, decimal_byte_measurement};
20//!
21//! fn example_bench(c: &mut Criterion) {
22//!     let mut group = c.benchmark_group("example_name");
23//!     group.throughput(criterion::Throughput::Bytes(/* Your input size here */ 1_000_000u64));
24//!
25//!     // Add your benchmarks to the group here...
26//!
27//!     group.finish();
28//! }
29//!
30//! criterion_group!(
31//!     name = example;
32//!     config = decimal_byte_measurement();
33//!     targets = example_bench
34//! );
35//! criterion_main!(example);
36//! ```
37//!
38//! ### With custom config
39//!
40//! If you use a custom configuration for your benches and want to combine it with this crate, the [`decimal_byte_measurement`] will not
41//! do, as it includes the default Criterion config. Instead, register the measurement with [`criterion::Criterion::with_measurement`]:
42//!
43//! #### Example
44//!
45//! ```
46//! use core::time::Duration;
47//! use criterion::{criterion_group, criterion_main};
48//! use criterion_decimal_throughput::{Criterion, DecimalByteMeasurement};
49//!
50//! fn example_bench(c: &mut Criterion) {
51//!     // ...
52//! }
53//!
54//! // Your custom configuration would come here.
55//! // As an example, we use a configuration that sets a non-default warm-up time of 10 seconds.
56//! pub fn my_custom_config() -> Criterion {
57//!     criterion::Criterion::default()
58//!         .warm_up_time(Duration::from_secs(10))
59//!         .with_measurement(DecimalByteMeasurement::new())
60//! //      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This enables the crate.
61//! }
62//! criterion_group!(
63//!     name = example;
64//!     config = my_custom_config();
65//!     targets = example_bench
66//! );
67//! criterion_main!(example);
68//! ```
69//!
70//! ## Origin
71//!
72//! Related criterion.rs issue: <https://github.com/bheisler/criterion.rs/issues/581>.
73
74#![warn(missing_docs)]
75#![warn(rustdoc::missing_crate_level_docs)]
76#![warn(
77    explicit_outlives_requirements,
78    unreachable_pub,
79    semicolon_in_expressions_from_macros,
80    unused_import_braces,
81    unused_lifetimes
82)]
83
84use criterion::{
85    measurement::{Measurement, ValueFormatter, WallTime},
86    Throughput,
87};
88
89/// Measurement type for decimal multiple-byte units.
90pub struct DecimalByteMeasurement(WallTime);
91
92/// Shorthand for the criterion manager with [`DecimalByteMeasurement`].
93pub type Criterion = criterion::Criterion<DecimalByteMeasurement>;
94
95/// Construct a default [`criterion::Criterion`] manager with [`DecimalByteMeasurement`].
96pub fn decimal_byte_measurement() -> Criterion {
97    criterion::Criterion::default().with_measurement(DecimalByteMeasurement::new())
98}
99
100impl Default for DecimalByteMeasurement {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl DecimalByteMeasurement {
107    /// Create a new [`DecimalByteMeasurement`] struct.
108    pub fn new() -> Self {
109        DecimalByteMeasurement(WallTime)
110    }
111}
112
113impl Measurement for DecimalByteMeasurement {
114    type Intermediate = <WallTime as Measurement>::Intermediate;
115
116    type Value = <WallTime as Measurement>::Value;
117
118    fn start(&self) -> Self::Intermediate {
119        self.0.start()
120    }
121
122    fn end(&self, i: Self::Intermediate) -> Self::Value {
123        self.0.end(i)
124    }
125
126    fn add(&self, v1: &Self::Value, v2: &Self::Value) -> Self::Value {
127        self.0.add(v1, v2)
128    }
129
130    fn zero(&self) -> Self::Value {
131        self.0.zero()
132    }
133
134    fn to_f64(&self, value: &Self::Value) -> f64 {
135        self.0.to_f64(value)
136    }
137
138    fn formatter(&self) -> &dyn ValueFormatter {
139        self
140    }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
144enum Multiple {
145    One,
146    Kilo,
147    Mega,
148    Giga,
149    Tera,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
153enum Unit {
154    Byte,
155    Elem,
156}
157
158impl Multiple {
159    fn denominator(&self) -> f64 {
160        match *self {
161            Multiple::One => 1.0,
162            Multiple::Kilo => 1_000.0,
163            Multiple::Mega => 1_000_000.0,
164            Multiple::Giga => 1_000_000_000.0,
165            Multiple::Tera => 1_000_000_000_000.0,
166        }
167    }
168}
169
170impl ValueFormatter for DecimalByteMeasurement {
171    fn scale_values(&self, typical_value: f64, values: &mut [f64]) -> &'static str {
172        self.0.formatter().scale_values(typical_value, values)
173    }
174
175    fn scale_throughputs(
176        &self,
177        typical_value: f64,
178        throughput: &criterion::Throughput,
179        values: &mut [f64],
180    ) -> &'static str {
181        use Multiple::*;
182        use Throughput::*;
183        use Unit::*;
184
185        let (total_units, unit) = match *throughput {
186            Bytes(bytes) => (bytes as f64, Byte),
187            Elements(elements) => (elements as f64, Elem),
188        };
189        let units_per_second = total_units * (1e9 / typical_value);
190        let multiple = if units_per_second >= 1e12 {
191            Tera
192        } else if units_per_second >= 1e9 {
193            Giga
194        } else if units_per_second >= 1e6 {
195            Mega
196        } else if units_per_second >= 1e3 {
197            Kilo
198        } else {
199            One
200        };
201        let denominator = multiple.denominator();
202
203        for val in values {
204            let units_per_second = total_units * (1e9 / *val);
205            *val = units_per_second / denominator;
206        }
207
208        match (unit, multiple) {
209            (Byte, One) => " B/s",
210            (Byte, Kilo) => "KB/s",
211            (Byte, Mega) => "MB/s",
212            (Byte, Giga) => "GB/s",
213            (Byte, Tera) => "TB/s",
214            (Elem, One) => " elem/s",
215            (Elem, Kilo) => "Kelem/s",
216            (Elem, Mega) => "Melem/s",
217            (Elem, Giga) => "Gelem/s",
218            (Elem, Tera) => "Telem/s",
219        }
220    }
221
222    fn scale_for_machines(&self, values: &mut [f64]) -> &'static str {
223        self.0.formatter().scale_for_machines(values)
224    }
225}
226
227#[cfg(test)]
228mod test {
229    use super::*;
230    use proptest::prelude::*;
231    use Target::*;
232
233    #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
234    enum Target {
235        One,
236        Kilo,
237        Mega,
238        Giga,
239        Tera,
240    }
241
242    impl Target {
243        fn get_base(self) -> f64 {
244            match self {
245                One => 1.0,
246                Kilo => 1e3,
247                Mega => 1e6,
248                Giga => 1e9,
249                Tera => 1e12,
250            }
251        }
252
253        fn expected_bytes(self) -> &'static str {
254            match self {
255                One => " B/s",
256                Kilo => "KB/s",
257                Mega => "MB/s",
258                Giga => "GB/s",
259                Tera => "TB/s",
260            }
261        }
262
263        fn expected_elems(self) -> &'static str {
264            match self {
265                One => " elem/s",
266                Kilo => "Kelem/s",
267                Mega => "Melem/s",
268                Giga => "Gelem/s",
269                Tera => "Telem/s",
270            }
271        }
272    }
273
274    fn arbitrary_target() -> impl Strategy<Value = Target> {
275        prop_oneof![Just(One), Just(Kilo), Just(Mega), Just(Giga), Just(Tera)]
276    }
277
278    proptest! {
279        #[test]
280        fn scale_throughputs_bytes_gives_correct_unit(target in arbitrary_target(), bytes in any::<u64>()) {
281            // bytes / seconds = target
282            // seconds = bytes / target
283            let thpt_config = Throughput::Bytes(bytes);
284            let seconds = (bytes as f64) / target.get_base();
285            let typical = (seconds * 1e9) * 0.999999;
286
287            let measurement = DecimalByteMeasurement::default();
288            let result = measurement.scale_throughputs(typical, &thpt_config, &mut []);
289
290            assert_eq!(result, target.expected_bytes());
291        }
292
293        #[test]
294        fn scale_throughputs_elems_gives_correct_unit(target in arbitrary_target(), elems in any::<u64>()) {
295            // elems / seconds = target
296            // seconds = elems / target
297            let thpt_config = Throughput::Elements(elems);
298            let seconds = (elems as f64) / target.get_base();
299            let typical = (seconds * 1e9) * 0.999999;
300
301            let measurement = DecimalByteMeasurement::default();
302            let result = measurement.scale_throughputs(typical, &thpt_config, &mut []);
303
304            assert_eq!(result, target.expected_elems());
305        }
306    }
307
308    #[test]
309    fn scale_throughputs_bytes() {
310        let thpt_config = Throughput::Bytes(1_000_000);
311        let typical = 1_000_000_000.0;
312        let mut values = [
313            100_000_000.0,
314            500_000_000.0,
315            999_999_999.0,
316            1_000_000_000.0,
317            1_000_000_001.0,
318            2_000_000_000.0,
319            10_000_000_000.0,
320        ];
321
322        let measurement = DecimalByteMeasurement::default();
323        let result = measurement.scale_throughputs(typical, &thpt_config, &mut values);
324
325        assert_eq!(result, "MB/s");
326        assert_eq!(values, [10.0, 2.0, 1.000000001, 1.0, 0.999999999, 0.5, 0.1]);
327    }
328
329    #[test]
330    fn scale_throughputs_elems_gives_correct_unit_regression1() {
331        let elems = 13302377187617527;
332        let target = Mega;
333
334        let thpt_config = Throughput::Elements(elems);
335        let seconds = (elems as f64) / target.get_base();
336        let typical = (seconds * 1e9) * 0.999999;
337
338        let measurement = DecimalByteMeasurement::default();
339        let result = measurement.scale_throughputs(typical, &thpt_config, &mut []);
340
341        assert_eq!(result, target.expected_elems());
342    }
343}