//! Criterion benchmarks for the empty-room baseline calibration module (ADR-135). //! //! Measures per-call throughput of CalibrationRecorder and BaselineCalibration //! across HT20 (K=52), HT40 (K=114), HE20 (K=256, all bins; #1009), and HE40 (K=484). //! //! Run (compile-only — no execution): //! cargo bench -p wifi-densepose-signal --no-default-features --bench calibration_bench --no-run //! //! Run to completion (generates HTML in target/criterion/): //! cargo bench -p wifi-densepose-signal --no-default-features --bench calibration_bench use std::f64::consts::PI; use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use ndarray::Array2; use num_complex::Complex64; use wifi_densepose_core::types::{AntennaConfig, CsiFrame, CsiMetadata, DeviceId, FrequencyBand}; use wifi_densepose_signal::calibration::{ BaselineCalibration, CalibrationConfig, CalibrationRecorder, }; // --------------------------------------------------------------------------- // Deterministic PRNG (xorshift32, seed=42) — duplicated locally. // --------------------------------------------------------------------------- struct Rng(u32); impl Rng { fn new(seed: u32) -> Self { assert_ne!(seed, 0); Self(seed) } fn next_u32(&mut self) -> u32 { let mut x = self.0; x ^= x << 13; x ^= x >> 17; x ^= x << 5; self.0 = x; x } fn next_f64(&mut self) -> f64 { (self.next_u32() as f64 + 1.0) / (u32::MAX as f64 + 2.0) } fn next_normal(&mut self) -> f64 { let u1 = self.next_f64(); let u2 = self.next_f64(); (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos() } } // --------------------------------------------------------------------------- // Tier specification table // --------------------------------------------------------------------------- struct TierSpec { label: &'static str, n_active: usize, bandwidth_mhz: u16, config: CalibrationConfig, } fn tiers() -> Vec { vec![ TierSpec { label: "ht20", n_active: 52, bandwidth_mhz: 20, config: CalibrationConfig::ht20() }, TierSpec { label: "ht40", n_active: 114, bandwidth_mhz: 40, config: CalibrationConfig::ht40() }, // Issue #1009 §1b: HE20 records all 256 delivered bins (he20().num_active == 256). TierSpec { label: "he20", n_active: 256, bandwidth_mhz: 20, config: CalibrationConfig::he20() }, TierSpec { label: "he40", n_active: 484, bandwidth_mhz: 40, config: CalibrationConfig::he40() }, ] } // --------------------------------------------------------------------------- // Synthetic CSI frame builder (stationary, seed=42) // --------------------------------------------------------------------------- fn make_frame(n_active: usize, bandwidth_mhz: u16, rng: &mut Rng) -> CsiFrame { let noise_std = 0.01_f64; let mut data = Array2::::zeros((1, n_active)); for k in 0..n_active { let amp = 0.3 + 0.7 * (k as f64 * PI / n_active as f64).sin().abs(); let phase = (k as f64 * 0.1).rem_euclid(2.0 * PI) - PI; let re = amp * phase.cos() + noise_std * rng.next_normal(); let im = amp * phase.sin() + noise_std * rng.next_normal(); data[(0, k)] = Complex64::new(re, im); } let mut meta = CsiMetadata::new(DeviceId::new("bench"), FrequencyBand::Band2_4GHz, 6); meta.bandwidth_mhz = bandwidth_mhz; meta.antenna_config = AntennaConfig::new(1, 1); CsiFrame::new(meta, data) } /// Build a `CalibrationRecorder` that has already absorbed 600 frames. fn pre_loaded_recorder(spec: &TierSpec) -> CalibrationRecorder { let mut rng = Rng::new(42); let mut recorder = CalibrationRecorder::new(spec.config.clone()); for _ in 0..600 { let frame = make_frame(spec.n_active, spec.bandwidth_mhz, &mut rng); recorder.record(&frame).expect("record should succeed in bench setup"); } recorder } /// Build a finalised `BaselineCalibration` for deviation and to_bytes benches. fn finalised_baseline(spec: &TierSpec) -> BaselineCalibration { pre_loaded_recorder(spec) .finalize() .expect("finalize should succeed in bench setup") } // --------------------------------------------------------------------------- // Bench 1: bench_recorder_record/ — single record() call (hot path) // --------------------------------------------------------------------------- fn bench_recorder_record(c: &mut Criterion) { let mut group = c.benchmark_group("bench_recorder_record"); for spec in tiers() { group.throughput(Throughput::Elements(spec.n_active as u64)); let mut rng = Rng::new(42); let frame = make_frame(spec.n_active, spec.bandwidth_mhz, &mut rng); let mut recorder = CalibrationRecorder::new(spec.config.clone()); group.bench_with_input( BenchmarkId::from_parameter(spec.label), &frame, |b, f| { b.iter(|| { // Accumulate into a shared recorder — measures per-call cost of record(). black_box(recorder.record(black_box(f)).ok()) }); }, ); } group.finish(); } // --------------------------------------------------------------------------- // Bench 2: bench_recorder_finalize/ — finalize() from 600 pre-loaded frames // --------------------------------------------------------------------------- fn bench_recorder_finalize(c: &mut Criterion) { let mut group = c.benchmark_group("bench_recorder_finalize"); for spec in tiers() { group.throughput(Throughput::Elements(spec.n_active as u64)); group.bench_function(BenchmarkId::from_parameter(spec.label), |b| { b.iter_with_setup( || pre_loaded_recorder(&spec), |recorder| { black_box(recorder.finalize().ok()) }, ); }); } group.finish(); } // --------------------------------------------------------------------------- // Bench 3: bench_deviation/ — deviation() on a single frame // --------------------------------------------------------------------------- fn bench_deviation(c: &mut Criterion) { let mut group = c.benchmark_group("bench_deviation"); for spec in tiers() { group.throughput(Throughput::Elements(spec.n_active as u64)); let baseline = finalised_baseline(&spec); let mut rng = Rng::new(42); let frame = make_frame(spec.n_active, spec.bandwidth_mhz, &mut rng); group.bench_with_input( BenchmarkId::from_parameter(spec.label), &frame, |b, f| { b.iter(|| { black_box(baseline.deviation(black_box(f)).ok()) }); }, ); } group.finish(); } // --------------------------------------------------------------------------- // Bench 4: bench_record_600/ — full 600-frame record session // --------------------------------------------------------------------------- fn bench_record_600(c: &mut Criterion) { let mut group = c.benchmark_group("bench_record_600"); for spec in tiers() { group.throughput(Throughput::Elements(600 * spec.n_active as u64)); // Pre-build 600 frames to avoid contaminating bench with frame construction. let mut rng = Rng::new(42); let frames: Vec = (0..600) .map(|_| make_frame(spec.n_active, spec.bandwidth_mhz, &mut rng)) .collect(); group.bench_with_input( BenchmarkId::from_parameter(spec.label), &frames, |b, fs| { b.iter_with_setup( || CalibrationRecorder::new(spec.config.clone()), |mut recorder| { for f in fs { black_box(recorder.record(black_box(f)).ok()); } black_box(recorder) }, ); }, ); } group.finish(); } // --------------------------------------------------------------------------- // Bench 5: bench_to_bytes/ — serialisation cost (to_bytes) // --------------------------------------------------------------------------- fn bench_to_bytes(c: &mut Criterion) { let mut group = c.benchmark_group("bench_to_bytes"); for spec in tiers() { group.throughput(Throughput::Elements(spec.n_active as u64)); let baseline = finalised_baseline(&spec); group.bench_function(BenchmarkId::from_parameter(spec.label), |b| { b.iter(|| { black_box(baseline.to_bytes()) }); }); } group.finish(); } // --------------------------------------------------------------------------- // Criterion harness // --------------------------------------------------------------------------- criterion_group!( benches, bench_recorder_record, bench_recorder_finalize, bench_deviation, bench_record_600, bench_to_bytes, ); criterion_main!(benches);