216 lines
7.5 KiB
Rust
216 lines
7.5 KiB
Rust
|
|
//! Benchmark suite for roto — themed after the 1995 film *Hackers*.
|
||
|
|
//!
|
||
|
|
//! Proto schema: `proto/hackers.proto`
|
||
|
|
//! Generated types: `src/hackers.rs` (via `protoc-gen-roto`)
|
||
|
|
//!
|
||
|
|
//! # Setup
|
||
|
|
//!
|
||
|
|
//! Generate the data files once before running benchmarks:
|
||
|
|
//!
|
||
|
|
//! ```sh
|
||
|
|
//! cargo run --release --bin gen_bench_data -- --preset tiny
|
||
|
|
//! cargo run --release --bin gen_bench_data -- --preset small
|
||
|
|
//! cargo run --release --bin gen_bench_data -- --preset medium
|
||
|
|
//! cargo run --release --bin gen_bench_data -- --preset large
|
||
|
|
//! ```
|
||
|
|
//!
|
||
|
|
//! Then run:
|
||
|
|
//!
|
||
|
|
//! ```sh
|
||
|
|
//! cargo bench --bench hackers_bench
|
||
|
|
//! ```
|
||
|
|
//!
|
||
|
|
//! Benchmark groups:
|
||
|
|
//! - `shallow_parse` — `Campaign::new(data)`, one scan of the whole blob
|
||
|
|
//! - `deep_parse` — Campaign → Operations → Hackers (a `::new()` per level)
|
||
|
|
//! - `field_access` — individual field reads on pre-parsed messages (O(1))
|
||
|
|
//! - `iterate` — counting repeated fields at different nesting depths
|
||
|
|
|
||
|
|
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
|
||
|
|
use roto::hackers::{Campaign, Hacker, Operation, Worm};
|
||
|
|
use std::hint::black_box;
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Data loading
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
/// Load a pre-generated data file from `data/bench/<name>.pb`.
|
||
|
|
/// Returns `None` (and prints a hint) if the file does not exist.
|
||
|
|
fn load(name: &str) -> Option<Vec<u8>> {
|
||
|
|
let path = format!("data/bench/{name}.pb");
|
||
|
|
match std::fs::read(&path) {
|
||
|
|
Ok(data) => Some(data),
|
||
|
|
Err(_) => {
|
||
|
|
eprintln!(
|
||
|
|
"[skip] {path} not found — \
|
||
|
|
run `cargo run --release --bin gen_bench_data -- --preset {name}` first"
|
||
|
|
);
|
||
|
|
None
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// Benchmarks
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
/// `Campaign::new()` — one linear scan to record field offsets, no allocation.
|
||
|
|
/// Throughput reported in MB/s so different sizes are directly comparable.
|
||
|
|
fn bench_shallow_parse(c: &mut Criterion) {
|
||
|
|
let cases = [
|
||
|
|
("tiny", load("tiny")),
|
||
|
|
("small", load("small")),
|
||
|
|
("medium", load("medium")),
|
||
|
|
("large", load("large")),
|
||
|
|
];
|
||
|
|
|
||
|
|
let mut group = c.benchmark_group("shallow_parse");
|
||
|
|
for (label, maybe_data) in &cases {
|
||
|
|
let Some(data) = maybe_data else { continue };
|
||
|
|
group.throughput(Throughput::Bytes(data.len() as u64));
|
||
|
|
group.bench_with_input(BenchmarkId::new("Campaign::new", label), data, |b, data| {
|
||
|
|
b.iter(|| Campaign::new(black_box(data)).unwrap())
|
||
|
|
});
|
||
|
|
}
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Walk every level of the tree: Campaign → Operations → Hackers.
|
||
|
|
/// Each `::new()` is an additional linear scan of that sub-message's bytes.
|
||
|
|
fn bench_deep_parse(c: &mut Criterion) {
|
||
|
|
let cases = [
|
||
|
|
("tiny", load("tiny")),
|
||
|
|
("small", load("small")),
|
||
|
|
("medium", load("medium")),
|
||
|
|
];
|
||
|
|
|
||
|
|
let mut group = c.benchmark_group("deep_parse");
|
||
|
|
for (label, maybe_data) in &cases {
|
||
|
|
let Some(data) = maybe_data else { continue };
|
||
|
|
group.throughput(Throughput::Bytes(data.len() as u64));
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::new("Campaign+Ops+Hackers", label),
|
||
|
|
data,
|
||
|
|
|b, data| {
|
||
|
|
b.iter(|| {
|
||
|
|
let campaign = Campaign::new(data).unwrap();
|
||
|
|
let mut hacker_count = 0usize;
|
||
|
|
for op_res in campaign.operations() {
|
||
|
|
let (op_bytes, _) = op_res.unwrap();
|
||
|
|
let op = Operation::new(op_bytes).unwrap();
|
||
|
|
for crew_res in op.crew() {
|
||
|
|
let (hacker_bytes, _) = crew_res.unwrap();
|
||
|
|
let hacker = Hacker::new(hacker_bytes).unwrap();
|
||
|
|
let _ = black_box(hacker.handle().unwrap());
|
||
|
|
hacker_count += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
black_box(hacker_count)
|
||
|
|
})
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// O(1) field accesses on pre-parsed messages.
|
||
|
|
/// Measures only the decode step at a known offset — not the scan.
|
||
|
|
fn bench_field_access(c: &mut Criterion) {
|
||
|
|
let Some(data) = load("small") else { return };
|
||
|
|
|
||
|
|
let campaign = Campaign::new(&data).unwrap();
|
||
|
|
let (op_bytes, _) = campaign.operations().next().unwrap().unwrap();
|
||
|
|
let op = Operation::new(op_bytes).unwrap();
|
||
|
|
let (hacker_bytes, _) = op.crew().next().unwrap().unwrap();
|
||
|
|
let hacker = Hacker::new(hacker_bytes).unwrap();
|
||
|
|
let worm = Worm::new(op.worm().unwrap()).unwrap();
|
||
|
|
|
||
|
|
let mut group = c.benchmark_group("field_access");
|
||
|
|
|
||
|
|
group.bench_function("campaign::name", |b| {
|
||
|
|
b.iter(|| black_box(campaign.name().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("campaign::total_bytes_stolen", |b| {
|
||
|
|
b.iter(|| black_box(campaign.total_bytes_stolen().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("operation::codename", |b| {
|
||
|
|
b.iter(|| black_box(op.codename().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("operation::timestamp", |b| {
|
||
|
|
b.iter(|| black_box(op.timestamp().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("operation::successful", |b| {
|
||
|
|
b.iter(|| black_box(op.successful().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("hacker::handle", |b| {
|
||
|
|
b.iter(|| black_box(hacker.handle().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("hacker::skill_level (f32)", |b| {
|
||
|
|
b.iter(|| black_box(hacker.skill_level().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("hacker::is_elite (bool)", |b| {
|
||
|
|
b.iter(|| black_box(hacker.is_elite().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("worm::polymorphic (bool)", |b| {
|
||
|
|
b.iter(|| black_box(worm.polymorphic().unwrap()))
|
||
|
|
});
|
||
|
|
group.bench_function("worm::payload (bytes)", |b| {
|
||
|
|
b.iter(|| black_box(worm.payload().unwrap()))
|
||
|
|
});
|
||
|
|
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Iterate repeated fields at different depths.
|
||
|
|
fn bench_iterate(c: &mut Criterion) {
|
||
|
|
let cases = [
|
||
|
|
("tiny", load("tiny")),
|
||
|
|
("small", load("small")),
|
||
|
|
("medium", load("medium")),
|
||
|
|
];
|
||
|
|
|
||
|
|
let mut group = c.benchmark_group("iterate");
|
||
|
|
for (label, maybe_data) in &cases {
|
||
|
|
let Some(data) = maybe_data else { continue };
|
||
|
|
|
||
|
|
// Top-level repeated field — walk Operation blobs, no inner parse.
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::new("count_operations", label),
|
||
|
|
data,
|
||
|
|
|b, data| {
|
||
|
|
b.iter(|| {
|
||
|
|
let campaign = Campaign::new(data).unwrap();
|
||
|
|
black_box(campaign.operations().count())
|
||
|
|
})
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
// Nested repeated field — parse each Operation to reach its crew.
|
||
|
|
group.bench_with_input(
|
||
|
|
BenchmarkId::new("count_all_crew", label),
|
||
|
|
data,
|
||
|
|
|b, data| {
|
||
|
|
b.iter(|| {
|
||
|
|
let campaign = Campaign::new(data).unwrap();
|
||
|
|
let mut n = 0usize;
|
||
|
|
for op_res in campaign.operations() {
|
||
|
|
let (op_bytes, _) = op_res.unwrap();
|
||
|
|
n += Operation::new(op_bytes).unwrap().crew().count();
|
||
|
|
}
|
||
|
|
black_box(n)
|
||
|
|
})
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
group.finish();
|
||
|
|
}
|
||
|
|
|
||
|
|
criterion_group!(
|
||
|
|
benches,
|
||
|
|
bench_shallow_parse,
|
||
|
|
bench_deep_parse,
|
||
|
|
bench_field_access,
|
||
|
|
bench_iterate
|
||
|
|
);
|
||
|
|
criterion_main!(benches);
|