store: benchmarks at 10k entries on disk
check / check (push) Canceled after 0s

benches/store.rs (criterion, in `just bench`): open 249 ms, viewer load
616 ms, full verify 809 ms, post one entry (four synced appends) 62 ms.
The books are built on a temp dir and copied under target/ so every
sync hits a real disk. Baselines in docs/testing.md; no budget, by
decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-13 19:06:11 -06:00
1 parent fb3b4ab476
commit 2cbb6a263f
5 files changed
+116 -1

No files matched your search

Generated
+1
View File
@@ -3564,6 +3564,7 @@ name = "ledger-store"
version = "0.1.0"
dependencies = [
"chrono",
"criterion",
"hex",
"ledger-core",
"proptest",
+5
View File
@@ -21,6 +21,11 @@ hex = "0.4"
tempfile = "3"
proptest = "1"
rust_decimal = "1"
criterion = "0.8.2"
[[bench]]
name = "store"
harness = false
[lints]
workspace = true
+98
View File
@@ -0,0 +1,98 @@
//! Benchmarks for the store's hot paths (rank 6: a baseline, never
//! correctness; no budget, by decision, 2026-10-13).
//!
//! `cargo bench -p ledger-store --bench store`. Books of 10k two-line
//! entries, on a real disk (under `target/`), since every journal append
//! is synced and a RAM disk would hide that: opening them (verify the hash
//! chain, replay, catch the database up), what the viewer does to load
//! them, a full verify, and posting one more entry (four synced appends).
use chrono::NaiveDate;
use criterion::{Criterion, criterion_group, criterion_main};
use ledger_core::{AccountType, Currency, Money, Side};
use ledger_store::{Snapshot, Store};
use rust_decimal::Decimal;
use std::path::{Path, PathBuf};
const ENTRIES: u32 = 10_000;
fn post(store: &mut Store, n: u32, a: u64, b: u64) {
let day = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap() + chrono::Days::new(u64::from(n % 365));
let e = store.create_entry(day, "bench").unwrap();
let m = Money::new(Decimal::new(i64::from(n % 50_000) + 1, 2), Currency::USD);
store.add_line(e, a, Side::Debit, m).unwrap();
store.add_line(e, b, Side::Credit, m).unwrap();
store.post_entry(e).unwrap();
}
fn copy_dir(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for e in std::fs::read_dir(from).unwrap() {
let e = e.unwrap();
let target = to.join(e.file_name());
if e.file_type().unwrap().is_dir() {
copy_dir(&e.path(), &target);
} else {
std::fs::copy(e.path(), target).unwrap();
}
}
}
/// 10k entries over 40 accounts, built in a temp dir (fast) and copied to
/// disk under target/ (honest syncs). Built once per run.
fn books() -> PathBuf {
let disk = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/bench-books");
let _ = std::fs::remove_dir_all(&disk);
let tmp = tempfile::tempdir().unwrap();
{
let mut store = Store::create(tmp.path(), Currency::USD).unwrap();
let types = [
AccountType::Asset,
AccountType::Liability,
AccountType::Equity,
AccountType::Revenue,
AccountType::Expense,
];
let ids: Vec<u64> = (0..40)
.map(|i| {
store
.add_account(&format!("{}", 1000 + i), "a", types[i % 5], None, false)
.unwrap()
})
.collect();
for n in 0..ENTRIES {
let a = ids[(n as usize * 7) % 40];
let b = ids[(n as usize * 13 + 1) % 40];
if a != b {
post(&mut store, n, a, b);
}
}
}
let _ = std::fs::remove_file(tmp.path().join(ledger_store::LOCK_FILE));
copy_dir(tmp.path(), &disk);
disk
}
fn bench(c: &mut Criterion) {
let dir = books();
let mut g = c.benchmark_group("store_10k");
g.sample_size(10);
g.bench_function("open", |b| b.iter(|| Store::open(&dir).unwrap()));
g.bench_function("viewer_load", |b| b.iter(|| Snapshot::load(&dir).unwrap()));
g.bench_function("verify", |b| {
let store = Store::open(&dir).unwrap();
b.iter(|| store.verify().unwrap())
});
g.bench_function("post_one_entry", |b| {
let mut store = Store::open(&dir).unwrap();
let mut n = ENTRIES;
b.iter(|| {
n += 1;
post(&mut store, n, 1, 2);
})
});
g.finish();
}
criterion_group!(benches, bench);
criterion_main!(benches);
+11 -1
View File
@@ -25,7 +25,7 @@ the store's journal safety; the other crates come next (gap list).
| Fuzzing | crash, hang, broken books (4) | `fuzz/fuzz_targets/`, corpora in `fuzz/corpus/` | `just fuzz <target> <secs>` | 10 min each | weekly |
| Miri | undefined behavior in our use of dependencies (4) | the core's unit, bridge, invariant tests | `just miri` | ~2 min | on demand |
| Golden | `tests/golden/fixture_company.json` (5) | `invariants.rs` | `just check` | <1s | every change |
| Benchmarks | a baseline (6) | `benches/books.rs` | `just bench` | ~1 min | none (no budget, by decision) |
| Benchmarks | a baseline (6) | `ledger-core/benches/books.rs`, `ledger-store/benches/store.rs` | `just bench` | ~2 min | none (no budget, by decision) |
| Mutation | can the tests fail? | `ledger-core` (2026-10-13: 1,011 caught + 13 timeouts of 1,024 viable, 0 missed) | `just mutants` | ~3 h | weekly |
CI: `.github/workflows/check.yml` runs `just check` on every push;
@@ -57,6 +57,16 @@ laptop:
| balance sheet | 47 µs | 1.04 ms |
| one month's P&L | 25 µs | 248 µs |
The store, 10k two-line entries on the laptop's disk (every journal
append is synced; `benches/store.rs` in ledger-store):
| | 10k entries |
|---|---|
| open the books (verify the chain, replay, catch the database up) | 249 ms |
| the viewer loading them | 616 ms |
| a full verify (replay, projection, every table) | 809 ms |
| post one entry (create, two lines, post: four synced appends) | 62 ms |
## Formerly assumed: checked against the standards (2026-10-13)
Stephen isn't an accountant and won't adjudicate these; each was checked
+1
View File
@@ -69,6 +69,7 @@ mutants:
# Benchmarks of the core's hot paths (no budget gates them yet)
bench:
cargo bench -p ledger-core --bench books
cargo bench -p ledger-store --bench store
# Build the release binaries (CLI, MCP server, GUI)
build: