#![allow(clippy::unwrap_used, clippy::cast_precision_loss)] //! Criterion benchmark for scatter-gather RETRIEVE fan-out. //! //! Today `scatter_gather_retrieve` spawns one OS thread *per shard, per query* //! behind a single global `Mutex` + `Condvar` permit semaphore (perf //! sweep 2026-06-13, finding rank 2). Before this bench existed there was no //! before/after for that threading model. It measures: //! //! - **`scatter_fanout/regions{4,16}`** — single-query fan-out latency. The gap //! between 4 and 16 regions is the per-shard thread create/teardown + //! 2 MiB-stack-reservation cost, since the underlying per-shard reads over the //! tiny replicated dataset are near-instant. //! //! - **`scatter_fanout_concurrent/regions{4,16}_q8`** — 8 concurrent queries //! issued at once, so `8 * regions` workers all contend the one global //! semaphore. This is the lock-bounce / queue-depth signal that a reused //! worker pool (wave 5) must improve without regressing single-query latency. //! //! Run: //! ```bash //! cargo bench -p tidal-server --bench scatter //! ``` use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use criterion::{Criterion, criterion_group, criterion_main}; use tidal_server::scatter_gather::scatter_gather_retrieve; use tidaldb::query::retrieve::Retrieve; use tidaldb::replication::shard::RegionId; use tidaldb::schema::{DecaySpec, EntityId, EntityKind, Schema, SchemaBuilder, Window}; use tidaldb::testing::SimulatedCluster; use tidaldb::testing::cluster::ClusterConfig; fn bench_schema() -> Schema { let mut builder = SchemaBuilder::new(); let _ = builder .signal( "view", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600), }, ) .windows(&[Window::OneHour]) .velocity(false) .add(); builder.build().unwrap() } /// Build an `n`-region replicated cluster pre-seeded with 64 viewed items. /// Returns the cluster plus the shard list and region-name map that /// `scatter_gather_retrieve` consumes. fn build_cluster( n: u16, ) -> ( Arc, Vec, HashMap, ) { let regions: Vec = (0..n).map(RegionId).collect(); let config = ClusterConfig { regions: regions.clone(), leader_region: RegionId(0), schema: bench_schema(), profiles: Vec::new(), transports: None, }; let cluster = Arc::new(SimulatedCluster::build(config)); // Replicated topology: write to the leader, all regions see all data. for i in 1..=64u64 { let eid = EntityId::new(i); cluster .write_item_with_metadata(eid, &HashMap::new()) .unwrap(); cluster.write_signal("view", eid, i as f64).unwrap(); } let names: HashMap = regions .iter() .map(|&r| (r, format!("region-{}", r.0))) .collect(); (cluster, regions, names) } fn trending_query() -> Retrieve { Retrieve::builder() .profile("trending") .limit(20) .build() .unwrap() } fn fanout_latency(c: &mut Criterion) { let mut group = c.benchmark_group("scatter_fanout"); group.sample_size(30); for n in [4u16, 16] { let (cluster, shards, names) = build_cluster(n); let query = trending_query(); group.bench_function(format!("regions{n}"), |b| { b.iter(|| { let (result, _meta) = scatter_gather_retrieve(&cluster, &query, &shards, &names, None).unwrap(); assert!(!result.items.is_empty()); }); }); } group.finish(); } fn fanout_concurrent(c: &mut Criterion) { const CONCURRENCY: usize = 8; let mut group = c.benchmark_group("scatter_fanout_concurrent"); group.sample_size(20); group.measurement_time(Duration::from_secs(12)); for n in [4u16, 16] { let (cluster, shards, names) = build_cluster(n); let query = trending_query(); group.bench_function(format!("regions{n}_q{CONCURRENCY}"), |b| { b.iter_custom(|iters| { let mut elapsed = Duration::ZERO; for _ in 0..iters { let start = Instant::now(); let threads: Vec<_> = (0..CONCURRENCY) .map(|_| { let cluster = Arc::clone(&cluster); let query = query.clone(); let shards = shards.clone(); let names = names.clone(); std::thread::spawn(move || { let (result, _meta) = scatter_gather_retrieve( &cluster, &query, &shards, &names, None, ) .unwrap(); assert!(!result.items.is_empty()); }) }) .collect(); for t in threads { t.join().unwrap(); } elapsed += start.elapsed(); } elapsed }); }); } group.finish(); } criterion_group!(benches, fanout_latency, fanout_concurrent); criterion_main!(benches);