Skip to content

Commit d386f1e

Browse files
authored
Merge pull request #1463 from rust-lang/runtime-cli-progress
Improve runtime benchmark CLI output
2 parents 05f2005 + 98dc856 commit d386f1e

File tree

15 files changed

+401
-204
lines changed

15 files changed

+401
-204
lines changed

collector/benchlib/src/benchmark.rs

Lines changed: 59 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,43 @@
11
use crate::cli::{parse_cli, Args, BenchmarkArgs};
2+
use crate::comm::messages::{BenchmarkMessage, BenchmarkResult, BenchmarkStats};
3+
use crate::comm::output_message;
24
use crate::measure::benchmark_function;
3-
use crate::messages::BenchmarkResult;
45
use crate::process::raise_process_priority;
5-
use log::LevelFilter;
66
use std::collections::HashMap;
77

8-
/// Create a new benchmark suite. Use the closure argument to define benchmarks.
9-
pub fn benchmark_suite<F: FnOnce(&mut BenchmarkSuite)>(define_func: F) {
10-
env_logger::Builder::from_default_env()
11-
.filter_level(LevelFilter::Info)
12-
.init();
13-
let mut suite = BenchmarkSuite::new();
14-
define_func(&mut suite);
15-
suite.run().expect("Benchmark suite has failed");
8+
/// Create a new benchmark group. Use the closure argument to define individual benchmarks.
9+
pub fn run_benchmark_group<F: FnOnce(&mut BenchmarkGroup)>(define_func: F) {
10+
env_logger::init();
11+
12+
let mut group = BenchmarkGroup::new();
13+
define_func(&mut group);
14+
group.run().expect("Benchmark group execution has failed");
1615
}
1716

18-
/// Type-erased function that performs a benchmark.
17+
/// Type-erased function that executes a single benchmark.
1918
struct BenchmarkWrapper {
20-
func: Box<dyn Fn() -> anyhow::Result<BenchmarkResult>>,
19+
func: Box<dyn Fn() -> anyhow::Result<BenchmarkStats>>,
2120
}
2221

23-
type BenchmarkMap = HashMap<&'static str, BenchmarkWrapper>;
24-
2522
#[derive(Default)]
26-
pub struct BenchmarkSuite {
27-
benchmarks: BenchmarkMap,
23+
pub struct BenchmarkGroup {
24+
benchmarks: HashMap<&'static str, BenchmarkWrapper>,
2825
}
2926

30-
impl BenchmarkSuite {
27+
impl BenchmarkGroup {
3128
pub fn new() -> Self {
3229
Self::default()
3330
}
3431

3532
/// Registers a single benchmark.
36-
/// `func` should return a closure that will be benchmarked.
33+
/// `constructor` should return a closure that will be benchmarked.
3734
pub fn register<F: Fn() -> Bench + Clone + 'static, R, Bench: FnOnce() -> R + 'static>(
3835
&mut self,
3936
name: &'static str,
4037
constructor: F,
4138
) {
4239
// We want to type-erase the target `func` by wrapping it in a Box.
43-
let benchmark_func = Box::new(move || benchmark_function(name, constructor.clone()));
40+
let benchmark_func = Box::new(move || benchmark_function(constructor.clone()));
4441
let benchmark_def = BenchmarkWrapper {
4542
func: benchmark_func,
4643
};
@@ -49,62 +46,81 @@ impl BenchmarkSuite {
4946
}
5047
}
5148

52-
/// Execute the benchmark suite. It will parse CLI arguments and decide what to do based on
49+
/// Execute the benchmark group. It will parse CLI arguments and decide what to do based on
5350
/// them.
5451
pub fn run(self) -> anyhow::Result<()> {
5552
raise_process_priority();
5653

5754
let args = parse_cli()?;
5855
match args {
59-
Args::Benchmark(args) => {
60-
run_benchmark(args, self.benchmarks)?;
56+
Args::Run(args) => {
57+
self.run_benchmarks(args)?;
6158
}
59+
Args::List => self.list_benchmarks()?,
6260
}
6361

6462
Ok(())
6563
}
66-
}
6764

68-
fn run_benchmark(args: BenchmarkArgs, benchmarks: BenchmarkMap) -> anyhow::Result<()> {
69-
let mut items: Vec<(&'static str, BenchmarkWrapper)> = benchmarks
70-
.into_iter()
71-
.filter(|(name, _)| passes_filter(name, args.exclude.as_deref(), args.include.as_deref()))
72-
.collect();
73-
items.sort_unstable_by_key(|item| item.0);
74-
75-
let mut results: Vec<BenchmarkResult> = Vec::with_capacity(items.len());
76-
for (name, def) in items {
77-
for i in 0..args.iterations {
78-
let result = (def.func)()?;
79-
log::info!("Benchmark (run {i}) `{name}` completed: {result:?}");
80-
results.push(result);
65+
fn run_benchmarks(self, args: BenchmarkArgs) -> anyhow::Result<()> {
66+
let mut items: Vec<(&'static str, BenchmarkWrapper)> = self
67+
.benchmarks
68+
.into_iter()
69+
.filter(|(name, _)| {
70+
passes_filter(name, args.exclude.as_deref(), args.include.as_deref())
71+
})
72+
.collect();
73+
items.sort_unstable_by_key(|item| item.0);
74+
75+
let mut stdout = std::io::stdout().lock();
76+
77+
for (name, def) in items {
78+
let mut stats: Vec<BenchmarkStats> = Vec::with_capacity(args.iterations as usize);
79+
for i in 0..args.iterations {
80+
let benchmark_stats = (def.func)()?;
81+
log::info!("Benchmark (run {i}) `{name}` completed: {benchmark_stats:?}");
82+
stats.push(benchmark_stats);
83+
}
84+
output_message(
85+
&mut stdout,
86+
BenchmarkMessage::Result(BenchmarkResult {
87+
name: name.to_string(),
88+
stats,
89+
}),
90+
)?;
8191
}
92+
93+
Ok(())
8294
}
8395

84-
println!("{}", serde_json::to_string(&results)?);
85-
Ok(())
96+
fn list_benchmarks(self) -> anyhow::Result<()> {
97+
let benchmark_list: Vec<&str> = self.benchmarks.into_keys().collect();
98+
serde_json::to_writer(std::io::stdout(), &benchmark_list)?;
99+
100+
Ok(())
101+
}
86102
}
87103

88-
/// Adds a single benchmark to the benchmark suite.
104+
/// Adds a single benchmark to the benchmark group.
89105
/// ```ignore
90106
/// use benchlib::define_benchmark;
91107
///
92-
/// define_benchmark!(suite, my_bench, {
108+
/// define_benchmark!(group, my_bench, {
93109
/// || do_something()
94110
/// });
95111
/// ```
96112
#[macro_export]
97113
macro_rules! define_benchmark {
98-
($suite:expr, $name:ident, $fun:expr) => {
114+
($group:expr, $name:ident, $fun:expr) => {
99115
let func = move || $fun;
100-
$suite.register(stringify!($name), func);
116+
$group.register(stringify!($name), func);
101117
};
102118
}
103119

104120
pub use define_benchmark;
105121

106122
/// Tests if the name of the benchmark passes through the include and exclude filter flags.
107-
fn passes_filter(name: &str, exclude: Option<&str>, include: Option<&str>) -> bool {
123+
pub fn passes_filter(name: &str, exclude: Option<&str>, include: Option<&str>) -> bool {
108124
match (exclude, include) {
109125
(Some(exclude), Some(include)) => name.starts_with(include) && !name.starts_with(exclude),
110126
(None, Some(include)) => name.starts_with(include),

collector/benchlib/src/cli.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use clap::{FromArgMatches, IntoApp};
22

33
#[derive(clap::Parser, Debug)]
44
pub enum Args {
5-
/// Benchmark all benchmarks in this benchmark suite and print the results as JSON.
6-
Benchmark(BenchmarkArgs),
5+
/// Benchmark all benchmarks in this benchmark group and print the results as JSON.
6+
Run(BenchmarkArgs),
7+
/// List benchmarks that are defined in the current group as a JSON array.
8+
List,
79
}
810

911
#[derive(clap::Parser, Debug)]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//! This module defines messages that are exchanged between a binary that uses `benchlib` and
2+
//! the `collector` crate.
3+
4+
use std::time::Duration;
5+
6+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
7+
pub enum BenchmarkMessage {
8+
Result(BenchmarkResult),
9+
}
10+
11+
/// Stats gathered by several iterations of a single benchmark.
12+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
13+
pub struct BenchmarkResult {
14+
pub name: String,
15+
pub stats: Vec<BenchmarkStats>,
16+
}
17+
18+
/// The stats gathered by a single benchmark execution.
19+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
20+
pub struct BenchmarkStats {
21+
pub cycles: u64,
22+
pub instructions: u64,
23+
pub branch_misses: u64,
24+
pub cache_misses: u64,
25+
pub cache_references: u64,
26+
pub wall_time: Duration,
27+
}

collector/benchlib/src/comm/mod.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use crate::comm::messages::BenchmarkMessage;
2+
use std::io::{BufRead, BufReader, Read, Write};
3+
4+
pub mod messages;
5+
6+
/// Messages are communicated as line-delimited JSON.
7+
pub fn output_message<W: Write>(mut sink: W, message: BenchmarkMessage) -> anyhow::Result<()> {
8+
serde_json::to_writer(&mut sink, &message)?;
9+
sink.write_all(b"\n")?;
10+
Ok(())
11+
}
12+
13+
pub struct MessageReader<R> {
14+
inner: BufReader<R>,
15+
line: String,
16+
}
17+
18+
impl<R: Read> MessageReader<R> {
19+
pub fn new(inner: R) -> Self {
20+
Self {
21+
inner: BufReader::new(inner),
22+
line: Default::default(),
23+
}
24+
}
25+
}
26+
27+
impl<R: Read> Iterator for MessageReader<R> {
28+
type Item = anyhow::Result<BenchmarkMessage>;
29+
30+
fn next(&mut self) -> Option<Self::Item> {
31+
match self.inner.read_line(&mut self.line) {
32+
Ok(0) => None,
33+
Ok(_) => match serde_json::from_str(&self.line) {
34+
Ok(value) => Some(Ok(value)),
35+
Err(error) => Some(Err(error.into())),
36+
},
37+
Err(error) => Some(Err(error.into())),
38+
}
39+
}
40+
}

collector/benchlib/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
1616
pub mod benchmark;
1717
mod cli;
18+
pub mod comm;
1819
pub mod measure;
19-
pub mod messages;
2020
pub mod process;

collector/benchlib/src/measure/perf_counter/unix.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::benchmark::black_box;
2-
use crate::messages::BenchmarkResult;
2+
use crate::comm::messages::BenchmarkStats;
33
use perf_event::events::Hardware;
44
use perf_event::{Builder, Counter, Group};
55
use std::time::Instant;
@@ -16,9 +16,8 @@ struct Counters {
1616
/// The function is executed twice, once to gather wall-time measurement and the second time to
1717
/// gather perf. counters.
1818
pub fn benchmark_function<F: Fn() -> Bench + 'static, R, Bench: FnOnce() -> R + 'static>(
19-
name: &'static str,
2019
benchmark_constructor: F,
21-
) -> anyhow::Result<BenchmarkResult> {
20+
) -> anyhow::Result<BenchmarkStats> {
2221
let mut group = create_group()?;
2322
let counters = prepare_counters(&mut group)?;
2423

@@ -48,8 +47,7 @@ pub fn benchmark_function<F: Fn() -> Bench + 'static, R, Bench: FnOnce() -> R +
4847
// Try to avoid optimizing the result out.
4948
black_box(output);
5049

51-
let result = BenchmarkResult {
52-
name: String::from(name),
50+
let result = BenchmarkStats {
5351
cycles: measurement[&counters.cycles],
5452
instructions: measurement[&counters.instructions],
5553
branch_misses: measurement[&counters.branch_misses],
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
use crate::messages::BenchmarkResult;
1+
use crate::comm::messages::BenchmarkStats;
22

3-
pub fn benchmark_function<F: FnOnce() -> R, R>(
4-
_name: &'static str,
5-
_func: F,
6-
) -> anyhow::Result<BenchmarkResult> {
3+
pub fn benchmark_function<F: FnOnce() -> R, R>(_func: F) -> anyhow::Result<BenchmarkStats> {
74
panic!("Runtime benchmarking is not supported on Windows");
85
}

collector/benchlib/src/messages.rs

Lines changed: 0 additions & 13 deletions
This file was deleted.

collector/runtime-benchmarks/bufreader/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::io::{BufRead, BufReader, Write};
22

33
use snap::{read::FrameDecoder, write::FrameEncoder};
44

5-
use benchlib::benchmark::{benchmark_suite, black_box};
5+
use benchlib::benchmark::{black_box, run_benchmark_group};
66
use benchlib::define_benchmark;
77

88
const BYTES: usize = 64 * 1024 * 1024;
@@ -11,8 +11,8 @@ fn main() {
1111
// Inspired by https://github.com/rust-lang/rust/issues/102727
1212
// The pattern we want is a BufReader which wraps a Read impl where one Read::read call will
1313
// never fill the whole BufReader buffer.
14-
benchmark_suite(|suite| {
15-
define_benchmark!(suite, bufreader_snappy, {
14+
run_benchmark_group(|group| {
15+
define_benchmark!(group, bufreader_snappy, {
1616
let data = vec![0u8; BYTES];
1717
move || {
1818
let mut compressed = Vec::new();

collector/runtime-benchmarks/hashmap/src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use benchlib;
2-
use benchlib::benchmark::benchmark_suite;
2+
use benchlib::benchmark::run_benchmark_group;
33
use benchlib::define_benchmark;
44

55
fn main() {
6-
benchmark_suite(|suite| {
6+
run_benchmark_group(|group| {
77
// Measures how long does it take to insert 10 thousand numbers into a `hashbrown` hashmap.
8-
define_benchmark!(suite, hashmap_insert_10k, {
8+
define_benchmark!(group, hashmap_insert_10k, {
99
let mut map = hashbrown::HashMap::with_capacity_and_hasher(
1010
10000,
1111
fxhash::FxBuildHasher::default(),

0 commit comments

Comments
 (0)