Refactor crate into multiple subcrates

This commit is contained in:
2026-05-04 22:45:55 -07:00
parent d71d96c15f
commit ea68537f0b
48 changed files with 9839 additions and 366 deletions
+215
View File
@@ -0,0 +1,215 @@
//! 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_benches::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);