tidaldb/tidal/benches/recovery.rs
2026-02-23 22:41:16 -07:00

114 lines
4.0 KiB
Rust

#![allow(clippy::unwrap_used, clippy::cast_precision_loss)]
//! Criterion benchmarks for cold-start recovery time.
//!
//! Measures the open-to-ready latency when `TidalDb::builder().open()` replays
//! a WAL + checkpoint from a previously populated database. This is the metric
//! operators care about most during restarts and crash recovery.
//!
//! ## Scope
//!
//! This benchmark measures **checkpoint restore + in-memory index rebuild** only.
//! All data is written via `db.close()` (clean checkpoint), so on the next open
//! the WAL replay phase is near-zero (the checkpoint covers all events). This is
//! the realistic production recovery path for graceful shutdowns.
//!
//! A true WAL-backlog benchmark (measuring recovery from unsaved in-flight events)
//! requires writing events after the checkpoint without calling `close()`. That
//! scenario is deferred and is not covered here.
//!
//! ## Scale
//!
//! The Criterion benchmark uses 10K entities (scaled down from the 1M specified
//! in task-05 for local iteration speed). Hard SLA bounds are enforced by the
//! integration tests in `tests/m7_recovery_sla.rs` (run as part of `cargo test`).
//! The full 1M-item SLA test (`recovery_under_30_seconds`) is marked
//! `#[ignore = "expensive"]` and must be run explicitly:
//!
//! ```bash
//! cargo test --manifest-path tidal/Cargo.toml --test m7_recovery_sla -- --ignored
//! ```
use std::time::Duration;
use criterion::{Criterion, criterion_group, criterion_main};
use tidaldb::TidalDb;
use tidaldb::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window};
fn bench_schema() -> tidaldb::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::AllTime])
.velocity(false)
.add();
builder.build().expect("valid schema")
}
fn generate_test_data(dir: &std::path::Path) {
let schema = bench_schema();
// Write 10K entities with signals (scaled down from 1M for CI).
// The benchmark is designed for local profiling; the smoke test (below)
// is the gatekeeping test for CI.
let db = TidalDb::builder()
.with_data_dir(dir)
.with_schema(schema.clone())
.open()
.expect("open should succeed");
let base_ns = 1_000_000_000_000u64;
// Write signals for entities.
let entity_count = 10_000u64;
for entity_id in 1..=entity_count {
let ts = Timestamp::from_nanos(base_ns + entity_id * 1_000_000);
db.signal("view", EntityId::new(entity_id), 1.0, ts)
.expect("signal should succeed");
}
// Force clean shutdown (triggers checkpoint + WAL compaction).
db.close().expect("close should succeed");
}
fn recovery_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("recovery");
// Recovery benchmarks can be slower -- allow more time.
group.sample_size(10);
group.measurement_time(Duration::from_secs(30));
// Generate the test data directory (done once, reused across iterations).
let dir = tempfile::tempdir().expect("tempdir");
generate_test_data(dir.path());
let schema = bench_schema();
group.bench_function("cold_start_10k_items", |b| {
b.iter(|| {
let db = TidalDb::builder()
.with_data_dir(dir.path())
.with_schema(schema.clone())
.open()
.expect("open should succeed");
// Verify the database is actually functional.
let count = db
.read_windowed_count(EntityId::new(1), "view", Window::AllTime)
.expect("read should succeed");
assert!(count > 0, "entity 1 should have signals after recovery");
db.close().expect("close should succeed");
});
});
group.finish();
}
criterion_group!(benches, recovery_benchmark);
criterion_main!(benches);