#![allow(clippy::unwrap_used)] use std::time::Duration; use criterion::{Criterion, black_box, criterion_group, criterion_main}; use tidaldb::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window}; use tidaldb::signals::{NoopWalWriter, SignalLedger, SignalTypeId}; fn view_ledger() -> (SignalLedger, SignalTypeId) { let mut builder = SchemaBuilder::new(); let _ = builder .signal( "view", EntityKind::Item, DecaySpec::Exponential { half_life: Duration::from_secs(7 * 24 * 3600), }, ) .windows(&[Window::OneHour, Window::TwentyFourHours, Window::SevenDays]) .velocity(false) .add(); let schema = builder.build().unwrap(); let type_id = { // Alphabetical sort assigns "view" → id 0 (only signal). SignalTypeId::new(0) }; let ledger = SignalLedger::new(schema, Box::new(NoopWalWriter)); (ledger, type_id) } // Pre-computed lambda for a 7-day half-life exponential decay. const LAMBDA_7D: f64 = std::f64::consts::LN_2 / (7.0 * 24.0 * 3600.0); /// Benchmark: single signal write (excluding WAL — `NoopWalWriter`). /// Target: < 100ns. fn bench_single_signal_write(c: &mut Criterion) { let (ledger, _type_id) = view_ledger(); let entity_id = EntityId::new(1); // Fixed timestamp avoids SystemTime::now() overhead per iteration. let fixed_ns = Timestamp::now().as_nanos(); let ts = Timestamp::from_nanos(fixed_ns); c.bench_function("signal_write_single", |b| { b.iter(|| { ledger .record_signal( black_box("view"), black_box(entity_id), black_box(1.0_f64), black_box(ts), ) .unwrap(); }); }); } /// Benchmark: decay score read for a single entity. /// Setup: 100 pre-written signals to ensure a non-trivial hot state. /// Target: < 100ns. fn bench_decay_score_read(c: &mut Criterion) { let (ledger, _type_id) = view_ledger(); let entity_id = EntityId::new(42); // Pre-warm: 100 signals spread over the past 7 days. let base_ns = Timestamp::now().as_nanos(); let seven_days_ns: u64 = 7 * 24 * 3600 * 1_000_000_000; for i in 0u64..100 { let ts = Timestamp::from_nanos( base_ns.saturating_sub(seven_days_ns) + i * (seven_days_ns / 100), ); ledger.record_signal("view", entity_id, 1.0, ts).unwrap(); } c.bench_function("signal_decay_score_read", |b| { b.iter(|| { ledger .read_decay_score(black_box(entity_id), black_box("view"), black_box(0)) .unwrap() }); }); } /// Benchmark: 200-entity scoring pass using direct `DashMap` access to isolate /// the hot-path read from schema lookup overhead. /// Setup: 200 entities, each with 50 pre-written signals. /// Target: < 5µs total. fn bench_200_entity_scoring_pass(c: &mut Criterion) { let (ledger, type_id) = view_ledger(); // Pre-warm: 200 entities × 50 signals each. let base_ns = Timestamp::now().as_nanos(); let entity_ids: Vec = (0u64..200).map(EntityId::new).collect(); for &entity_id in &entity_ids { for j in 0u64..50 { let ts = Timestamp::from_nanos( base_ns.saturating_sub(3_600_000_000_000) + j * 72_000_000_000, ); ledger.record_signal("view", entity_id, 1.0, ts).unwrap(); } } let now_ns = Timestamp::now().as_nanos(); c.bench_function("signal_200_entity_scoring_pass", |b| { b.iter(|| { let mut sum = 0.0_f64; for &entity_id in black_box(&entity_ids) { if let Some(entry) = ledger.entries().get(&(entity_id, type_id)) { sum += entry.hot.current_score( black_box(0), black_box(now_ns), black_box(LAMBDA_7D), ); } } black_box(sum) }); }); } criterion_group!( benches, bench_single_signal_write, bench_decay_score_read, bench_200_entity_scoring_pass, ); criterion_main!(benches);