//! A/B/C for the phase-1 integrity verifier's bounded result table. //! //! `historical` probes every manifest occurrence serially. `owned` models the //! current accepted 256-occurrence/concurrency-16 candidate exactly: each //! distinct key is cloned into the map and cloned again into the work vector. //! `borrowed` changes only those scratch keys to `&str`. Tiny stores retain the //! exact historical loop through four valid occurrences in both candidates. use foldhash::quality::RandomState; use futures::stream::{self, StreamExt}; use std::alloc::{GlobalAlloc, Layout, System}; use std::collections::HashMap; use std::future::Future; use std::hint::black_box; use std::path::Path; use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::{Duration, Instant}; const PRODUCTION_CONCURRENCY: usize = 16; const WINDOW: usize = 256; const SERIAL_FAST_PATH_OCCURRENCES: usize = 4; static CANDIDATE_CONCURRENCY: AtomicUsize = AtomicUsize::new(PRODUCTION_CONCURRENCY); type Manifest = (String, Vec, Vec, i64); type OwnedSizes = HashMap, RandomState>; type BorrowedSizes<'a> = HashMap<&'a str, Option, RandomState>; type AuditBoxFut<'a, T> = Pin + Send + 'a>>; struct TrackingAllocator; static LIVE_ALLOCATED: AtomicUsize = AtomicUsize::new(0); static PEAK_ALLOCATED: AtomicUsize = AtomicUsize::new(0); #[global_allocator] static ALLOCATOR: TrackingAllocator = TrackingAllocator; #[inline] fn update_peak(candidate: usize) { let mut peak = PEAK_ALLOCATED.load(Ordering::Relaxed); while candidate > peak { match PEAK_ALLOCATED.compare_exchange_weak( peak, candidate, Ordering::Relaxed, Ordering::Relaxed, ) { Ok(_) => break, Err(observed) => peak = observed, } } } // SAFETY: every operation delegates to `System` with the original pointer and // layout; the counters are diagnostic and do not affect allocation semantics. unsafe impl GlobalAlloc for TrackingAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { let pointer = unsafe { System.alloc(layout) }; if !pointer.is_null() { let live = LIVE_ALLOCATED.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); update_peak(live); } pointer } unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { LIVE_ALLOCATED.fetch_sub(layout.size(), Ordering::Relaxed); unsafe { System.dealloc(pointer, layout) }; } unsafe fn realloc(&self, pointer: *mut u8, old: Layout, new_size: usize) -> *mut u8 { let new_pointer = unsafe { System.realloc(pointer, old, new_size) }; if !new_pointer.is_null() { if new_size >= old.size() { let growth = new_size - old.size(); let live = LIVE_ALLOCATED.fetch_add(growth, Ordering::Relaxed) + growth; update_peak(live); } else { LIVE_ALLOCATED.fetch_sub(old.size() - new_size, Ordering::Relaxed); } } new_pointer } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Mode { Historical, Owned, Borrowed, Sorted, } impl Mode { fn parse(value: &str) -> Self { match value { "historical" => Self::Historical, "owned" => Self::Owned, "borrowed" => Self::Borrowed, "sorted" => Self::Sorted, _ => panic!("mode must be historical, owned, borrowed, or sorted"), } } } #[derive(Clone, Copy)] enum Latency { Immediate, Local { metadata: Duration, hash: Duration }, Remote(Duration), RealFs(&'static Path), } #[derive(Clone)] struct SimBackend { latency: Latency, calls: Arc, } impl SimBackend { fn new(latency: Latency) -> Self { Self { latency, calls: Arc::new(AtomicUsize::new(0)), } } fn blob_size<'a>(&'a self, hash: &'a str) -> AuditBoxFut<'a, Option> { Box::pin(async move { self.calls.fetch_add(1, Ordering::Relaxed); match self.latency { Latency::Immediate => {} Latency::Local { metadata, .. } => tokio::time::sleep(metadata).await, Latency::Remote(delay) => tokio::time::sleep(delay).await, Latency::RealFs(root) => { return tokio::fs::metadata(root.join(hash)) .await .ok() .map(|metadata| metadata.len()); } } if hash.starts_with("missing-") { None } else if hash.starts_with("wrong-") { Some(999) } else { Some(256) } }) } async fn hash_local_blob(&self, blob_hash: &str) { match self.latency { Latency::Local { hash, .. } => tokio::time::sleep(hash).await, Latency::RealFs(root) => { black_box(tokio::fs::read(root.join(blob_hash)).await.ok()); } Latency::Immediate | Latency::Remote(_) => {} } } fn calls(&self) -> usize { self.calls.load(Ordering::Relaxed) } } #[inline] fn label(value: &str) -> &str { &value[..value.len().min(12)] } #[inline] fn uses_serial_fast_path(manifests: &[Manifest]) -> bool { if manifests.len() == 1 { let (_, hashes, sizes, _) = &manifests[0]; return hashes.len() != sizes.len() || hashes.len() <= SERIAL_FAST_PATH_OCCURRENCES; } let mut occurrences = 0usize; for (_, hashes, sizes, _) in manifests { if hashes.len() == sizes.len() { occurrences = occurrences.saturating_add(hashes.len()); if occurrences > SERIAL_FAST_PATH_OCCURRENCES { return false; } } } true } async fn historical(manifests: &[Manifest], backend: &SimBackend) -> Vec { let mut issues = Vec::new(); for (file_hash, hashes, expected_sizes, total_size) in manifests { let file_label = label(file_hash); if hashes.len() != expected_sizes.len() { issues.push(format!( "Manifest {file_label}: chunk_hashes/chunk_sizes length mismatch" )); continue; } let sum: i64 = expected_sizes.iter().sum(); if sum != *total_size { issues.push(format!( "Manifest {file_label}: total_size {total_size} != sum of chunk_sizes {sum}" )); } for (index, hash) in hashes.iter().enumerate() { let chunk_label = label(hash); match backend.blob_size(hash).await { Some(actual) if actual != expected_sizes[index] as u64 => issues.push(format!( "Manifest {file_label} chunk {chunk_label}: size mismatch (expected {}, actual {actual})", expected_sizes[index] )), None => issues.push(format!( "Manifest {file_label} chunk {chunk_label}: missing in backend" )), Some(_) => {} } } } issues } fn replay_owned(manifests: &[Manifest], sizes: &OwnedSizes) -> Vec { replay(manifests, |hash| sizes.get(hash).copied().flatten()) } fn replay_borrowed(manifests: &[Manifest], sizes: &BorrowedSizes<'_>) -> Vec { replay(manifests, |hash| sizes.get(hash).copied().flatten()) } fn replay(manifests: &[Manifest], mut size_of: F) -> Vec where F: FnMut(&str) -> Option, { let mut issues = Vec::new(); for (file_hash, hashes, expected_sizes, total_size) in manifests { let file_label = label(file_hash); if hashes.len() != expected_sizes.len() { issues.push(format!( "Manifest {file_label}: chunk_hashes/chunk_sizes length mismatch" )); continue; } let sum: i64 = expected_sizes.iter().sum(); if sum != *total_size { issues.push(format!( "Manifest {file_label}: total_size {total_size} != sum of chunk_sizes {sum}" )); } for (index, hash) in hashes.iter().enumerate() { let chunk_label = label(hash); match size_of(hash) { Some(actual) if actual != expected_sizes[index] as u64 => issues.push(format!( "Manifest {file_label} chunk {chunk_label}: size mismatch (expected {}, actual {actual})", expected_sizes[index] )), None => issues.push(format!( "Manifest {file_label} chunk {chunk_label}: missing in backend" )), Some(_) => {} } } } issues } async fn fill_owned(sizes: OwnedSizes, backend: &SimBackend) -> OwnedSizes { let hashes: Vec = sizes.keys().cloned().collect(); stream::iter(hashes) .map(|hash| async move { let size = backend.blob_size(&hash).await; (hash, size) }) .buffer_unordered(CANDIDATE_CONCURRENCY.load(Ordering::Relaxed)) .fold(sizes, |mut sizes, (hash, size)| async move { sizes.insert(hash, size); sizes }) .await } async fn fill_borrowed<'a>(sizes: BorrowedSizes<'a>, backend: &SimBackend) -> BorrowedSizes<'a> { let hashes: Vec<&'a str> = sizes.keys().copied().collect(); stream::iter(hashes) .map(|hash| async move { let size = backend.blob_size(hash).await; (hash, size) }) .buffer_unordered(CANDIDATE_CONCURRENCY.load(Ordering::Relaxed)) .fold(sizes, |mut sizes, (hash, size)| async move { sizes.insert(hash, size); sizes }) .await } async fn owned_batch(manifests: &[Manifest], backend: &SimBackend) -> Vec { let mut sizes = OwnedSizes::default(); for (_, hashes, expected_sizes, _) in manifests { if hashes.len() == expected_sizes.len() { for hash in hashes { sizes.entry(hash.clone()).or_insert(None); } } } let sizes = fill_owned(sizes, backend).await; replay_owned(manifests, &sizes) } async fn borrowed_batch(manifests: &[Manifest], backend: &SimBackend) -> Vec { let mut sizes = BorrowedSizes::default(); for (_, hashes, expected_sizes, _) in manifests { if hashes.len() == expected_sizes.len() { for hash in hashes { sizes.entry(hash.as_str()).or_insert(None); } } } let sizes = fill_borrowed(sizes, backend).await; replay_borrowed(manifests, &sizes) } async fn sorted_batch(manifests: &[Manifest], backend: &SimBackend) -> Vec { let mut hashes = Vec::new(); for (_, manifest_hashes, expected_sizes, _) in manifests { if manifest_hashes.len() == expected_sizes.len() { hashes.extend(manifest_hashes.iter().map(String::as_str)); } } hashes.sort_unstable(); hashes.dedup(); let mut values = vec![None; hashes.len()]; let concurrency = CANDIDATE_CONCURRENCY.load(Ordering::Relaxed).max(1); let mut pending = futures::stream::FuturesUnordered::new(); let mut next = 0usize; while next < hashes.len() || !pending.is_empty() { while next < hashes.len() && pending.len() < concurrency { let index = next; let hash = hashes[index]; pending.push(async move { let size = backend.blob_size(hash).await; (index, size) }); next += 1; } if let Some((index, size)) = pending.next().await { values[index] = size; } } replay(manifests, |hash| { hashes .binary_search(&hash) .ok() .and_then(|index| values[index]) }) } async fn windowed(manifests: &[Manifest], backend: &SimBackend, mode: Mode) -> Vec { let mut issues = Vec::new(); let mut start = 0; while start < manifests.len() { let (_, hashes, expected_sizes, _) = &manifests[start]; if hashes.len() == expected_sizes.len() && hashes.len() > WINDOW { let (file_hash, hashes, expected_sizes, total_size) = &manifests[start]; let file_label = label(file_hash); let sum: i64 = expected_sizes.iter().sum(); if sum != *total_size { issues.push(format!( "Manifest {file_label}: total_size {total_size} != sum of chunk_sizes {sum}" )); } for offset in (0..hashes.len()).step_by(WINDOW) { let end = (offset + WINDOW).min(hashes.len()); let synthetic = ( file_hash.clone(), hashes[offset..end].to_vec(), expected_sizes[offset..end].to_vec(), expected_sizes[offset..end].iter().sum(), ); let batch = std::slice::from_ref(&synthetic); let mut batch_issues = match mode { Mode::Owned => owned_batch(batch, backend).await, Mode::Borrowed => borrowed_batch(batch, backend).await, Mode::Sorted => sorted_batch(batch, backend).await, Mode::Historical => unreachable!(), }; // Slice replay must not repeat a total-size issue already emitted. batch_issues.retain(|issue| !issue.contains("sum of chunk_sizes")); issues.extend(batch_issues); } start += 1; continue; } let mut occurrences = 0; let mut end = start; while end < manifests.len() { let (_, hashes, expected_sizes, _) = &manifests[end]; let next = if hashes.len() == expected_sizes.len() { hashes.len() } else { 0 }; if next > WINDOW || (occurrences > 0 && occurrences + next > WINDOW) { break; } occurrences += next; end += 1; } debug_assert!(end > start); let batch = &manifests[start..end]; issues.extend(match mode { Mode::Owned => owned_batch(batch, backend).await, Mode::Borrowed => borrowed_batch(batch, backend).await, Mode::Sorted => sorted_batch(batch, backend).await, Mode::Historical => unreachable!(), }); start = end; } issues } async fn verify(manifests: &[Manifest], backend: &SimBackend, mode: Mode) -> Vec { if mode == Mode::Historical || uses_serial_fast_path(manifests) { historical(manifests, backend).await } else { windowed(manifests, backend, mode).await } } fn fixture(manifests: usize, chunks: usize, unique: usize, anomalies: bool) -> Vec { let unique = unique.max(1); let mut rows = Vec::with_capacity(manifests + usize::from(anomalies) * 3); for manifest in 0..manifests { let hashes = (0..chunks) .map(|chunk| audit_hash((manifest * chunks + chunk) % unique)) .collect(); rows.push(( format!("file-{manifest:059}"), hashes, vec![256; chunks], (chunks * 256) as i64, )); } if anomalies { rows.push(( "sum-mismatch-file".into(), vec!["wrong-shared".into(), "wrong-shared".into()], vec![256, 257], 1, )); rows.push(( "missing-file".into(), vec!["missing-shared".into(), "missing-shared".into()], vec![256, 256], 512, )); rows.push(( "malformed-file".into(), vec!["missing-must-not-be-queried".into()], vec![], 0, )); } rows } fn audit_hash(index: usize) -> String { fn mix(mut value: u64) -> u64 { value = value.wrapping_add(0x9e37_79b9_7f4a_7c15); value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9); value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb); value ^ (value >> 31) } let a = mix(index as u64); let b = mix(a); let c = mix(b); let d = mix(c); format!("{a:016x}{b:016x}{c:016x}{d:016x}") } fn mark_missing(rows: &mut [Manifest], every: usize) { let mut occurrence = 0usize; for (_, hashes, _, _) in rows { for hash in hashes { if occurrence.is_multiple_of(every) { *hash = format!("missing-{hash}"); } occurrence += 1; } } } fn populate_real_fs(root: &Path, scenarios: &[Vec]) { std::fs::create_dir_all(root).expect("create real-filesystem fixture directory"); for rows in scenarios { for (_, hashes, expected_sizes, _) in rows { if hashes.len() != expected_sizes.len() { continue; } for hash in hashes { if hash.starts_with("missing-") { continue; } let file = std::fs::File::create(root.join(hash)).expect("create fixture blob"); let length = if hash.starts_with("wrong-") { 999 } else { 256 }; file.set_len(length).expect("size fixture blob"); } } } } fn storage_hashes(manifests: &[Manifest]) -> Vec<&str> { let mut unique: HashMap<&str, (), RandomState> = HashMap::default(); for (_, hashes, expected_sizes, _) in manifests { if hashes.len() == expected_sizes.len() { for hash in hashes { if !hash.starts_with("missing-") && !hash.starts_with("wrong-") { unique.entry(hash).or_insert(()); } } } } let mut hashes: Vec<&str> = unique.into_keys().collect(); hashes.sort_unstable(); hashes } async fn phase_two(hashes: &[&str], backend: &SimBackend) { stream::iter(hashes.iter().copied()) .map(|hash| async move { black_box(backend.blob_size(hash).await); backend.hash_local_blob(hash).await; }) .buffer_unordered(PRODUCTION_CONCURRENCY) .collect::>() .await; } async fn phase_two_generated(count: usize, backend: &SimBackend) { stream::iter(0..count) .map(|index| async move { let hash = audit_hash(index); black_box(backend.blob_size(&hash).await); backend.hash_local_blob(&hash).await; }) .buffer_unordered(PRODUCTION_CONCURRENCY) .collect::>() .await; } #[derive(Clone, Copy)] struct Observation { phase: Duration, full: Duration, phase_calls: usize, full_calls: usize, issue_checksum: usize, } async fn observe( mode: Mode, manifests: &[Manifest], latency: Latency, repetitions: usize, storage: &[&str], ) -> Observation { let phase_backend = SimBackend::new(latency); let start = Instant::now(); let mut issues = Vec::new(); for _ in 0..repetitions { issues = verify(manifests, &phase_backend, mode).await; black_box(&issues); } let phase = start.elapsed() / repetitions as u32; let full_backend = SimBackend::new(latency); let start = Instant::now(); for _ in 0..repetitions { issues = verify(manifests, &full_backend, mode).await; phase_two(storage, &full_backend).await; black_box(&issues); } let full = start.elapsed() / repetitions as u32; Observation { phase, full, phase_calls: phase_backend.calls() / repetitions, full_calls: full_backend.calls() / repetitions, issue_checksum: issues.iter().map(String::len).sum(), } } fn median(mut values: Vec) -> Duration { values.sort_unstable(); values[values.len() / 2] } async fn measure( manifests: &[Manifest], latency: Latency, samples: usize, repetitions: usize, ) -> [Observation; 4] { let storage = storage_hashes(manifests); let modes = [Mode::Historical, Mode::Owned, Mode::Borrowed, Mode::Sorted]; let mut phase = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; let mut full = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; let mut last = [None, None, None, None]; for sample in 0..=samples { for offset in 0..4 { let index = (sample + offset) % 4; let observation = observe(modes[index], manifests, latency, repetitions, &storage).await; if sample > 0 { phase[index].push(observation.phase); full[index].push(observation.full); last[index] = Some(observation); } } } std::array::from_fn(|index| { let mut observation = last[index].expect("at least one measured sample"); observation.phase = median(std::mem::take(&mut phase[index])); observation.full = median(std::mem::take(&mut full[index])); observation }) } fn print_header() { println!( "scenario,historical_phase_ms,owned_phase_ms,borrowed_phase_ms,sorted_phase_ms,\ borrowed_vs_owned_phase,sorted_vs_owned_phase,historical_full_ms,owned_full_ms,\ borrowed_full_ms,sorted_full_ms,borrowed_vs_owned_full,sorted_vs_owned_full,\ calls_historical,calls_owned,calls_borrowed,calls_sorted,full_calls_historical,\ full_calls_owned,full_calls_borrowed,full_calls_sorted,issues_equal" ); } async fn run_scenario( name: &str, latency: Latency, rows: &[Manifest], samples: usize, repetitions: usize, ) { let historical_gate = verify(rows, &SimBackend::new(Latency::Immediate), Mode::Historical).await; let owned_gate = verify(rows, &SimBackend::new(Latency::Immediate), Mode::Owned).await; let borrowed_gate = verify(rows, &SimBackend::new(Latency::Immediate), Mode::Borrowed).await; let sorted_gate = verify(rows, &SimBackend::new(Latency::Immediate), Mode::Sorted).await; assert_eq!( historical_gate, owned_gate, "owned issue gate failed for {name}" ); assert_eq!( historical_gate, borrowed_gate, "borrowed issue gate failed for {name}" ); assert_eq!( historical_gate, sorted_gate, "sorted issue gate failed for {name}" ); let observations = measure(rows, latency, samples, repetitions).await; let [historical, owned, borrowed, sorted] = observations; let equal = historical.issue_checksum == owned.issue_checksum && historical.issue_checksum == borrowed.issue_checksum && historical.issue_checksum == sorted.issue_checksum; assert!(equal, "issue gate failed for {name}"); assert_eq!(owned.phase_calls, borrowed.phase_calls); assert_eq!(owned.phase_calls, sorted.phase_calls); assert_eq!(owned.full_calls, borrowed.full_calls); assert_eq!(owned.full_calls, sorted.full_calls); assert!(owned.phase_calls <= historical.phase_calls); println!( "{name},{:.6},{:.6},{:.6},{:.6},{:.3},{:.3},{:.6},{:.6},{:.6},{:.6},{:.3},{:.3},{},{},{},{},{},{},{},{},{}", historical.phase.as_secs_f64() * 1e3, owned.phase.as_secs_f64() * 1e3, borrowed.phase.as_secs_f64() * 1e3, sorted.phase.as_secs_f64() * 1e3, owned.phase.as_secs_f64() / borrowed.phase.as_secs_f64(), owned.phase.as_secs_f64() / sorted.phase.as_secs_f64(), historical.full.as_secs_f64() * 1e3, owned.full.as_secs_f64() * 1e3, borrowed.full.as_secs_f64() * 1e3, sorted.full.as_secs_f64() * 1e3, owned.full.as_secs_f64() / borrowed.full.as_secs_f64(), owned.full.as_secs_f64() / sorted.full.as_secs_f64(), historical.phase_calls, owned.phase_calls, borrowed.phase_calls, sorted.phase_calls, historical.full_calls, owned.full_calls, borrowed.full_calls, sorted.full_calls, equal, ); } #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main() { let args: Vec = std::env::args().collect(); let candidate_concurrency = std::env::var("OXICLOUD_AUDIT_CONCURRENCY") .ok() .map(|value| value.parse::().expect("numeric concurrency")) .unwrap_or(PRODUCTION_CONCURRENCY); assert!(matches!(candidate_concurrency, 4 | 8 | 16)); CANDIDATE_CONCURRENCY.store(candidate_concurrency, Ordering::Relaxed); if args.get(1).is_some_and(|arg| arg == "--memory") { let mode = Mode::parse(args.get(2).map(String::as_str).unwrap_or("borrowed")); let full_method = args.iter().any(|arg| arg == "full" || arg == "full-drop"); let drop_manifests = args.iter().any(|arg| arg == "full-drop"); let rows = fixture(1_000, 250, 250_000, false); let manifest_count = rows.len(); let backend = SimBackend::new(Latency::Immediate); let live_before = LIVE_ALLOCATED.load(Ordering::Relaxed); PEAK_ALLOCATED.store(live_before, Ordering::Relaxed); let issues = verify(&rows, &backend, mode).await; let phase_peak = PEAK_ALLOCATED.load(Ordering::Relaxed); black_box(&issues); if drop_manifests { drop(rows); } let live_before_phase_two = LIVE_ALLOCATED.load(Ordering::Relaxed); if full_method { phase_two_generated(250_000, &backend).await; } let full_peak = PEAK_ALLOCATED.load(Ordering::Relaxed); println!( "mode={mode:?} concurrency={candidate_concurrency} full={full_method} drop_manifests={drop_manifests} manifests={manifest_count} occurrences=250000 calls={} issues={} live_before={} phase_peak={} phase_scratch_peak={} live_before_phase_two={} full_peak={}", backend.calls(), issues.len(), live_before, phase_peak, phase_peak.saturating_sub(live_before), live_before_phase_two, full_peak, ); return; } if args.iter().any(|arg| arg == "--real-fs") { let mut tiny_missing = fixture(2, 1, 2, false); mark_missing(&mut tiny_missing, 2); let shared = fixture(64, 8, 32, false); let unique = fixture(32, 8, 256, false); let mut mixed = fixture(32, 8, 256, false); mark_missing(&mut mixed, 17); let real_rows = vec![fixture(1, 2, 2, false), tiny_missing, shared, unique, mixed]; let path = std::env::temp_dir().join(format!( "oxicloud-integrity-real-fs-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .expect("system clock") .as_nanos() )); populate_real_fs(&path, &real_rows); let leaked_root: &'static Path = Box::leak(path.clone().into_boxed_path()); print_header(); let names = [ "real_tiny_1x2", "real_tiny_missing_2x1", "real_shared", "real_unique", "real_mixed_unique", ]; for (name, rows) in names.into_iter().zip(real_rows.iter()) { let repetitions = if name.starts_with("real_tiny") { 128 } else { 1 }; run_scenario(name, Latency::RealFs(leaked_root), rows, 31, repetitions).await; } std::fs::remove_dir_all(path).expect("remove real-filesystem fixture directory"); return; } let scenarios = [ ( "immediate_1x2", Latency::Immediate, 1, 2, 2, false, 51, 10_000, ), ( "immediate_2x1", Latency::Immediate, 2, 1, 2, false, 51, 10_000, ), ( "immediate_1x4", Latency::Immediate, 1, 4, 4, false, 51, 10_000, ), ( "cpu_shared", Latency::Immediate, 64, 8, 32, false, 31, 1_000, ), ( "cpu_unique", Latency::Immediate, 32, 8, 256, false, 31, 1_000, ), ( "local_semantics", Latency::Local { metadata: Duration::from_micros(250), hash: Duration::from_millis(1), }, 2, 4, 4, true, 15, 1, ), ( "local_shared", Latency::Local { metadata: Duration::from_micros(250), hash: Duration::from_millis(1), }, 64, 8, 32, false, 7, 1, ), ( "local_unique", Latency::Local { metadata: Duration::from_micros(250), hash: Duration::from_millis(1), }, 32, 8, 256, false, 7, 1, ), ( "remote_shared", Latency::Remote(Duration::from_millis(4)), 24, 8, 24, false, 7, 1, ), ( "remote_unique", Latency::Remote(Duration::from_millis(4)), 16, 8, 128, false, 7, 1, ), ]; print_header(); let remote_only = args.iter().any(|arg| arg == "--remote-only"); for (name, latency, manifests, chunks, unique, anomalies, samples, repetitions) in scenarios { if remote_only && !name.starts_with("remote_") { continue; } let rows = fixture(manifests, chunks, unique, anomalies); run_scenario(name, latency, &rows, samples, repetitions).await; } }