## M0p1 — Embeddable Runtime Skeleton (329 tests)
- TidalDb with builder(), health_check(), close(), and Drop-based cleanup
- TidalDbBuilder fluent API: ephemeral(), with_data_dir(), wal_dir(), cache_dir()
- Config, StorageMode, ConfigError types; Config(ConfigError) variant on LumenError
- Paths: single source of truth for directory layout (wal, items, users, creators, cache)
- TempTidalHome: test isolation helper gated behind #[cfg(test)] / test-utils feature
- 8 integration tests: tests/sandboxed_storage.rs
## M0p2 — Tooling & Diagnostics (349 tests)
- Workspace root Cargo.toml (members: ["tidal", "tidalctl"])
- tidal/build.rs: BUILD_HASH from GIT_HASH with option_env!() fallback to "dev"
- MetricsState: always-compiled Arc-shared atomics (uptime, health_ok)
- MetricsHandle (metrics feature): hand-rolled TcpListener HTTP, zero new deps
- GET /healthz → {"status":"ok","uptime_secs":N}
- GET /metrics → Prometheus text (tidaldb_uptime_seconds, health_ok, info)
- TidalDbBuilder.enable_metrics(addr) starts background metrics thread
- tidalctl binary: status + paths commands, manual std::env::args() parsing
- 7 metrics integration tests, 9 tidalctl CLI tests
## m1p4 Signal Ledger (in-progress)
- SignalLedger: DashMap<(EntityId, SignalTypeId), EntitySignalEntry>, WAL-first writes
- HotSignalState: #[repr(C, align(64))], lock-free CAS decay, out-of-order handling
- BucketedCounter: 60 per-minute + 168 per-hour circular buffers, trigger-based rotation
- CheckpointMeta + serialize/restore: 983-byte fixed records, atomic WriteBatch
- Property tests: running score matches analytical to 1e-6, decay monotonic, non-negative
- Proptest regression: signals/warm.txt
## Documentation and planning
- ROADMAP: m0p1 COMPLETE (329), m0p2 COMPLETE (349), product track milestones
- PRODUCT_ROADMAP: P0-P4 product milestone track (personal briefing beachhead)
- Milestone planning docs: milestone-0 (phases 1-3), milestone-p (phases 1-5)
- docs/research/tidaldb_tooling_and_diagnostics.md
- ARCHITECTURE.md, CLAUDE.md, VISION.md updates
## Site
- Blog: every-platform-builds-the-same-6-systems.mdx (new)
- Blog: why-tidaldb.mdx (updated)
- next.config.ts, layout.tsx, blog/page.tsx updates
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
201 lines
6.5 KiB
Rust
201 lines
6.5 KiB
Rust
#![allow(clippy::unwrap_used)]
|
|
|
|
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
|
use tidaldb::schema::{EntityId, EntityKind};
|
|
use tidaldb::storage::{
|
|
FjallStorage, InMemoryBackend, StorageEngine, Tag, WriteBatch, encode_key, entity_prefix,
|
|
};
|
|
|
|
fn bench_sequential_put(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("sequential_put");
|
|
|
|
group.bench_function("in_memory_10k", |b| {
|
|
b.iter_batched(
|
|
InMemoryBackend::new,
|
|
|engine| {
|
|
for i in 0u64..10_000 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
engine.put(&key, b"value_data_here").unwrap();
|
|
}
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.bench_function("fjall_10k", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let storage = FjallStorage::open(dir.path()).unwrap();
|
|
(dir, storage)
|
|
},
|
|
|(_dir, storage)| {
|
|
let items = storage.backend(EntityKind::Item);
|
|
for i in 0u64..10_000 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
items.put(&key, b"value_data_here").unwrap();
|
|
}
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
fn bench_random_get(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("random_get");
|
|
|
|
group.bench_function("in_memory_10k", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let engine = InMemoryBackend::new();
|
|
for i in 0u64..10_000 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
engine.put(&key, b"value_data_here").unwrap();
|
|
}
|
|
engine
|
|
},
|
|
|engine| {
|
|
for i in (0u64..10_000).rev() {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
let _ = engine.get(&key).unwrap();
|
|
}
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.bench_function("fjall_10k", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let storage = FjallStorage::open(dir.path()).unwrap();
|
|
let items = storage.backend(EntityKind::Item);
|
|
for i in 0u64..10_000 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
items.put(&key, b"value_data_here").unwrap();
|
|
}
|
|
(dir, storage)
|
|
},
|
|
|(_dir, storage)| {
|
|
let items = storage.backend(EntityKind::Item);
|
|
for i in (0u64..10_000).rev() {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
let _ = items.get(&key).unwrap();
|
|
}
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
fn bench_prefix_scan(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("prefix_scan");
|
|
|
|
// Scan an entity with 10 keys (various tags/suffixes)
|
|
group.bench_function("in_memory_10_keys", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let engine = InMemoryBackend::new();
|
|
let id = EntityId::new(42);
|
|
let tags = [Tag::Evt, Tag::Sig, Tag::Meta, Tag::Rel, Tag::Mv, Tag::Idx];
|
|
for (i, tag) in tags.iter().enumerate() {
|
|
let key = encode_key(id, *tag, format!("suffix_{i}").as_bytes());
|
|
engine.put(&key, b"data").unwrap();
|
|
}
|
|
// Add extra keys under same tag
|
|
for i in 0..4 {
|
|
let key = encode_key(id, Tag::Evt, format!("evt_{i}").as_bytes());
|
|
engine.put(&key, b"event_data").unwrap();
|
|
}
|
|
engine
|
|
},
|
|
|engine| {
|
|
let prefix = entity_prefix(EntityId::new(42));
|
|
let results: Vec<_> = engine
|
|
.scan_prefix(&prefix)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.unwrap();
|
|
assert_eq!(results.len(), 10);
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
fn bench_batch_write(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("batch_write");
|
|
|
|
group.bench_function("in_memory_100_ops", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let engine = InMemoryBackend::new();
|
|
let mut batch = WriteBatch::with_capacity(100);
|
|
for i in 0u64..100 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
batch.put(key, b"value".to_vec());
|
|
}
|
|
(engine, batch)
|
|
},
|
|
|(engine, batch)| {
|
|
engine.write_batch(batch).unwrap();
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.bench_function("fjall_100_ops", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let storage = FjallStorage::open(dir.path()).unwrap();
|
|
let mut batch = WriteBatch::with_capacity(100);
|
|
for i in 0u64..100 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
batch.put(key, b"value".to_vec());
|
|
}
|
|
(dir, storage, batch)
|
|
},
|
|
|(_dir, storage, batch)| {
|
|
let items = storage.backend(EntityKind::Item);
|
|
items.write_batch(batch).unwrap();
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.bench_function("in_memory_1000_ops", |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let engine = InMemoryBackend::new();
|
|
let mut batch = WriteBatch::with_capacity(1000);
|
|
for i in 0u64..1000 {
|
|
let key = encode_key(EntityId::new(i), Tag::Sig, b"");
|
|
batch.put(key, b"value".to_vec());
|
|
}
|
|
(engine, batch)
|
|
},
|
|
|(engine, batch)| {
|
|
engine.write_batch(batch).unwrap();
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
group.finish();
|
|
}
|
|
|
|
criterion_group!(
|
|
benches,
|
|
bench_sequential_put,
|
|
bench_random_get,
|
|
bench_prefix_scan,
|
|
bench_batch_write,
|
|
);
|
|
criterion_main!(benches);
|