Repository navigation
perf(index): a fresh mid-size index stops syncing the database inside its resolution commits - #518
Merged
Conversation
…the pool starts mid-run A fresh index below 150,000 pending references resolves in memory-journal mode. When the resolver pool starts part-way through, ensureWalForPool switches the database to WAL, but nothing turned off SQLite's default 1000-page autocheckpoint for that phase, so checkpoints and their fsyncs ran inside the batch-persist commits on the main thread. Arm the autocheckpoint deferral and the WAL valve for the whole resolution phase on the fast-init path, not only when the pool starts up front. Both are inert until a WAL exists, so a run that stays sequential is unchanged; a mid-run switch now behaves like the up-front pool path: off-thread passive checkpoints bound the WAL, maintenance folds it, and the interval is restored afterwards.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Oct 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On a fresh index of a mid-size project, where the resolver pool starts part-way through, persisting resolved batches now takes 21–39% less time: ktor 3.66 s to 2.25 s, firefly-iii 1.60 s to 1.14 s (full index, Node 24, median of 3 alternating runs per build, identical graphs). During resolution the main thread syncs the disk 6 times instead of 145 on firefly-iii and 7 times instead of 204 on Exposed. Whole-index time, CPU and peak memory show no clear change on any of the six projects measured.
The chart's "no clear change" line is 10.3% of the baseline: the largest spread between before and after runs on any persist row. Each row is also judged in the tables below against its own project's spread.
What was wrong
A fresh index (fast init) starts in memory-journal mode. Below 150,000 pending references, the adaptive pool from #506 can start part-way through resolution. When it does,
ensureWalForPool(src/codegraph.ts) switches the database to write-ahead logging so the pool's readers can share it. Nothing turned SQLite's default auto-checkpoint off at that point, and no write-ahead-log valve was armed. So from then on, every 1,000 pages of log made a batchCOMMITcopy pages into the database file and sync it while the main thread waited. At or above the 150,000 gate this does not happen: the index switches to write-ahead logging before resolution, with auto-checkpoint at 0 and the valve running.The change
In
indexAll(src/codegraph.ts), a fast-init index now always sets auto-checkpoint to 0 and starts theWalCheckpointValvefor the resolution phase. Before, it did this only above the 150,000 gate. While the database is still in memory-journal mode both have no effect. If the pool later switches to write-ahead logging, it inherits them. After that:finallyblock restores the previous auto-checkpoint value and journal mode. Maintenance runs the final checkpoint, also as before.foldWalForSnapshotis unchanged. The pool's reader snapshot still folds the log before it opens.The source change is 16 lines added and 8 removed in
src/codegraph.ts, including a comment inensureWalForPool, plus one 37-line test.Trade-off: the write-ahead log now grows until the valve folds it. Peak log size per run was 492 MiB on ktor (before: 10 MiB), 328 MiB on Exposed, 176 MiB on firefly-iii, 160 MiB on paperless-ngx and 70 MiB on bloc. pretix was 178 MiB both before and after. The valve caps the file at 1 GiB.
Crash safety: a fast-init index already counts as disposable until it completes. Its
index_statestaysindexing, and an interrupted run re-indexes from scratch. With write-ahead logging andsynchronous = NORMAL, a process crash loses no committed batch. A power loss can drop the most recent batches but leaves a consistent database. A large log left behind by a crash is folded at the next open, which already happens above 64 MB.Results
Before =
fork/consolidatedd161ef74; after = this change. The after build was measured twice per round: the change itself, plus an identical copy of it as a control arm. A change counts only when the medians differ by more than the floor and the ranges do not overlap; otherwise it is "no clear change". All runs used Node 24.22.0, 3 rounds with alternating builds, and were admitted only on a quiet host.Persist time and main-thread commits come from instrumented runs (SQLite statement timing, 3 per build). For these, the floor is the larger of the before and after ranges.
COMMITtime / writes, before → afterThe plain runs give whole-index time, CPU and peak memory (3 per build). The floor is the spread across all six runs of the change and its control copy.
Wall time does not move because the saving is split up and absorbed elsewhere. The resolution phase gets shorter: 270 ms on firefly-iii, 264 ms on paperless-ngx and 86 ms on bloc. The ktor and Exposed reductions (327 and 185 ms) are inside their floors. But maintenance gets 42–133 ms longer, because the final fold of a larger log now happens there. Resolution and maintenance together come out ahead by 160 ms on firefly-iii (floor 92 ms) and 43 ms on bloc (floor 30 ms); on the other projects the combined change is inside the floor. On ktor and Exposed the pool's own resolve time, not the commits, appears to set the pace.
A syscall trace of one run per project, compared with the same trace of the before build, shows:
Output identity: on every accepted run, both copies of the change produce the same graph hash as the before build on the same project: ktor
57e9c5136b5e, Exposedb332758c901b, firefly-iii496d20b7d5b6, paperless-ngx2cca11fd5dbc, blocc49b5b74c4a9, pretixbc3469fcdcfc. Plain runs: 72 accepted, 1 rejected and re-run (memory pressure from other processes). Instrumented runs: 36 accepted, none rejected.Checks
All ran on the head of this branch after rebasing onto
87e304a5:__tests__/wal-deferral.test.ts, new case "fast init: a resolver pool that boots mid-resolution". It starts the pool's write-ahead-log switch inside resolution and records the database state there. It fails on the base (auto-checkpoint 1000, no valve) and passes with the change (auto-checkpoint 0, valve armed; afterwards the journal mode iswaland auto-checkpoint is back at 1000).npx tsc --noEmit: exit 0.npm run build:kernel, includingcargo clippy --release --lib -- -D warnings: exit 0.npx vitest run __tests__/kernel-golden-dumps.test.ts: 8 passed; no.dumpfile changed.npm run check:agent-docs: exit 0.npx vitest run: 746 files, 9,263 passed, 40 skipped, exit 0.The resolution precision gate was not run. Resolution logic is unchanged, and the graph hashes are identical on all six projects.
The benchmark compared builds of
d161ef74. Since then the base has gained #517, a kernel memory change for Kotlin, and #516, a review-bot setting. This change does not touch either, but the numbers above were not re-measured on the rebased head.README and docs
None of them moves, since whole-index time, CPU and memory show no clear change. The README is unchanged.
docs/benchmarks/adaptive-pool-engage-2026-10-15.md: its Limits section said a mid-run pool start has no checkpoint valve. A sentence now points to this change and its log size.Not covered
Summary by cubic
On a fresh mid-size index where the resolver pool boots part-way through, the switch to write-ahead logging brought SQLite's default 1000-page auto-checkpoint into the batch commits, so pages were copied and synced on the main thread during resolution. A fresh index now keeps auto-checkpointing off for the whole resolution phase and arms the write-ahead-log valve up front; both settings are inert until a WAL exists, so sequential runs are unchanged.
Persisting resolved batches is 21–39% faster on the five projects that take this path (ktor 3.66 s to 2.25 s, firefly-iii 1.60 s to 1.14 s), and main-thread syncs during resolution drop from 145 to 6 on firefly-iii and 204 to 7 on Exposed. Whole-index time, CPU and peak memory show no clear change, and graphs are identical.
Trade-offs
Written for commit 1f8993a. Summary will update on new commits.