Repository navigation
grust-turso: durable group commit, parallel MVCC loads, FK-off loads, and a WAL/MVCC head-to-head - #6
Conversation
Add grust_datafusion::cypher::RoutedGraph: one graph captured as a typed index and an Arrow snapshot, one bounded-read admission per query, and DataFusion execution for single-node scan plans whose every physical operator charges candidate work and intermediate bytes against the caller's ReadQueryPolicy. Joins, unsupported shapes, graphs under 3,000 nodes and plans over a limit stay on the reference executor through the new run_prepared_read_query_indexed, which keeps the original deadline. Split large single-batch Arrow tables into zero-copy partitions so no round-robin repartition charges each queued slice its shared parent buffers; that intermittently exhausted a 256 MiB pool at one million nodes. Add LadybugConfig::max_db_bytes and cap unit-test databases at 16 GiB; parallel tests exhausted x86-64 address space with 8 TiB reservations. Workspace gate on quegee: 1,675 passed, 0 failed, 49 ignored; Clippy clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WZ7SjQFbe58VDZs6xR3nCo
…RUNCATE Profiling an MVCC bulk load (pprof, bulk_load example, 1M edges over 50k nodes) put 60% of the time in the upserts' index work in the MVCC store and 31% in the automatic checkpoints that write the same rows into the B-tree. The edge table carried three indexes: the primary key (from_id, label, to_id, identity_key), to_idx, and from_idx on from_id alone. Turso's planner never reads from_idx: source lookups and every traversal hop already use the primary key's from_id prefix, with or without from_idx present (EXPLAIN QUERY PLAN). The index only cost a write per edge. Turso's dialect now reports edge_source_index() = false (a new GraphSqlDialect method, default true, so Postgres keeps its index), the universal bootstrap omits the index for it, and bootstrap drops it from existing Turso stores. Query plans and results are unchanged. MVCC loads now end in PRAGMA wal_checkpoint(TRUNCATE) inside put_graph, as WAL loads already did, so a load returns with its rows in the database file and the in-memory store drained. Under the default blocking automatic checkpoints this costs nothing measurable. Optional mimalloc feature: Turso's own default allocator (turso/mimalloc), which installs it as the binary's global allocator; MVCC loads ran about 9% faster with it. Off by default, because it changes the allocator of every backend in a binary. 1M edges, 4-core host: MVCC 12.7k -> 15.9k edges/s, WAL 43.6k -> 51.5k. New tests: an MVCC bulk load with checkpoints mid-load stores exactly the rows of the WAL load (identity_key included), survives a reopen and takes concurrent BEGIN CONCURRENT writers afterwards; bootstrap omits the index and drops a legacy one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MVCC connections open with turso::Builder:: experimental_mvcc_passive_checkpoint(true), so the automatic checkpoints taken every mvcc_checkpoint_threshold bytes of logical log (4,120,000 by default in Turso 0.7.2) run PASSIVE instead of TRUNCATE and do not block the store's other readers and writers. put_graph still ends in one TRUNCATE checkpoint, so a load returns durable in the database file. WAL connections are unchanged; the option only affects MVCC checkpoints. Measured cost on a single-writer bulk load (bulk_load example, 4-core host, no edge-source index): 5M edges 14.4k edges/s blocking -> 8.7k passive, peak RSS 0.9 GB -> 2.8 GB. PASSIVE reclaims emptied version chains lazily, so the in-memory store and each checkpoint's scan grow during the load, and each put_graph pays its deferred work in the final TRUNCATE (~11 s per 1M-edge call). The option is experimental in Turso 0.7.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
put_graph on an MVCC store sets PRAGMA mvcc_checkpoint_threshold = -1 for the load, so no automatic checkpoint writes the rows committed so far into the B-tree mid-load (profiled at about a third of an MVCC load). The load still ends in one TRUNCATE checkpoint, and the store's previous threshold is restored whether or not the load succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Turning automatic checkpoints off for an MVCC put_graph kept every row version of the load resident: 13.4 GB at 5M edges against 11.4 GB for Turso's 4 MB default, heading past a 32 GB host well before com-Orkut. Loads now raise the threshold to MVCC_LOAD_CHECKPOINT_BYTES (64 MiB) and restore the store's own threshold afterwards. bulk_load example on quegee, MVCC: 1M edges 11.8k -> 15.4k edges/s (2.31 -> 2.35 GB peak); 5M edges 995 s -> 423 s, 11.4 GB both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…IVE" This reverts commit 3e8b13a.
…ver" This reverts commit 0291906.
This reverts commit a26fdff.
…example connect_shared opens another connection on a store's own turso::Database instead of building a second database object on the same path. The concurrent_writes example reproduces the adversarial-graph A4 shape (N writers, put_node then put_edge onto one hub) with separate or shared handles, in WAL or MVCC mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Under Turso's default synchronous = FULL every MVCC commit fsyncs the logical log while holding the engine's global commit lock, with no group commit. An MVCC put_graph now runs at NORMAL, which skips the per-commit fsync, and restores the connection's previous setting afterwards; its closing TRUNCATE checkpoint still fsyncs, so the load is durable when put_graph returns. TursoGraphStore::set_synchronous sets PRAGMA synchronous on a handle's connection for callers that choose NORMAL or OFF for ordinary writes (a crash can then lose the commits since the last checkpoint). The concurrent_writes example gains --sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Under synchronous = FULL every MVCC commit fsyncs the logical log while holding Turso's global commit lock, so concurrent writers pay one fsync each, one after another (A4-shaped load on eigen's disk: 16 writers x 200 node+edge writes, 40.8 s, 78 writes/s; the same at NORMAL: 0.12 s). TursoGraphStore::with_group_commit starts a committer task on its own connection. Single-statement writes from the store and from every handle opened with connect_shared go to it; it commits whatever is waiting as one BEGIN CONCURRENT ... COMMIT (one fsync per batch, up to 256 statements) and answers each caller only after that commit. A batch that fails is replayed one statement at a time with the usual conflict retry, so a failing write fails only its own caller. WAL stores and bulk loads are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
… once MVCC keeps every index entry of every row version in an in-memory skip list as well as in the B-tree; a perf profile of an MVCC bulk load put 16% of its time in that skip list's key comparisons (SortableIndexKey:: compare), which WAL loads do not pay. When an MVCC put_graph starts on an empty edge (node) table, it drops <prefix>_edges_to_idx (<prefix>_nodes_label_idx) for the load and rebuilds it with one CREATE INDEX at the end, whether or not the load succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…ry index once" Measured on lakecat's disk, web-Google MVCC bulk_load: 349.6 s with the secondary indexes maintained during the load, 437.9 s with them dropped and rebuilt by CREATE INDEX at the end. Building the index afterwards under MVCC costs more than maintaining it row by row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
An MVCC load keeps every index entry of every row version in an in-memory skip list as well as the B-tree (16% of an MVCC load's time in SortableIndexKey::compare alone), so it runs several times slower than a WAL load. Turso 0.7.2 switches a live database between journal modes (probe tests: a WAL-loaded file reopens as MVCC; a live MVCC store with a group committer and a shared handle switches to WAL and back, with rows and concurrent writers intact). TursoGraphStore::set_bulk_load_via_wal(true) makes an MVCC store's put_graph switch to WAL, load in one transaction, checkpoint, and switch back to MVCC, also when the load fails. Journal mode is database-wide, so callers must not write concurrently during such a put_graph; a bulk load before serving is the intended use. The TursoJournalMode doc that said a WAL database cannot be converted is corrected. bulk_load gains --wal-load on|off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
MVCC admits concurrent writers where WAL admits one, and the dominant cost of an MVCC load (the in-memory index work per row version) is done inside each transaction before its commit. set_mvcc_load_parallelism(n) makes an MVCC put_graph load its nodes, then its edges, as n contiguous slices on n shared connections and tasks (BEGIN CONCURRENT groups at synchronous = NORMAL, as the single-writer path runs); the first error is returned after every writer finishes. 1, the default, is the single-writer path. bulk_load gains --load-threads and runs on a multi-threaded runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Free-running parallel writers kept every row version resident, because an automatic checkpoint cannot run while another writer's transaction is open: web-Google (5.1 M edges) on eigen peaked at 14.6-15.3 GB with 2-8 writers against 1.7 GB for one. Writers now advance in rounds of MVCC_PARALLEL_ROUND_GROUPS (5) commit groups each, and after every round, with nothing in flight, one TRUNCATE checkpoint folds it into the file. tests/parallel_load.rs checks that a 4-writer load stores every row once. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Each connection inherits PAGE_CACHE_KIB (1 GiB), so eight load writers budgeted 8 GiB of page cache alone; a writer streams rows in and re-reads almost nothing, so its connection now takes MVCC_LOAD_WRITER_CACHE_KIB (128 MiB). Turso also runs an inline garbage-collection pass on the commit path every mvcc_gc_threshold new row versions; a bulk load leaves nothing worth collecting and the round checkpoints drain the versions, so the load sets the threshold to -1 and restores the store's own value afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…ause Every load row went in as an upsert, whose ON CONFLICT probes the index for a conflict that cannot exist on a table that was empty when the load began; index seeks are 22% of a WAL load's profile and 8% of an MVCC load's. put_graph now checks each side once (SELECT EXISTS, not COUNT), and while that load runs its batches insert without the clause. A batch that collides anyway -- duplicate keys inside one load -- is replayed as an upsert, so the stored rows are unchanged; the flag is cleared on every exit, and shared with the parallel writers' handles. tests/fresh_load_duplicates.rs loads 300 node keys and 299 edge keys twice in one call, in both journal modes, and checks one row per key with the last write winning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
set_mvcc_load_round_groups overrides MVCC_PARALLEL_ROUND_GROUPS (0 keeps it), so a sweep can trade checkpoint count against resident row versions without a rebuild. bulk_load gains --round-groups and --batch (the store's batch_size, previously hard-coded at 500 in the example). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Alternated against the upsert path on lakecat (web-Google, on disk): WAL 133.9 s and 134.9 s with the plain INSERT against 128.0 s and 130.2 s with the upsert, and MVCC with 4 writers 177.4 s against 179.4 s -- about 4% slower on WAL and a wash on MVCC. A plain INSERT still pays the same B-tree insert, so skipping the conflict clause buys nothing, and the 22% of a WAL load's samples in index seeks is that insert rather than the upsert's probe. Removed with it: the loading_empty_tables flag shared across handles, the put_graph_rows wrapper, both prepared_insert_* builders, is_key_collision and table_has_rows, whose only callers were the removed check. tests/fresh_load_duplicates.rs stays: one row per key on a fresh load with duplicate keys is worth asserting on whichever path runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
|
Carried over from #5 (closed as superseded; 1. Harness confirmation on lakecat (4 cores, 15 GB), adversarial-graph
The interim pins with PASSIVE checkpoints did not finish web-Google within 1,800 s (eigen, lakecat), and cit-Patents was OOM-killed at a 13 GiB cap. 2. turso-wal through the full strain ladder on grust (2xlarge class, 32 GB; harness pin a26fdff, whose WAL code is identical to this branch's). Every graph passed LOAD, A1, A2, A4, A7 and A12; A3 is unsupported as before.
The previously published strain summary had Turso WAL passing every strict gate only up to GAP-road. 3. Full workspace gate on This corrects the Verification section of this PR. That section reports So the accurate statement is: the workspace gate passes in full once lbug is 🤖 Generated with Claude Code |
Whether a dangling edge was accepted depended on the writer count: WAL and single-writer MVCC loads ran on the bootstrap connection with PRAGMA foreign_keys = ON and rejected an edge to an unstored node, while parallel MVCC writers (connect_shared handles, which never set the pragma) accepted it. Same store, same put_graph, different result. Load with foreign keys off on the bootstrap connection for the duration of put_graph, restored afterwards whether or not the load succeeds, so every path agrees and matches the Memory reference, where a vertex can exist only as an edge endpoint. The schema's REFERENCES nodes(id) ON DELETE CASCADE stays in force for every other write, and cascade fires on the connection that deletes, so serving semantics are unchanged. WAL and single-writer loads now also skip the two endpoint probes per edge that parallel loads already skipped, so their rates rise and the WAL-vs-MVCC comparison stops being biased. Tests: tests/dangling_edges.rs loads a dangling edge through WAL, one MVCC writer and four MVCC writers, reads it back identically, and then checks a single put_edge to a missing node is still refused. 80 grust-turso tests pass against turso 0.7.2 and against turso main (0.8.0-pre.11). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…tion A Busy or conflict on BEGIN CONCURRENT means another writer holds the commit or checkpoint lock; retrying at once spins against it. All three retry loops now yield to the runtime once per attempt so far before the next attempt. The workspace tokio has no timer feature, so this is a yield rather than a sleep. Loads measured zero such retries across eight full-graph runs, so this removes a latent hot loop rather than changing throughput. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
PRAGMA mvcc_group_commit exists on Turso 0.8 (upstream main), store-wide and on by default; 0.7.2 has no such pragma and the call returns an error. Same host, same binary, 16 writers x 200 hot-node writes at synchronous=FULL: on 0.8 the engine's group commit alone takes 2.62 s and adding with_group_commit on top costs ~19% (3.2 s); on 0.7.2 with_group_commit is the only group commit and is worth ~6.6x (21.1 s -> 3.2 s). Bulk loads are unaffected by the pragma either way. Exposed so the client-side committer can be switched off, and its default flipped, when the pinned engine moves to 0.8. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…facade The facade forwarded `turso` and `turso-sync` but not `grust-turso/mimalloc`, so a crate depending on `grust-graph` could not enable Turso's allocator without depending on the adapter directly. `turso-mimalloc` mirrors `turso-sync`. Off by default: a global allocator is the application's decision. Measured on 4-writer MVCC bulk loads of web-Google, same host, alternating pairs: +14% on Turso 0.8 (31.3k -> 35.5k edges/s) and +16% on 0.7.2 (35.7k -> 41.6k); +9-16% on WAL loads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Add a book chapter covering the Turso adapter on this branch: WAL and MVCC contracts and live conversion, parallel MVCC loads with round checkpoints, the fill through WAL, foreign keys off on every load path, durable group commit and its scoping to Turso 0.7.2 versus the engine group commit on 0.8, the turso-mimalloc feature, the strain-harness results against WAL and Neo4j on one host, the Turso main findings (WAL +22%, MVCC loads -15 to -26%, reads faster, upstream #8385), the methodology, and the limitations. Point the manuscript's Turso section and the compact-snapshots chapter at the chapter, and replace the stale note that MVCC is a fresh-database-only mode. Add a "Turso under strain" section to the Brine post, framed as branch work measured against the 0.20.0 crates and not part of them, with links to the pull request and the strain results. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
The MVCC rows in the Neo4j and WAL tables ran eight writers at synchronous = NORMAL, so their hot-node-write times are not comparable with Neo4j's durable commits; say so, and state the durable same-host result the strain page counts instead (Neo4j wins every pair against MVCC). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Neo4j's A7 row reads "backend does not implement GraphCommitStore", a Grust trait the harness's own Cypher-over-Bolt driver never implemented. Calling that a Turso capability win was unfair to Neo4j; say what it is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…ns it off Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…20.4 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
…p the read-back The registered-Arrow COPY FROM (MATCH …) path is superlinear in the engine on relationship tables: 4,100 edges/s at 500 k, unfinished after two hours at 5 M (cit-Patents never loaded). The engine's file COPY takes the same rows, STRING keys and JSON props included, at 1.3 M edges/s and 2.2 M nodes/s, so nodes, edges and the metadata index are written through a temporary CSV (Arrow's writer, RFC 4180 quoting, ESCAPE='"'). A count query skips the read-back of existing ids and pairs when the target table is empty. Upsert semantics unchanged; the round-trip test covers commas, quotes, newlines, backslashes and nested JSON, then a second put over the same ids. lbug 0.20.4 (crates.io, 2026-09-10; no Rust-API change from 0.20.2). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ
grust-ladybug: bulk loads COPY from a temporary CSV; lbug 0.20.4
Documentation only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
grust-turso: durable group commit, parallel MVCC loads, FK-off loads, and a WAL/MVCC head-to-head
Pinned engine: Turso 0.7.2 (crates.io
max_stable_version; every 0.8.0 is apre-release). Several claims below are version-scoped against upstream
main(0.8.0-pre.11, 1,859 commits ahead), measured the same day. Where a number is
from
mainit says so.What this does
with_group_commit) batches concurrentsingle-statement writes into one transaction: one fsync per batch, every
acknowledged write durable.
GROUP_COMMIT_MAX_STATEMENTS = 256.Worth 6.6x on 0.7.2; costs 19% on Turso 0.8, which has engine-level
group commit on by default -- see "Version scoping".
set_mvcc_load_parallelism,put_mvcc_parallel):N writers under
BEGIN CONCURRENTon disjoint slices, checkpointing betweenrounds (
MVCC_PARALLEL_ROUND_GROUPS = 5) so peak memory is bounded.3f1ed19). Before this,WAL and single-writer MVCC loads enforced
REFERENCES nodes(id)and parallelMVCC writers (from
connect_shared) did not, so whether a dangling edge wasaccepted depended on the writer count. Now every path matches the Memory
reference (an endpoint may exist only as an edge end);
ON DELETE CASCADEstill fires on the deleting connection, and a single
put_edgeto amissing node is still refused.
tests/dangling_edges.rschecks all threepaths and the restored strictness. This also roughly doubled WAL loads
(below).
connect_shared(),TursoSynchronous/set_synchronous(),set_mvcc_group_commit()(Turso >= 0.8 pragma, errors on 0.7.2; lets theclient committer be switched off when the pin moves), a Busy backoff in
all three concurrent-retry loops (
acac24e; loads measured zero retries ineight full-graph runs, so this removes a latent hot loop, not throughput),
query_scalar_i64(), and an opt-in WAL load path (set_bulk_load_via_wal)with live WAL<->MVCC conversion, test-verified on 0.7.2 and on
main.Results (Turso 0.7.2, harness pin
f6d3391-- see Provenance)A4 = 16 writers x 200 edges to one hot node,
synchronous=FULL.All cells
pass;duplicate_durable_mutation = 0; quegee is a c5n.4xlarge(no CPU steal); grust/eigen are t2.2xlarge and these particular cells ran at
<0.1% steal (checked in
host_steal_us).The headline is not the load rate. Same host (quegee), same binary, the
three largest graphs:
WAL loads 1.5-1.7x faster at roughly half the memory, and then drops 96-98%
of concurrent hot-node writes. MVCC with group commit accepts every one. NB:
these WAL rows enforced foreign keys and the parallel-MVCC rows did not
(item 3), so the load ratio understates WAL. Measured after
3f1ed19(eigen,web-Google, 0 s steal, alternating): FK-off alone lifts WAL loads ~60%
(0.7.2 42.9k -> 70.3k / 69.7k e/s;
main80.7k / 82.0k), and mimalloc adds9-16% on top (0.7.2 76.7k;
main93.4k). Parallel MVCC is unchanged by theFK commit, since its writers were already unchecked.
Against the pre-change single-writer lane, same host (grust, 4 writers),
parallel MVCC loads are 1.9-9.5x faster: cit-Patents 5,526 -> 26,661 e/s,
sx-stackoverflow 2,929 -> 13,464, GAP-road 2,928 -> 27,708, soc-Pokec
4,698 -> 26,385, web-Google 16,013 -> 30,985. Round checkpoints cut peak load
memory from ~15 GB to 3-7 GB. Group commit took A4 from ~22 s to 3.9-4.2 s.
Neo4j, same host (quegee), for scale: com-Orkut -- Neo4j 16,800 e/s /
4.9 GB / A4 2.90 s; MVCC matches its load (-4.5%) and beats its A4 (1.82 s).
soc-LiveJournal1 -- Neo4j 28,062 e/s / 6.7 GB / A4 2.90 s; Neo4j loads 1.42x
faster than MVCC and wins A4 (4.81 s). Turso wins A1/A2 (2.3-3.1x) and passes
A7 (Neo4j: unsupported) on both; Neo4j is 3-4.6x leaner in memory.
Version scoping: what changes on Turso
main(0.8.0-pre.11)Engine SHA asserted in every probe and CPU steal recorded per run. The
hot-node, WAL, mimalloc and via-WAL numbers are alternating A/B pairs on one
host with the same Grust head. The at-scale MVCC rows are sequential lanes on
quegee (c5n.4xlarge, no burst credits, <=0.5 s steal per cell),
mainatGrust
e5d7b4eagainst 0.7.2 at the harness pinf6d3391-- the loader isunchanged between those for parallel MVCC writers.
Hot-node writes (16x200, FULL): 0.7.2 with our committer 3.2 s, without
21.1 s;
mainwith our committer 3.2 s, without 2.62 s.mainhasengine group commit (
PRAGMA mvcc_group_commit, default on); our committerbecomes a second layer that fights it. Keep
with_group_commiton 0.7.2;switch it off (and flip its default) when the pin moves to 0.8.
MVCC loads:
mainis ~5% slower at 1 writer and ~15% slower at 4writers than 0.7.2 on web-Google (5.1 M edges), ~26% slower on
cit-Patents (16.5 M), and on quegee (same host, 4 writers, NORMAL, glibc,
<=0.5 s steal per cell):
maine/sLoads regressed; reads (A1 fan-out, A2 deep paths) are faster on
mainonevery graph. Peak RSS runs 16-21% higher on
mainat these sizes (nodifference at 5.1 M edges). A4 at
synchronous=NORMALis not cited: itspans 1.4-5.2 s on both engines with no consistent direction (it measures
conflict-retry luck, not the commit path); the FULL-sync A/B above is the
write-contention comparison. The load regression is not group commit, not
checkpointing, not the allocator (each tested);
perfputs it intypes::cmp_in_column+read_value_serial_typeunderSortableIndexKey::compare. Upstream PR #8385 rewrites exactly that path:measured against its own base it is +29-30% on this load, and under it the
~31% index-key share collapses to a single 12.2% symbol.
WAL loads:
mainis +22% faster (B-tree path improved).mimalloc: +14% on
main, +16% on 0.7.2 for parallel MVCC loads,+9-16% on WAL loads; off in every harness number above. Now reachable from
the facade as
grust-graph/turso-mimalloc(abea518, mirrorsturso-sync). Off by default -- a global allocator is the application'sdecision -- and recommended on in any deployment that owns its binary.
Not yet used by the adversarial-graph harness: its rows carry no allocator
tag, so a mimalloc build would produce cells indistinguishable from the
glibc cells above; that needs a
turso_alloctag first.Fastest fill of an MVCC store on
main:set_bulk_load_via_wal--94k edges/s on web-Google and 79.5k on cit-Patents (eigen, FK off,
mimalloc), identical to plain WAL, 2.65x and 3.1x parallel MVCC
respectively, and +19-22% over 0.7.2 on the same path; then serve with
engine group commit.
Ideas measured and rejected
383ca18, reverted9fdbdb9).e5d7b4e.identity_keyschema probe: a real 2-4%, not worth a stateful dialect.2.35x win only against a branch head that already carried it; measured
against the harness pin it is slower; reverted
5dff9ac).mainreworkedpassive checkpoints; not re-measured there.
The Amdahl fit over 1/2/4/8 writers on 0.7.2 gives ~35% serialized. That
fraction is in the commit path but is not solely the global
pager_commit_lock: 0.7.2's per-insert rowid write lock and spinning waitersare in it too, and both are gone on
main.Provenance
The harness rows above were produced at Grust pin
f6d3391, four commitsbehind the original PR head
e5d7b4e(91f6e3fmeasured flat,48b4252later reverted,
618caa0tunables). The FK, backoff, pragma and facade-featurecommits (
3f1ed19,acac24e,c8ec310,abea518) postdate them; theireffect is measured in "Version scoping" and not yet in the harness rows.
abea518is manifest-only (a forwarded feature) and does not change any testoutcome; the gate below is at
c8ec310.Verification
cargo fmt --all -- --check: clean.cargo clippy -p grust-turso --all-targets -- -D warningsandcargo clippy --workspace --exclude grust-ladybug --all-targets -- -D warningsat
c8ec310: clean.cargo test --workspace --exclude grust-ladybug --no-fail-fastatc8ec310:1,551 passed, 0 failed, 47 ignored, 105 suites (1,550 at
e5d7b4eplustests/dangling_edges.rs).grust-tursoalone: 80 passed, 0 failed, on Turso 0.7.2 and onmain.5dff9acwith the lbug git pin (ladybug-rust47bfd12e, PR Accounting: child executions, per-query views of cached projections, and every work charge inlined again #33)and
LBUG_LOCALIZE_BUNDLED_SYMBOLS=1:--workspace --all-features1,677passed / 0 failed / 49 ignored, Clippy clean.
grust-ladybugis excluded only because an unpinned checkout cannot link it:~20
duplicate symbol: ZSTD_*betweenliblbug.aandlibzstd_sys(viaarrow-ipc 58.3-> lance -> lancedb), pre-existing onmain(reproduced atmerge-base
a134e05) and already solved by the lbug pin above.Also on this branch: LadybugDB bulk loads (PR #7, merged)
PR #7 landed here as a stacked PR:
grust-ladybugbulk writes now go through a temporary CSV and the engine'sCOPY t FROM 'file'instead ofCOPY … FROM (MATCH …)over a registered Arrow table, which is superlinear in the engine on relationship tables (4.1 k edges/s at 500 k, unfinished after two hours at 5 M — why cit-Patents never loaded). Same schema, 1.28 M edges/s; an empty target table skips the read-back of existing keys; upsert semantics unchanged;lbug0.20.4. Details and measurements in #7.🤖 Generated with Claude Code
https://claude.ai/code/session_01Sak2FQcdgs5pkL2ruKrSUZ