tidaldb/tidal/benches/signals.rs
jordan 6fdaa1584b feat: complete M1 signal engine — m0p3 samples/docs, m1p5 TidalDb API, examples, and periodic checkpoint
- m0p3: CONTRIBUTING.md with run-samples checklist, all 4 examples
  (quickstart, cli_embedding, axum_embedding, actix_embedding), doc-test
  coverage for every public API surface
- m1p5: TidalDb public API — write_item, signal, read_decay_score,
  read_windowed_count, read_velocity; StorageBox enum routing memory vs
  fjall; WalSender/WalHandleWriter bridge; WAL replay on open
- Periodic checkpoint: 30s background thread for persistent+schema mode;
  FjallBackend::Clone (O(1), fjall::Keyspace is ref-counted); graceful
  shutdown via Arc<AtomicBool> + join before final checkpoint
- ROADMAP.md: M0 and M1 fully marked COMPLETE (341 tests passing)
- Milestone 2 planning scaffolding added under docs/planning/milestone-2/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 22:45:10 -07:00

129 lines
4.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![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<EntityId> = (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);