- 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>
129 lines
4.2 KiB
Rust
129 lines
4.2 KiB
Rust
#![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);
|