plan_bench — a write-heavy-then-derive-read benchmark corpus
Area: Benchmarks Teaches: the two-phase benchmark profile — a large scripted series of mutations followed by a derived measurement that traverses all of it. A self-contained investment ledger whose mutation count is the scaling knob, sized so the headline configuration runs in the dozens of seconds (real benchmark signal) while a small configuration stays fast for CI. Prerequisites: double-entry accounting (mutate flows and aggregates); robot plan execution (rule-driven derivation). Run:
ox run-scenario examples/plan_bench --scenario scenarios/plan.toml && ox test examples/plan_bench— andexamples/plan_bench/bench.shto time it at scale. (The--scenariopin matters after abench.shrun: a barerun-scenarioalso picks up the locally generatedscenarios/generated.toml.)
A repository-owned benchmark corpus with the two-phase profile a plan-search benchmark measures: do a large scripted series of mutations, then measure a derived quantity whose computation traverses everything the mutations built.
The domain is a self-contained, simplified investment ledger — lots are
acquired and sold, and each disposition’s realized gain (proceeds - costBasis)
is classified by holding period and asset class, then rolled up. Nothing here is
tax law or an external model; it is entirely owned by this repository and
formatter-canonical.
Shape
- Mutation phase (
planner.ar) —recordBuy+recordSale, oneLot/Sale/soldedge per pair. The number of pairs is the scaling knob. - Measurement phase (
rules.ar) — the derived rollupstotalRealizedGain,longTermGain,shortTermGain, and the groupednetByAssetClass. Reading them forces the derivation plane to traverse every accumulated disposition.
The OrphanDisposition check re-evaluates on every commit, so under
ox run-scenario (one transaction per step) the write phase is deliberately
super-linear in the pair count — that is what gives the benchmark real signal
(dozens of seconds at the headline scale, versus the millisecond-scale rest of
the repository).
Construct coverage
- mutate/apply flows with
insert/update(planner); - deductive reads feeding inserts through nested
forloops over parameterized queries (planner::classifyRound); - rule-driven derivation — joins, comparisons, global and grouped aggregates,
and an integrity
check(rules); - a relation with constrained endpoints (
catalog::heldIn); - a concept (kind) hierarchy and a
standpointscope (catalog,seed).
Running it
Correctness golden (tiny, hand-checkable — 2 securities, 3 lots, 3 sales):
ox run-scenario examples/plan_bench # runs scenarios/plan.toml, asserts the rollups
ox test examples/plan_bench # the in-language goldens (tests/mod.ar)
Benchmark at a chosen scale:
cd examples/plan_bench
./bench.sh 25 # smoke scale (~0.1s)
./bench.sh 500 # headline scale (~67s), dozens of seconds of real signal
bench.sh calls gen.py <N> to emit scenarios/generated.toml (git-ignored),
builds the package, and reports the ox check and ox run-scenario
wall-clocks. Measured on an Apple-silicon dev reference machine with release
ox (the timings below are Apple-silicon reference numbers):
| pairs (N) | ox run-scenario |
|---|---|
| 25 | ~0.1 s |
| 100 | ~1.4 s |
| 300 | ~18 s |
| 500 | ~67 s |
ox check is ~0.01 s at every scale (the schema is fixed-size; only the
scenario grows), so the corpus build gate stays fast.
Note:
ox run-scenario examples/plan_benchwith no--scenariodiscovers and runs everyscenarios/*.toml. After abench.shrun the git-ignoredscenarios/generated.tomlsits alongsideplan.toml, so a barerun-scenariowill also execute the generated scale scenario. Pass--scenario scenarios/plan.tomlto run only the correctness golden.