Skip to content

perf: cut edit-loop compile overhead in the reuse store, hashing and walks - #1144

Merged
kvlonge merged 5 commits into
mainfrom
perf/edit-loop-overheads
Oct 18, 2026
Merged

kvlonge merged 5 commits into
mainfrom
perf/edit-loop-overheads

Conversation

@kvlonge

@kvlonge kvlonge commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Why

On a dense 10k-model project a one-model edit compile took ~17 s and the compile right after an edit 2.7 s, even with every per-model cache hitting. Much of that was fixed overhead: a 96 MB reuse store fully checksummed with software SHA-256 on every read and rewritten on every racy refresh, repeated tree walks, a duplicate project fingerprint, pathlib conversions and SQL-test planning holding the GIL (CHI-628, first step).

Changes

  • Project reuse store split into a small checked <slot>.store (inputs plus output identity) and <slot>.output (report), with per-section BLAKE3 checksums; checks read only the slot, refreshes rewrite only the slot. Directory project-reuse-v3; older stores miss once and are removed. Orphaned outputs are pruned.
  • Native store frames, SQL-test identities and the project fingerprint use BLAKE3 (internal cache keys only); SQL-test identities hash with the GIL released.
  • Recording stats the compiled tree once; the stale-file walk is skipped while every compiled folder's stamp and the managed file set are unchanged (racy folders, links and unreadable folders always walk).
  • Fewer repeated walks and fingerprints; _model_paths without pathlib.
  • Regenerated stale skill reference pages.

Verification

  • Dense 10k (instructions, alternating runs): edit 173 G to 145 G (16.9 s to 14.7 s), warm after edit 21.9 G to 1.8 G (2.7 s to 0.9 s), warm no change 1.3 s to 0.9 s.
  • Exit codes, JSON minus timings, stderr, compiled trees and reuse decisions identical to base on dense 1k and 10k across cold, warm, edit and post-edit runs; edit output equals a cold compile.
  • New unit and DuckDB E2E tests for damaged, missing, swapped and old-format entries, racy refresh, changes during compile, stale files under stamped and linked trees, and orphan pruning; make test passed.
  • Independent review and one follow-up; findings fixed.
  • make check-ci and compiler differential pass.

A reuse check read and SHA-256-verified the whole stored compile, including
the full report, on every check, and a hit that refreshed racy stamps read
and rewrote all of it again. On a dense 10,000-model project that is a
96 MB file: about 0.5 s per check and 1.3 s per refresh.

Each slot is now a small `<slot>.store` file holding the recorded inputs and
the identity of a `<slot>.output` file holding the report. Both use a new
section format with BLAKE3 checksums per section; readers skip sections they
do not know and a refresh carries them forward, so later releases can add
sections without another format break. A check decodes only the slot, the
output is read only once the inputs match, and a refresh rewrites only the
slot. A damaged, missing or mismatched output file is a miss. Stored
compiles move to `project-reuse-v3`; the v1 and v2 folders are retired.

Recording now stats the compiled tree once and re-stats only the artifacts
whose content it had to read, instead of walking the tree a second time.
Native store frames are checksummed with BLAKE3 (format version 2), so older
store files open empty once.
…piles

Several per-compile costs on a one-model edit scaled with the whole project
although every per-model cache hit:

- Background SQL test planning held the interpreter while it SHA-256 hashed
  an identity for every model and test, which blocked the main thread for
  about 0.6 s. Identities are now BLAKE3 digests of the same payload text,
  hashed in one batch without the interpreter, and the planning loops let
  other threads run every 64 items. The test artifact cache is version 5.
- The macro call store listed model paths through `PurePath.relative_to`
  (0.5 s for 10,000 models); a string prefix check now covers the common
  case, with the old path as the fallback. Saving the store skips the second
  project fingerprint when the compile recorded no new calls, and the
  fingerprint uses BLAKE3.
- Lint selection resolved every project SQL file through the filesystem one
  by one; folders shared by many files are now resolved once.
- The stale compiled-file walk visited every artifact on every compile. It
  now leaves a stamp of each compiled folder in the compile cache and is
  skipped while the managed file set is the same and no folder changed;
  folders changed within the racy window are never trusted.
The stale-file walk stamp judged the racy window against the time it was
recorded, after the walk. A walk longer than the window could stamp a folder
another process changed after the walk listed it, and later compiles would
then never remove that stray file. The window is now measured from before
the walk starts.

A folder that cannot be stamped, such as a linked `target/compiled` root, or
one the walk could not read, was silently left out of the stamp, so stray
files directly in it were never removed. Such a tree is now never stamped and
is walked on every compile. Folders the walk removed are no longer listed.

Pruning the reuse store also removes `.output` files with no slot once they
are ten minutes old, so a compile that stops between writing the output and
the slot no longer leaves the output behind for good.

The compile reference now describes `project-reuse-v3` with its `.store`
and `.output` files.
@kvlonge
kvlonge enabled auto-merge (squash) October 18, 2026 15:33
@kvlonge
kvlonge merged commit 5f52299 into main Oct 18, 2026
45 checks passed
@kvlonge
kvlonge deleted the perf/edit-loop-overheads branch October 18, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant