//! 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/.pb`. /// Returns `None` (and prints a hint) if the file does not exist. fn load(name: &str) -> Option> { 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);