![Maintenance](https://img.shields.io/badge/maintenance-activly--developed-brightgreen.svg)
# benchmark-rs
A benchmarking library for Rust package authors.
Benchmark-rs crate provides tools for Rust package authors to evaluate performance of their implementation for
varying workloads with varying configurations, find performance regressions between versions and also some crude
facilities to measure execution times of code blocks.
For example, if you implement a concurrent algorithm with an expectation of reduced execution time when more
CPU cores are added, benchmark-rs will help validate that the concurrency has the desired effect and that there
is no anomaly for varying data sets, that is, the algorithm performs consistently within boundaries defined for it.
However, this doesn't have to be a fancy algorithm that we want to test, some banal looking code can exhibit
radically different behavior for different data sets, depending on database queries, buffer sizes, number of
open files, etc. We may want to compare the behavior of our code with different querying strategies or different
buffer sizes for a range of data sets to find the best fit for our use-case.
Another common use-case is to verify that there is no regression in performance introduced by the new code. Benchmark-rs
supports comparison to previous results with user defined equality threshold.
## Design
Benchmark-rs is designed to evaluate performance of subjects processing a set of workloads with modifiable configurations.
For example if we write a protocol parser we may define our workload points as 10MB, 20MB, ..., 100MB input sizes. Workloads
are user defined in benchmark-rs. Any type `W: Clone + Display` can be used to specify workloads. It could
be an integer that specifies the size of the workload, a path to a file or a key that we can use
to fetch the workload from the benchmark configuration. The `Display` is required
for the workload point to produce the key in result series, so it is recommended to keep it short and descriptive.
See examples below.
Each benchmark is repeated `repeat` times for every workload point after it was ramped up for that point. Upon
completion the summary is available as JSON or as CSV.
## Issues
Issues are welcome and appreciated. Please submit to https://github.com/navigatorsguild/benchmark-rs/issues
## Examples
A real world example can be found at
[command-executor](https://github.com/navigatorsguild/command-executor) project
[blocking_queue.rs](https://github.com/navigatorsguild/command-executor/blob/main/benches/blocking_queue.rs)
benchmark and at [Benchmarks](https://github.com/navigatorsguild/command-executor/wiki/Benchmarks)
wiki page which was built from the generated data.
![link](https://user-images.githubusercontent.com/122003456/235414598-727d804a-b8ad-4520-871b-5fd8be33bf44.png)
### Simple Benchmark
A simple benchmark that measures execution time for increasing workloads. In this case the workload is simulated by
by a `u64` value passed to `thread::sleep` function
```rust
use std::thread;
use std::time::Duration;
use benchmark_rs::benchmarks::Benchmarks;
use benchmark_rs::stopwatch::StopWatch;
fn example(_stop_watch: &mut StopWatch, _config: &str, work: u64) -> Result<(), anyhow::Error> {
thread::sleep(Duration::from_millis(work));
Ok(())
}
fn main() -> Result<(), anyhow::Error> {
let mut benchmarks = Benchmarks::new("Example");
benchmarks.add("A Simple Benchmark", example, "No Configuration", (1..=10).collect(), 2, 1)?;
benchmarks.run()?;
let summary = benchmarks.summary_as_json();
println!("Summary: {summary}");
Ok(())
}
```
### Benchmark Workloads
A more complex example that shows how to use Benchmark configuration and how to control the
stopwatch from within the benchmark to avoid measuring the housekeeping tasks.
Benchmark Workloads Example
```rust
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use std::thread;
use std::time::Duration;
use benchmark_rs::benchmarks::Benchmarks;
use benchmark_rs::stopwatch::StopWatch;
#[derive(Clone)]
struct Config {
// simulate available resources - CPU cores, memory buffers, etc.
pub resources: u32,
pub workloads: BTreeMap,
}
impl Display for Config {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let keys: Vec = self.workloads.keys().map(|k| k.to_string()).collect();
write!(f, "{}", keys.join(", "))
}
}
fn example(stop_watch: &mut StopWatch, config: Config, work: u64) -> Result<(), anyhow::Error> {
stop_watch.pause();
// perform potentially lengthy preparation that will not reflect in the measurement
// ...
// fetch the workload definition from configuration associated with the 'work' point
let sleep_time = config.workloads.get(&work).unwrap().clone();
// resume the stopwatch to measure the actual work
stop_watch.resume();
// perform measured computation
thread::sleep(sleep_time / config.resources);
stop_watch.pause();
// perform potentially lengthy cleanup
// ...
Ok(())
}
fn main() -> Result<(), anyhow::Error> {
let mut benchmarks = Benchmarks::new("benchmark-workloads");
let workloads: BTreeMap =
(0..=10).map(|i| (i, Duration::from_millis(i))).collect();
benchmarks.add(
"benchmark-workload-1",
example,
Config {
resources: 1,
workloads: workloads.clone(),
},
(1..=10).collect(),
2,
1,
)?;
benchmarks.add(
"benchmark-workload-2",
example,
Config {
resources: 2,
workloads: workloads.clone(),
},
(1..=10).collect(),
2,
1,
)?;
benchmarks.run()?;
let summary = benchmarks.summary_as_json();
println!("Benchmark summary in JSON format.");
println!("Summary:");
println!("{summary}");
println!();
println!("Benchmark series in CSV format.");
let csv_data = benchmarks.summary_as_csv(true, false);
for (k, v) in csv_data {
println!("Benchmark name: {k}");
for line in v {
println!("{line}")
}
println!();
}
Ok(())
}
```
Benchmark Workloads Summary
```json
{
"name": "benchmark-workloads",
"created_at": "2023-08-13 04:09:13.923036",
"series": {
"benchmark-workload-2": {
"name": "benchmark-workload-2",
"config": "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10",
"runs": [
[
"1",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 639791,
"min_sec": 0.000639791,
"min_str": "00:00:00.000",
"max_nanos": 644250,
"max_sec": 0.00064425,
"max_str": "00:00:00.000",
"median_nanos": 642020,
"median_sec": 0.00064202,
"median_str": "00:00:00.000",
"std_dev": 3152.9891373108153,
"std_dev_sec": 3.1529891373108154e-6,
"std_dev_str": "00:00:00.000"
}
],
[
"2",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 1264124,
"min_sec": 0.001264124,
"min_str": "00:00:00.001",
"max_nanos": 1264250,
"max_sec": 0.00126425,
"max_str": "00:00:00.001",
"median_nanos": 1264187,
"median_sec": 0.001264187,
"median_str": "00:00:00.001",
"std_dev": 89.09545442950498,
"std_dev_sec": 8.909545442950498e-8,
"std_dev_str": "00:00:00.000"
}
],
[
"3",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 1891833,
"min_sec": 0.001891833,
"min_str": "00:00:00.001",
"max_nanos": 1892875,
"max_sec": 0.001892875,
"max_str": "00:00:00.001",
"median_nanos": 1892354,
"median_sec": 0.001892354,
"median_str": "00:00:00.001",
"std_dev": 736.8052659963826,
"std_dev_sec": 7.368052659963826e-7,
"std_dev_str": "00:00:00.000"
}
],
[
"4",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 2566417,
"min_sec": 0.002566417,
"min_str": "00:00:00.002",
"max_nanos": 2576416,
"max_sec": 0.002576416,
"max_str": "00:00:00.002",
"median_nanos": 2571416,
"median_sec": 0.002571416,
"median_str": "00:00:00.002",
"std_dev": 7070.360705084288,
"std_dev_sec": 7.0703607050842884e-6,
"std_dev_str": "00:00:00.000"
}
],
[
"5",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 2928666,
"min_sec": 0.002928666,
"min_str": "00:00:00.002",
"max_nanos": 3174917,
"max_sec": 0.003174917,
"max_str": "00:00:00.003",
"median_nanos": 3051791,
"median_sec": 0.003051791,
"median_str": "00:00:00.003",
"std_dev": 174125.75197396852,
"std_dev_sec": 0.00017412575197396852,
"std_dev_str": "00:00:00.000"
}
],
[
"6",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 3377791,
"min_sec": 0.003377791,
"min_str": "00:00:00.003",
"max_nanos": 3784625,
"max_sec": 0.003784625,
"max_str": "00:00:00.003",
"median_nanos": 3581208,
"median_sec": 0.003581208,
"median_str": "00:00:00.003",
"std_dev": 287675.0802172479,
"std_dev_sec": 0.00028767508021724787,
"std_dev_str": "00:00:00.000"
}
],
[
"7",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 4404499,
"min_sec": 0.004404499,
"min_str": "00:00:00.004",
"max_nanos": 4915541,
"max_sec": 0.004915541,
"max_str": "00:00:00.004",
"median_nanos": 4660020,
"median_sec": 0.00466002,
"median_str": "00:00:00.004",
"std_dev": 361361.26367113565,
"std_dev_sec": 0.00036136126367113564,
"std_dev_str": "00:00:00.000"
}
],
[
"8",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 4882584,
"min_sec": 0.004882584,
"min_str": "00:00:00.004",
"max_nanos": 5029209,
"max_sec": 0.005029209,
"max_str": "00:00:00.005",
"median_nanos": 4955896,
"median_sec": 0.004955896,
"median_str": "00:00:00.004",
"std_dev": 103679.53179147754,
"std_dev_sec": 0.00010367953179147753,
"std_dev_str": "00:00:00.000"
}
],
[
"9",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 5638959,
"min_sec": 0.005638959,
"min_str": "00:00:00.005",
"max_nanos": 5643416,
"max_sec": 0.005643416,
"max_str": "00:00:00.005",
"median_nanos": 5641187,
"median_sec": 0.005641187,
"median_str": "00:00:00.005",
"std_dev": 3151.5749237484424,
"std_dev_sec": 3.1515749237484423e-6,
"std_dev_str": "00:00:00.000"
}
],
[
"10",
{
"name": "benchmark-workload-2",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 5086250,
"min_sec": 0.00508625,
"min_str": "00:00:00.005",
"max_nanos": 5587292,
"max_sec": 0.005587292,
"max_str": "00:00:00.005",
"median_nanos": 5336771,
"median_sec": 0.005336771,
"median_str": "00:00:00.005",
"std_dev": 354290.19585927017,
"std_dev_sec": 0.00035429019585927015,
"std_dev_str": "00:00:00.000"
}
]
]
},
"benchmark-workload-1": {
"name": "benchmark-workload-1",
"config": "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10",
"runs": [
[
"1",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 1259375,
"min_sec": 0.001259375,
"min_str": "00:00:00.001",
"max_nanos": 1276125,
"max_sec": 0.001276125,
"max_str": "00:00:00.001",
"median_nanos": 1267750,
"median_sec": 0.00126775,
"median_str": "00:00:00.001",
"std_dev": 11844.038584874672,
"std_dev_sec": 0.000011844038584874672,
"std_dev_str": "00:00:00.000"
}
],
[
"2",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 2513584,
"min_sec": 0.002513584,
"min_str": "00:00:00.002",
"max_nanos": 2520875,
"max_sec": 0.002520875,
"max_str": "00:00:00.002",
"median_nanos": 2517229,
"median_sec": 0.002517229,
"median_str": "00:00:00.002",
"std_dev": 5155.515541631118,
"std_dev_sec": 5.155515541631118e-6,
"std_dev_str": "00:00:00.000"
}
],
[
"3",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 3755750,
"min_sec": 0.00375575,
"min_str": "00:00:00.003",
"max_nanos": 3784000,
"max_sec": 0.003784,
"max_str": "00:00:00.003",
"median_nanos": 3769875,
"median_sec": 0.003769875,
"median_str": "00:00:00.003",
"std_dev": 19975.766568519968,
"std_dev_sec": 0.00001997576656851997,
"std_dev_str": "00:00:00.000"
}
],
[
"4",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 5003833,
"min_sec": 0.005003833,
"min_str": "00:00:00.005",
"max_nanos": 5030084,
"max_sec": 0.005030084,
"max_str": "00:00:00.005",
"median_nanos": 5016958,
"median_sec": 0.005016958,
"median_str": "00:00:00.005",
"std_dev": 18562.26011292806,
"std_dev_sec": 0.00001856226011292806,
"std_dev_str": "00:00:00.000"
}
],
[
"5",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 5518500,
"min_sec": 0.0055185,
"min_str": "00:00:00.005",
"max_nanos": 6283084,
"max_sec": 0.006283084,
"max_str": "00:00:00.006",
"median_nanos": 5900792,
"median_sec": 0.005900792,
"median_str": "00:00:00.005",
"std_dev": 540642.5311867353,
"std_dev_sec": 0.0005406425311867353,
"std_dev_str": "00:00:00.000"
}
],
[
"6",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 6942292,
"min_sec": 0.006942292,
"min_str": "00:00:00.006",
"max_nanos": 7541458,
"max_sec": 0.007541458,
"max_str": "00:00:00.007",
"median_nanos": 7241875,
"median_sec": 0.007241875,
"median_str": "00:00:00.007",
"std_dev": 423674.3416564189,
"std_dev_sec": 0.00042367434165641895,
"std_dev_str": "00:00:00.000"
}
],
[
"7",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 8784417,
"min_sec": 0.008784417,
"min_str": "00:00:00.008",
"max_nanos": 8812875,
"max_sec": 0.008812875,
"max_str": "00:00:00.008",
"median_nanos": 8798646,
"median_sec": 0.008798646,
"median_str": "00:00:00.008",
"std_dev": 20122.84477900677,
"std_dev_sec": 0.00002012284477900677,
"std_dev_str": "00:00:00.000"
}
],
[
"8",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 9426875,
"min_sec": 0.009426875,
"min_str": "00:00:00.009",
"max_nanos": 11596125,
"max_sec": 0.011596125,
"max_str": "00:00:00.011",
"median_nanos": 10511500,
"median_sec": 0.0105115,
"median_str": "00:00:00.010",
"std_dev": 1533891.3850889183,
"std_dev_sec": 0.0015338913850889183,
"std_dev_str": "00:00:00.001"
}
],
[
"9",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 9498333,
"min_sec": 0.009498333,
"min_str": "00:00:00.009",
"max_nanos": 9953333,
"max_sec": 0.009953333,
"max_str": "00:00:00.009",
"median_nanos": 9725833,
"median_sec": 0.009725833,
"median_str": "00:00:00.009",
"std_dev": 321733.5854398791,
"std_dev_sec": 0.0003217335854398791,
"std_dev_str": "00:00:00.000"
}
],
[
"10",
{
"name": "benchmark-workload-1",
"ramp_up": 1,
"repeat": 2,
"min_nanos": 11674000,
"min_sec": 0.011674,
"min_str": "00:00:00.011",
"max_nanos": 12627167,
"max_sec": 0.012627167,
"max_str": "00:00:00.012",
"median_nanos": 12150583,
"median_sec": 0.012150583,
"median_str": "00:00:00.012",
"std_dev": 673990.849303238,
"std_dev_sec": 0.0006739908493032379,
"std_dev_str": "00:00:00.000"
}
]
]
}
}
}
```
Benchmark Workloads Series
Benchmark name: benchmark-workload-2
```csv
point,ramp_up,repeat,min_sec,max_sec,median_sec,std_dev_sec
1,1,2,0.00063575,0.000637376,0.000636563,0.0000011497556262093261
2,1,2,0.001265333,0.001269584,0.001267458,0.0000030059109268240135
3,1,2,0.001890958,0.001892333,0.001891645,0.0000009722718241315028
4,1,2,0.002512042,0.002604791,0.002558416,0.0000655834468482711
5,1,2,0.003129084,0.00314525,0.003137167,0.000011431088224661727
6,1,2,0.003092583,0.003767833,0.003430208,0.0004774738539962162
7,1,2,0.004055333,0.004084166,0.004069749,0.000020388009821951726
8,1,2,0.004171542,0.005756541,0.004964041,0.0011207635410738967
9,1,2,0.004549334,0.005149792,0.004849563,0.0004245879236177119
10,1,2,0.006351,0.007404083,0.006877541,0.000744642130452273
```
Benchmark name: benchmark-workload-1
```csv
point,ramp_up,repeat,min_sec,max_sec,median_sec,std_dev_sec
1,1,2,0.001265417,0.001266709,0.001266063,0.0000009135819612930194
2,1,2,0.002518501,0.002534999,0.00252675,0.000011665847676015662
3,1,2,0.003762958,0.004072459,0.003917708,0.00021885025588401765
4,1,2,0.004471833,0.004559375,0.004515604,0.00006190154183863275
5,1,2,0.005676917,0.005765751,0.005721334,0.00006281512379992577
6,1,2,0.006610875,0.007539833,0.007075354,0.0006568725012374928
7,1,2,0.007831792,0.008021959,0.007926875,0.0001344683752579022
8,1,2,0.009668583,0.009832959,0.009750771,0.00011623138426431993
9,1,2,0.009677333,0.011178501,0.010427917,0.0010614860725002473
10,1,2,0.011440417,0.012526,0.011983208,0.0007676231008408358
```
Find Regressions Example
```rust
use std::collections::BTreeMap;
use std::fmt::{Display, Formatter};
use std::thread;
use std::time::Duration;
use benchmark_rs::benchmarks::Benchmarks;
use benchmark_rs::stopwatch::StopWatch;
use rand::Rng;
#[derive(Clone)]
struct Config {
// simulate available resources - CPU cores, memory buffers, etc.
pub resources: u32,
pub workloads: BTreeMap,
}
impl Display for Config {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for k in self.workloads.keys() {
write!(f, "{k} ")?;
}
Ok(())
}
}
fn example(_stop_watch: &mut StopWatch, config: Config, work: u64) -> Result<(), anyhow::Error> {
let sleep_time = config.workloads.get(&work).unwrap().clone();
thread::sleep(sleep_time / config.resources);
Ok(())
}
fn modified_example(
_stop_watch: &mut StopWatch,
config: Config,
work: u64,
) -> Result<(), anyhow::Error> {
// we introduce a random deviation in modified example to simulate
// regression introduced by a code change
let deviation = Duration::from_millis(rand::thread_rng().gen_range(0..10));
let sleep_time = config.workloads.get(&work).unwrap().clone();
thread::sleep(sleep_time / config.resources + deviation);
Ok(())
}
fn main() -> Result<(), anyhow::Error> {
let mut previous_benchmarks = Benchmarks::new("benchmarks");
let workloads: BTreeMap = (1..=10)
.map(|i| (i, Duration::from_millis(i * 25)))
.collect();
previous_benchmarks.add(
"benchmark-1",
example,
Config {
resources: 1,
workloads: workloads.clone(),
},
(1..=10).collect(),
5,
3,
)?;
previous_benchmarks.run()?;
let previous_summary = previous_benchmarks.summary_as_json();
let mut current_benchmarks = Benchmarks::new("benchmarks");
current_benchmarks.add(
"benchmark-1",
modified_example,
Config {
resources: 1,
workloads: workloads.clone(),
},
(1..=10).collect(),
5,
3,
)?;
current_benchmarks.add(
"benchmark-2",
modified_example,
Config {
resources: 2,
workloads: workloads.clone(),
},
(1..=10).collect(),
5,
3,
)?;
current_benchmarks.run()?;
// compare results of this run with the results of previous runs with threshold of 5 percent
let analysis_result = current_benchmarks.analyze(Some(previous_summary), 5.0)?;
println!("Analysis result:");
println!("{}", analysis_result.to_string());
assert!(!analysis_result.divergent_series().is_empty());
Ok(())
}
```
Find Regressions Output
Analysis result. Current and previous values are in nanosecond units, the change is in percents.
```json
{
"name": "benchmarks",
"new_series": [
"benchmark-2"
],
"equal_series": {},
"divergent_series": {
"benchmark-1": {
"8": {
"Equal": {
"point": "8",
"previous": 200686042,
"current": 206411375,
"change": 2.852880520709064
}
},
"7": {
"Equal": {
"point": "7",
"previous": 176700166,
"current": 183395333,
"change": 3.788998704166474
}
},
"10": {
"Equal": {
"point": "10",
"previous": 251701083,
"current": 256856000,
"change": 2.0480313149864315
}
},
"2": {
"Equal": {
"point": "2",
"previous": 52684667,
"current": 55052334,
"change": 4.494034288951653
}
},
"5": {
"Equal": {
"point": "5",
"previous": 127510583,
"current": 130817709,
"change": 2.5936090340046434
}
},
"6": {
"Equal": {
"point": "6",
"previous": 152803084,
"current": 158141750,
"change": 3.4938208446106955
}
},
"9": {
"Equal": {
"point": "9",
"previous": 225706250,
"current": 229522083,
"change": 1.6906191122310474
}
},
"1": {
"Greater": {
"point": "1",
"previous": 26413209,
"current": 35264166,
"change": 33.50958605597674
}
},
"4": {
"Equal": {
"point": "4",
"previous": 104201208,
"current": 109166958,
"change": 4.7655397622645665
}
},
"3": {
"Equal": {
"point": "3",
"previous": 79801875,
"current": 84060834,
"change": 5.336915955922095
}
}
}
}
}
```
## Similar Projects
* [criterion](https://crates.io/crates/criterion)
* [iai](https://crates.io/crates/iai)
License: MIT OR Apache-2.0