Skip to content

Commit 4042bc1

Browse files
committed
Print runtime benchmark results interactively
1 parent fb05da8 commit 4042bc1

File tree

8 files changed

+135
-55
lines changed

8 files changed

+135
-55
lines changed

collector/benchlib/src/benchmark.rs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,22 @@
11
use crate::cli::{parse_cli, Args, BenchmarkArgs};
2+
use crate::comm::messages::{BenchmarkMeasurement, BenchmarkMessage, 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

88
/// Create a new benchmark suite. Use the closure argument to define benchmarks.
99
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();
10+
env_logger::init();
11+
1312
let mut suite = BenchmarkSuite::new();
1413
define_func(&mut suite);
1514
suite.run().expect("Benchmark suite has failed");
1615
}
1716

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

2322
type BenchmarkMap = HashMap<&'static str, BenchmarkWrapper>;
@@ -80,16 +79,25 @@ fn run_benchmark(args: BenchmarkArgs, benchmarks: BenchmarkMap) -> anyhow::Resul
8079
.collect();
8180
items.sort_unstable_by_key(|item| item.0);
8281

83-
let mut results: Vec<BenchmarkResult> = Vec::with_capacity(items.len());
82+
let mut stdout = std::io::stdout().lock();
83+
8484
for (name, def) in items {
85+
let mut measurements: Vec<BenchmarkMeasurement> =
86+
Vec::with_capacity(args.iterations as usize);
8587
for i in 0..args.iterations {
86-
let result = (def.func)()?;
87-
log::info!("Benchmark (run {i}) `{name}` completed: {result:?}");
88-
results.push(result);
88+
let measurement = (def.func)()?;
89+
log::info!("Benchmark (run {i}) `{name}` completed: {measurement:?}");
90+
measurements.push(measurement);
8991
}
92+
output_message(
93+
&mut stdout,
94+
BenchmarkMessage::Stats(BenchmarkStats {
95+
name: name.to_string(),
96+
measurements,
97+
}),
98+
)?;
9099
}
91100

