Contents
Extension store cache: results
Run on the EC2 c7i.8xlarge in env.txt (32 vCPU, 61.8 GiB, one NVMe), mentat
d34e0177 (perf(sqlite-ext,duckdb): cache the open Store per path), built
from git archive (none of the working tree’s uncommitted changes).
DuckDB 1.5.5, Python 3.11. REPS=1, MIN_S=10, MAX_S=60, PROBE_S=20.
Full per-scenario tables: summary.md (generated by report.py); raw rows:
raw.csv; checks: checks.txt (104 PASS, 0 FAIL, incl. the post-write checks).
Columns:
- (a) per-call open, 1.9.0 —
baseline/, the earlierext-baserun: every call opened the store, andStore::openscanned the whole log (O(history)). Onlysloaded; atma single call took 5 s (duckdb) or hit the 20 s probe ceiling (sqlite-ext), and the load through the extension crashed. - (a') per-call open, this build —
nocache-*.csv: the same build withMENTAT_STORE_CACHE=0. Since5072ef13(another agent’s commit)Store::openis O(1), so this column separates the open-cost fix from the cache. Four scenarios only. - (b) cached — this run.
m for (a') and (b): the extensions query a copy of the 9.85M-datom store the
embedded runner built at 1.9.0, upgraded to schema v2 on first open (16 s,
once). Loading m through the extension was not attempted.
Single client, p50 ms
| backend | scale | scenario | (a) per-call open 1.9.0 | (a') per-call open, O(1) open | (b) cached |
|---|---|---|---|---|---|
| sqlite-ext | s | point_lookup | 483.8 | 0.527 | 0.083 |
| sqlite-ext | s | ref_traversal | 526.8 | 40.3 | 37.4 |
| sqlite-ext | s | aggregate | 588.0 | 94.3 | 89.4 |
| sqlite-ext | s | predicate_scan | 520.7 | 31.2 | |
| sqlite-ext | s | pull | 486.5 | 0.526 | 0.047 |
| sqlite-ext | s | as_of | ceiling (>20 s) | 84.8 | |
| sqlite-ext | s | since | 536.5 | 4.666 | |
| sqlite-ext | s | input_bindings | 485.8 | 0.485 | |
| sqlite-ext | m | point_lookup | ceiling (>20 s) | 1.116 | 0.514 |
| sqlite-ext | m | ref_traversal | ceiling (>20 s) | 518.6 | 479.3 |
| sqlite-ext | m | aggregate | ceiling (>20 s) | 1,122.7 | 1,083.5 |
| sqlite-ext | m | predicate_scan | ceiling (>20 s) | 390.7 | |
| sqlite-ext | m | pull | ceiling (>20 s) | 2.937 | 1.149 |
| sqlite-ext | m | as_of | ceiling (>20 s) | 1,096.5 | |
| sqlite-ext | m | since | ceiling (>20 s) | 47.0 | |
| sqlite-ext | m | input_bindings | ceiling (>20 s) | 2.149 | |
| duckdb | s | point_lookup | 465.9 | 1.171 | 0.625 |
| duckdb | s | ref_traversal | 510.5 | 41.0 | 38.7 |
| duckdb | s | aggregate | 568.1 | 91.9 | 89.6 |
| duckdb | s | predicate_scan | 503.3 | 27.9 | |
| duckdb | s | pull | 461.2 | 1.149 | 0.559 |
| duckdb | s | as_of | 18,069.0 | 85.3 | |
| duckdb | s | since | 520.6 | 5.123 | |
| duckdb | s | input_bindings | 468.3 | 1.051 | |
| duckdb | m | point_lookup | 5,003.4 | 1.757 | 1.126 |
| duckdb | m | ref_traversal | 5,542.9 | 500.3 | 493.4 |
| duckdb | m | aggregate | 6,133.1 | 1,088.8 | 1,066.9 |
| duckdb | m | predicate_scan | 333.3 | ||
| duckdb | m | pull | 1.187 | 1.798 | |
| duckdb | m | as_of | ceiling (>20 s) | 1,096.0 | |
| duckdb | m | since | 48.3 | ||
| duckdb | m | input_bindings | 2.660 |
Per call, (a) → (b) is 470-520 ms → 0.05-0.6 ms at s for the cheap queries:
that ~480 ms was Store::open. (a') → (b) is what the cache adds on top of
the O(1) open: 0.53 → 0.083 ms (sqlite-ext point_lookup at s), 0.53 → 0.047 ms
(pull). An open is still a SQLite connection, the WAL/shm mapping, and the
schema read. Heavy queries (ref_traversal, aggregate at m) are
dominated by the query itself and barely move between (a') and (b). as_of
went from ceiling / 18 s to 85 ms, but that is the history index in
5072ef13, not the cache.
duckdb’s cheap reads are ~0.5 ms slower than sqlite-ext’s: that is DuckDB’s bind/plan/execute for a table function plus the Python result conversion, on every call.
bulk_load at s (edn_t per transaction, one writer)
| backend | (a) 1.9.0 s | datoms/s | (b) cached s | datoms/s |
|---|---|---|---|---|
| sqlite-ext | 92.2 | 10,683 | 67.6 | 14,566 |
| duckdb | 89.8 | 10,962 | 67.9 | 14,513 |
275 transactions of ~3,600 datoms each. How much of the 27% is the cache vs the O(1) open vs transactor changes since 1.9.0 was not separated (no (a') load was run).
concurrency_sweep (processes, each with its own host connection)
| backend | scale | clients | (a) ops/s | (a) p50 ms | (b) ops/s | (b) p50 ms | (b) p99 ms |
|---|---|---|---|---|---|---|---|
| sqlite-ext | s | 1 | 2.020 | 491.7 | 79.4 | 0.147 | 38.1 |
| sqlite-ext | s | 8 | 15.1 | 517.4 | 619.6 | 0.149 | 39.1 |
| sqlite-ext | s | 32 | 29.8 | 1,024.3 | 1,405.2 | 0.236 | 68.7 |
| sqlite-ext | s | 64 | 31.2 | 1,914.1 | 1,390.5 | 0.233 | 208.7 |
| sqlite-ext | s | 128 | 28.9 | 3,816.7 | 1,382.3 | 0.241 | 417.6 |
| sqlite-ext | m | 1 | ceiling (>20 s) | ceiling (>20 s) | 6.120 | 0.606 | 488.3 |
| sqlite-ext | m | 8 | 47.9 | 0.713 | 498.2 | ||
| sqlite-ext | m | 32 | 109.7 | 1.178 | 848.2 | ||
| sqlite-ext | m | 64 | 105.7 | 1.187 | 2,559.0 | ||
| sqlite-ext | m | 128 | 102.2 | 1.330 | 4,392.1 | ||
| duckdb | s | 1 | 2.060 | 475.7 | 74.8 | 0.694 | 39.4 |
| duckdb | s | 8 | 15.2 | 512.2 | 578.8 | 0.717 | 40.7 |
| duckdb | s | 32 | 24.9 | 1,159.9 | 1,282.1 | 1.305 | 79.3 |
| duckdb | s | 64 | 26.7 | 2,134.8 | 1,260.8 | 2.914 | 197.8 |
| duckdb | s | 128 | 27.5 | 4,060.1 | 1,205.7 | 9.997 | 385.6 |
| duckdb | m | 1 | 5.980 | 1.219 | 497.4 | ||
| duckdb | m | 8 | 46.5 | 1.588 | 509.5 | ||
| duckdb | m | 32 | 109.9 | 2.859 | 846.8 | ||
| duckdb | m | 64 | 104.0 | 9.844 | 2,500.8 | ||
| duckdb | m | 128 | 101.6 | 33.7 | 4,156.5 |
The mix alternates point_lookup, ref_traversal and pull, so throughput is
bounded by ref_traversal (38 ms at s, 480 ms at m). Throughput levels off
from 32 clients: 32 vCPUs. (a) ran pinned to 24 CPUs (cpu_affinity 0-23 in
baseline/env.txt), (b) to 32. That affects the plateau, not the 1-8 client
rows.
write_mixed (8 reader processes + 1 writer, 60 s)
| backend | scale | op | (a) ops/s | (a) p50 ms | (b) ops/s | (b) p50 ms |
|---|---|---|---|---|---|---|
| sqlite-ext | s | read x8 | 14.7 | 560.2 | 348.7 | 40.0 |
| sqlite-ext | s | write x1 | 1.950 | 518.3 | 295.7 | 3.376 |
| sqlite-ext | m | read x8 | 15.7 | 559.2 | ||
| sqlite-ext | m | write x1 | 216.8 | 3.828 | ||
| duckdb | s | read x8 | 14.9 | 546.6 | 329.2 | 41.1 |
| duckdb | s | write x1 | 1.930 | 517.6 | 246.7 | 4.011 |
| duckdb | m | read x8 | 17.2 | 560.2 | ||
| duckdb | m | write x1 | 191.2 | 4.494 |
Every write invalidates the other processes' cached stores, so a reader’s
next call after a write reopens its store: the staleness check doing its
job. The reader p50 is ref_traversal’s cost; the writer’s 3.4-4.5 ms p50 is
the transaction itself. The post-write
checks pass for both backends at both scales (checks.txt).
Notes
pull/duckdb/mshows as a REGRESSION incompare.py nocache-timings.csv timings.csv(1.19 → 1.80 ms). A rerun on the same copy gave 0.588 and 0.584 ms cached vs 1.190 and 1.167 ms uncached: the first run read cold pages of the freshly copied 2 GB file.compare.py baseline timings.csvlists 11 “regressions”: all are rows that wereceilingin (a) and have real numbers now (compare.py counts a changed op as MISSING).