//! Small benchmark harness for the paranoia read path and sample index. //! //! Run with: //! cargo run --release --example bench_paranoia //! cargo run --release --example bench_paranoia -- 5000 use cdrustanoia_core::device::image::ImageDrive; use cdrustanoia_core::device::samples_to_bytes; use cdrustanoia_core::paranoia::sort::SortIndex; use cdrustanoia_core::paranoia::{Paranoia, PARANOIA_MODE_FULL}; use std::collections::HashMap; use std::time::Instant; fn make_samples(sectors: usize) -> Vec { let mut out = Vec::with_capacity(sectors * 1176); let mut x = 0x1234_5678_9abc_def0u64; for i in 0..sectors * 1176 { x ^= x << 13; x ^= x >> 7; x ^= x << 17; let v = if i % 5000 < 700 { 0 } else { (x as i16).wrapping_add((i as i16).wrapping_mul(11)) }; out.push(v); } out } fn bench_index(samples: &[i16]) { let mut index = SortIndex::new(samples.len()); let started = Instant::now(); index.setup(samples, 0, 0, samples.len() as i64); let setup = started.elapsed(); let mut found = 0usize; let started = Instant::now(); for i in 0..10_000i64 { let post = (i * 7919) % samples.len() as i64; let value = samples[post as usize]; if index.getmatch(samples, post, 512, value).is_some() { found += 1; while index.nextmatch(samples).is_some() { found += 1; } } } let queries = started.elapsed(); println!( "SortIndex: setup={:?} 10k queries={:?} matches={} ({} samples)", setup, queries, found, samples.len() ); } struct OldIndex { buckets: HashMap>, size: usize, cursor: Option<(i16, usize, usize)>, } impl OldIndex { fn new() -> Self { Self { buckets: HashMap::new(), size: 0, cursor: None, } } fn setup(&mut self, samples: &[i16]) { self.buckets.clear(); self.size = samples.len(); for (i, &sample) in samples.iter().enumerate() { self.buckets.entry(sample).or_default().push(i as u32); } } fn getmatch(&mut self, post: i64, overlap: i64, value: i16) -> Option { let post = post.clamp(0, self.size as i64); let lo = (post - overlap).max(0) as usize; let hi = ((post + overlap).min(self.size as i64)).max(0) as usize; self.cursor = None; let positions = self.buckets.get(&value)?; let idx = positions.partition_point(|p| (*p as usize) < lo); if idx >= positions.len() { return None; } let pos = positions[idx] as usize; if pos >= hi { return None; } self.cursor = Some((value, idx + 1, hi)); Some(pos) } fn nextmatch(&mut self) -> Option { let (value, idx, hi) = self.cursor?; let positions = self.buckets.get(&value)?; if idx >= positions.len() { self.cursor = None; return None; } let pos = positions[idx] as usize; if pos >= hi { self.cursor = None; return None; } self.cursor = Some((value, idx + 1, hi)); Some(pos) } } fn bench_old_index(samples: &[i16]) { let mut index = OldIndex::new(); let started = Instant::now(); index.setup(samples); let setup = started.elapsed(); let mut found = 0usize; let started = Instant::now(); for i in 0..10_000i64 { let post = (i * 7919) % samples.len() as i64; let value = samples[post as usize]; if index.getmatch(post, 512, value).is_some() { found += 1; while index.nextmatch().is_some() { found += 1; } } } let queries = started.elapsed(); println!( "Old HashMap index: setup={:?} 10k queries={:?} matches={}", setup, queries, found ); } fn main() { let sectors: usize = std::env::args() .nth(1) .and_then(|s| s.parse().ok()) .unwrap_or(2000); let samples = make_samples(sectors); bench_index(&samples); bench_old_index(&samples); let bytes = samples_to_bytes(&samples); let drive = ImageDrive::from_bytes(bytes); let mut p = Paranoia::new(Box::new(drive)); p.modeset(PARANOIA_MODE_FULL); let mut cb = |_: i64, _: i32| {}; let started = Instant::now(); let mut got = 0usize; for _ in 0..sectors { let sector = p.read(&mut cb).expect("read sector"); got += sector.len(); } let elapsed = started.elapsed(); let bytes_read = (got * 2) as f64; println!( "Read: {} sectors, {} samples, {:?}, {:.1} MB/s", sectors, got, elapsed, bytes_read / elapsed.as_secs_f64() / 1_000_000.0 ); assert_eq!(got, sectors * 1176); }