92-
println!("{}", serde_json::to_string(&results)?);
93101
Ok(())
94102
}
95103

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+
Stats(BenchmarkStats),
9+
}
10+
11+
/// Results of several measurements of a single benchmark.
12+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
13+
pub struct BenchmarkStats {
14+
pub name: String,
15+
pub measurements: Vec<BenchmarkMeasurement>,
16+
}
17+
18+
/// Results of a single benchmark execution.
19+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
20+
pub struct BenchmarkMeasurement {
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 & 4 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::BenchmarkMeasurement;
33
use perf_event::events::Hardware;
44
use perf_event::{Builder, Counter, Group};
55
use std::time::Instant;
@@ -18,7 +18,7 @@ struct Counters {
1818
pub fn benchmark_function<F: Fn() -> Bench + 'static, R, Bench: FnOnce() -> R + 'static>(
1919
name: &'static str,
2020
benchmark_constructor: F,
21-
) -> anyhow::Result<BenchmarkResult> {
21+
) -> anyhow::Result<BenchmarkMeasurement> {
2222
let mut group = create_group()?;
2323
let counters = prepare_counters(&mut group)?;
2424

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

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

33
pub fn benchmark_function<F: FnOnce() -> R, R>(
44
_name: &'static str,
55
_func: F,
6-
) -> anyhow::Result<BenchmarkResult> {
6+
) -> anyhow::Result<BenchmarkStats> {
77
panic!("Runtime benchmarking is not supported on Windows");
88
}

collector/benchlib/src/messages.rs

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

collector/src/runtime.rs

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
use crate::benchmark::profile::Profile;
22
use crate::toolchain::{get_local_toolchain, LocalToolchain};
33
use benchlib::benchmark::passes_filter;
4-
use benchlib::messages::BenchmarkResult;
4+
use benchlib::comm::messages::BenchmarkMessage;
55
use cargo_metadata::Message;
6+
use std::io::{BufRead, BufReader};
67
use std::path::{Path, PathBuf};
7-
use std::process::Command;
8+
use std::process::{Command, Stdio};
89

910
#[derive(Debug)]
1011
struct BenchmarkBinary {
1112
path: PathBuf,
1213
benchmark_names: Vec<String>,
1314
}
1415

16+
impl BenchmarkBinary {
17+
fn name(&self) -> &str {
18+
self.path.file_name().unwrap().to_str().unwrap()
19+
}
20+
}
21+
1522
#[derive(Debug)]
1623
struct BenchmarkDatabase {
1724
binaries: Vec<BenchmarkBinary>,
@@ -73,25 +80,40 @@ pub fn bench_runtime(
7380
total_benchmark_count - filtered
7481
);
7582

83+
let mut benchmark_index = 0;
7684
for binary in benchmark_db.binaries {
77-
let name = binary.path.file_name().and_then(|s| s.to_str()).unwrap();
78-
79-
let data: Vec<BenchmarkResult> = execute_runtime_binary(&binary.path, name, &filter)?;
80-
// TODO: do something with the result
81-
println!("{name}: {:?}", data);
85+
for message in execute_runtime_benchmark(&binary.path, &filter)? {
86+
let message = message.map_err(|err| {
87+
anyhow::anyhow!(
88+
"Cannot parse BenchmarkMessage from benchmark {}: {err:?}",
89+
binary.path.display()
90+
)
91+
})?;
92+
match message {
93+
BenchmarkMessage::Stats(stats) => {
94+
benchmark_index += 1;
95+
println!(
96+
"Finished {}/{} ({}/{})",
97+
binary.name(),
98+
stats.name,
99+
benchmark_index,
100+
filtered
101+
);
102+
}
103+
}
104+
}
82105
}
83106

84107
Ok(())
85108
}
86109

87-
/// Execute a single runtime benchmark suite defined in a binary crate located in
110+
/// Starts executing a single runtime benchmark suite defined in a binary crate located in
88111
/// `runtime-benchmarks`. The binary is expected to use benchlib's `BenchmarkSuite` to execute
89-
/// a set of runtime benchmarks and return a list of `BenchmarkResult`s encoded as JSON.
90-
fn execute_runtime_binary(
112+
/// a set of runtime benchmarks and print `BenchmarkMessage`s encoded as JSON, one per line.
113+
fn execute_runtime_benchmark(
91114
binary: &Path,
92-
name: &str,
93115
filter: &BenchmarkFilter,
94-
) -> anyhow::Result<Vec<BenchmarkResult>> {
116+
) -> anyhow::Result<impl Iterator<Item = anyhow::Result<BenchmarkMessage>>> {
95117
// Turn off ASLR
96118
let mut command = Command::new("setarch");
97119
command.arg(std::env::consts::ARCH);
@@ -106,19 +128,16 @@ fn execute_runtime_binary(
106128
command.args(&["--include", include]);
107129
}
108130

109-
let result = command.output()?;
110-
111-
if !result.status.success() {
112-
return Err(anyhow::anyhow!(
113-
"Failed to run runtime benchmark {name}\n{}\n{}",
114-
String::from_utf8_lossy(&result.stdout),
115-
String::from_utf8_lossy(&result.stderr)
116-
));
117-
}
118-
119-
log::info!("Successfully ran runtime benchmark {name}");
131+
command.stdout(Stdio::piped());
132+
let mut child = command.spawn()?;
133+
let stdout = child.stdout.take().unwrap();
120134

121-
Ok(serde_json::from_slice(&result.stdout)?)
135+
let reader = BufReader::new(stdout);
136+
let iterator = reader.lines().map(|line| {
137+
line.and_then(|line| Ok(serde_json::from_str::<BenchmarkMessage>(&line)?))
138+
.map_err(|err| err.into())
139+
});
140+
Ok(iterator)
122141
}
123142

124143
/// Compiles all runtime benchmarks and returns the stdout output of Cargo.

0 commit comments

Comments
 (0)