- story 02: `status: done`, `review_pending` (forks 1–7 auto-approved for
autonomy); progress rows 6a ✅, 6b ➡ databasev2 5 Phase A, 7 `a310496`;
5c/5d rows cite the `dev` hashes (the pre-merge ones were unreachable);
task 6a's Given/When/Then met; Info records the seven forks (sentinel over
`:memory:`, its rules, the refusal contract, startup-only, the budget
leaves for 5, library-owned tables bind consumers, the v8 table bit);
History keeps the first cut that refused every class-bearing program
- database/src/CODE-LOGIC.md: "Startup refusal + WO_EPHEMERAL" — contract,
hatch, table bit, measured blast radius, deferred items, proof; the
dispatcher paragraph no longer says a failed commit un-applies the row
(fatal since databasev2 4 part A; WO_T_IO unreachable from a write path)
- residency spec + plan: task 6 items annotated with the 2026-09-09
decisions; the byte budget marked moved to databasev2 5
- README, seven example READMEs and four guides carry the one-line rule
(durable default refuses without WO_DATA; WO_EPHEMERAL=1; durable:
false); shop's RAM-only command sets the sentinel
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 2c3531998124042fe736388e8b926abda3841194)
87 lines
6.6 KiB
Markdown
87 lines
6.6 KiB
Markdown
# db-bench — iteration 22's load generator
|
||
|
||
The measurement backbone (spec:
|
||
`docs/superpowers/specs/2026-08-21-db-bench-design.md`). Pure `.wo`;
|
||
every measured mode prints one machine-parsable line per operation
|
||
class:
|
||
|
||
<op> <count> <ops/sec> <p50us> <p99us>
|
||
|
||
Timing is per-operation via `time.ticks` (CLOCK_MONOTONIC µs).
|
||
Percentiles come from a 1µs-bucket histogram clamped at 20000µs — exact
|
||
to the microsecond below the clamp; a p99 AT 20000 means "clamp or
|
||
worse". (A histogram, not the spec's reservoir: the language has no
|
||
container element-write or sort, and the histogram's tail fidelity is
|
||
strictly better. Recorded as a plan deviation.)
|
||
|
||
## Modes
|
||
|
||
| mode | what it prices |
|
||
| --- | --- |
|
||
| `all N` | the throughput campaign in ONE process: seed N, read N/2, query N/10, write N/2. Without `WO_DATA` the store is RAM and dies with the process, so the measured modes must share the seeding run. |
|
||
| `seed N` | timed inserts: one parent per 100 children (FK probe each insert, unique-index maintenance per parent), k non-unique (10 rows/key), deterministic v. Writes `Meta` expectation rows. |
|
||
| `read N` | indexed take-1 point lookups, LCG-spread keys. |
|
||
| `query N` | full equality probes on the k index (≈10 rows each), materialized and counted. |
|
||
| `write N` | alternating inserts (disjoint k range 2e6+) and updates through query results. Corrupts the checksum by design — durability legs run on a fresh store. |
|
||
| `wal N` | the crash battery's vehicle: insert-only (k range 1e6+), `acked <i>` printed AFTER each insert returns — the return IS the ack (RAM applied, WAL record staged, ONE commit done). |
|
||
| `wmix N C` | **databasev2 4:** every op a durable write (update through a query result), C at once. Exists because `mix` writes on one op in ten with C=4 — 20 writes in a quick run, measured mean batch **1.01** — so no existing leg could show whether group commit engages. Histogram kind 2, because a replayed store still holds the seeding run's kind-0/1 `Hist` rows. Seed first. |
|
||
| `boot` | **databasev2 3:** does NOTHING. With `WO_DATA` set the runtime replays the whole log before `main` runs, so a mode with no work of its own is the only honest way to price boot |
|
||
| `verify` | store vs its own Meta rows: count, checksum, one unique probe. Exit 3 on mismatch. |
|
||
| `verify-acked M` | after kill -9 mid-`wal`: rows 1..M exist with the right v; rows beyond M allowed (acked after the last print flushed). Exit 3 on mismatch. |
|
||
|
||
## Env knobs
|
||
|
||
| var | effect |
|
||
| --- | --- |
|
||
| `WO_DATA=<dir>` (or `WO_DATA=<path>.db`, below) | durability on: replay `<dir>/shard-0.wal` at boot, log every write. A program with any durable table (the default) refuses to start without `WO_DATA`; `WO_EPHEMERAL=1` opts into a RAM-only run, `@table(durable: false)` opts a table out. |
|
||
| `WO_EPHEMERAL=1` | **databasev2 2 task 6a:** the RAM-only opt-in the driver sets on its ram/msgrate/growth/randread legs. A `WO_DATA` exported in your shell no longer silently turns those legs durable — the two are incompatible and the run refuses loudly |
|
||
| `WO_SHARDS=<n>` | shard count. **`1` means every statement runs inline on shard 0 and group commit cannot engage** — batches form only where writes queue from other shards |
|
||
| `WO_CHECKPOINT_BYTES` / `WO_CHECKPOINT_RATIO` | **databasev2 3:** the checkpoint trigger — the log must exceed the floor AND exceed the ratio times the last compaction's own size. A tiny floor forces compaction in a few writes, which is how the gate tests the policy at all; an enormous one disables it, which is how the checkpoint leg measures the same workload with and without |
|
||
| `WO_WAL_STATS=1` | **databasev2 4:** print one line at exit — `walstats batches=… records=… peak_batch=… peak_staged=… compactions=… compact_us_max=… compact_us_total=… compacted_bytes=…`. Opt-in so it does not pollute every durable program's output. Mean batch is `records/batches`; **mean 1.0 means group commit is not engaging**, which is expected for a serial writer or `WO_SHARDS=1` and a bug anywhere else |
|
||
| `WO_DATA=<path>.db` | **databasev2 7:** the store as ONE file — the path IS the log (created if absent, its parent must exist; a directory or trailing `/` keeps the `<dir>/shard-0.wal` form). `scripts/db-bench.py --wo-data-file` runs the restart proof and the kill -9 battery against `<tmp>/app.db` instead of a directory; same acceptance, no metric, baseline untouched |
|
||
|
||
**Do not put `WO_DATA` on `/tmp`.** It is `tmpfs` on the reference machine,
|
||
where `fdatasync` is free: the same `wmix` run measured **195 000 ops/s at p50
|
||
1 µs** there against **2200 ops/s at p50 7200 µs** on ext4. There is no
|
||
durability barrier to price on a memory filesystem. The driver keeps its stores
|
||
under `bench/` for exactly this reason.
|
||
|
||
## Coordination idiom (this side of iteration 31)
|
||
|
||
There is no request/response surface yet: concurrent modes drive
|
||
completion the db-actor way — actors write rows, main polls the store
|
||
until the expected count, then settles. Retired when 31 lands.
|
||
|
||
## Standing finding (2026-08-21, first run)
|
||
|
||
A hand-built `multi Bucket` of insert results SEGVs on drop: the
|
||
compiler classifies the elements OWNED while table refs are scalar ids.
|
||
Query-built multis are runtime-typed and safe. Worked around here
|
||
(single ref local, bucket-major seeding); the compiler fix is its own
|
||
slice.
|
||
|
||
## Reference-machine numbers (first campaign, 2026-08-21)
|
||
|
||
`bench/baseline.json` is the contract; headline readings:
|
||
|
||
- ram seed 245–290k inserts/s; **durable seed ≈4.5k/s** (fsync-per-commit
|
||
≈220µs each — the gap iteration 23 exists to close).
|
||
- reads/queries ≈1.1–1.3M ops/s at p50 1µs since the read-path index
|
||
slice (2026-08-22, engine `wo_idx_probe` + emitter index selection) —
|
||
up from ≈1.5k/s at p50 600µs when point lookups walked every slab
|
||
(~×850). mixread 89k ops/s single-shard, ~1.9k multi-shard (was
|
||
1,280 / 21): the RPC round-trip is now the visible cost, as designed.
|
||
- msgrate ≈13M msgs/s same-heap vs ≈2.4M cross-shard (the mutex-inbox
|
||
number, stage-2 deviation 4).
|
||
- Tolerance policy lives in the DRIVER (`tolerance_for`), not hand-edits
|
||
— a baseline refresh regenerates it: mix*/sN/read/query 50%
|
||
(scheduling + µs-scale jitter), rest 15%; latency floors
|
||
`max(4×value, 100µs)` — the tripwire means "µs became ms".
|
||
|
||
## The gate must bite (proven 2026-08-21)
|
||
|
||
`scripts/db-bench.py --check <results.json>` evaluates a recorded run:
|
||
the real results pass 74/0; a doctored copy FAILS on exactly the
|
||
doctored metrics — use a 15%-class metric (seed) halved plus a
|
||
50%-class metric (read) quartered, so both tolerance classes prove they
|
||
bite. Re-run the smoke after any gate or policy change.
|