LITE-028 DONE: index catch-up is O(history) — readState full-scans the lane per new commit

Now
DONE
Sev
HIGH
Ask
gritzko
Rep
///lite
Env
~/src/linux, ~1.3M commits; any click after a git commit stalls ~10 s

After one git commit in a linux-sized repo, the next lite verb (any link click) stalls ~10 seconds "doing index update" for a one-file change. The tree-diff pruning is INNOCENT — descend/diffMap (DOG-030) is O(changed) as designed. The cost is readState (index/index.js:365): whenever the tip is not marked, it makes ONE full cursor pass over the ENTIRE lane — every K_BLOB/K_CMMT/K_CPAR row ever written — to build next/byPB/top for ALL paths and done for ALL indexed commits, then indexes the single new commit. O(history) paid for O(1) of new work, every commit.

Input

Context

bringUp (index/index.js:621): O(1) mark check (markSet, tip marked = no-op), else readState full pass, collect walk (stops at done, fine), per-commit descend (pruned, fine), emit consuming st.next/byPB/top. The 2026-08-13 no-ceiling ruling made PRESENCE the walk boundary; the full arrival-state pass rode along and was invisible on be-sized repos — linux exposes it.

Goals

Post-commit catch-up cost proportional to the NEW work, exact same rows written.

Constraints

WIP

Design decisions

TODOs

Blockers and bummers

Outcome

readState's full pass is gone: state() + hasDone() + loadPath() read the lane BY KEY, emit loads a path on first use, an empty lane is settled by one row read. ///lite wt work/LITE-028, staged, not committed.

Measured with the counting shim (test/index/lazy.js), runtime quickjab, rows READ off the lane / wall ms, synthetic fixtures in ~/tmp/lite028:

case lane rows before after
1 new commit, 20k-commit lane 100,003 100,003 / 167 79 / 9
1 new commit, 200k-commit lane 1,000,003 1,000,003 /1127 206 / 13
200-commit backlog, 100k-row lane 100,010 100,010 / 161 15,207/ 75
5000-commit backlog, 151k-row lane 151,008 151,008 /1094 95,607/1195
from-scratch derive, 30,201 commits 0 0 /3015 0 /2687

So the catch-up is 87× cheaper at 200k commits and gets cheaper the bigger the history is (the 1.3M-commit ~/src/linux extrapolates to 6.5M rows read, the reported 10 s). What the catch-up DOES read is the touched file's own rev chain, not the repo's history.

Backlog fallback, both costs (30,201-commit derive; 5000-commit backlog on a 125k-row lane): with a 4096-keyed-seek threshold that buys the full pass, 2813 ms and 1399 ms / 130,109 reads; unconditional seeks, 2687 ms and 1153 ms / 80,609 reads. The threshold loses on both and its cost is UNBOUNDED on a big lane (a 4096-commit pull on linux would read all 6.5M rows again), so it is not in the code. Overturn if wanted — it is ~15 lines.

Row identity, dumped key/val and cmp-ed against the pre-fix code over the same history: 176,007-row lane after a 5000-commit backlog and 1,000,007-row lane after a one-commit catch-up, both BYTE-IDENTICAL. Interrupted run at scale: _faultMid at commit 50 of 100 over a 176k-row lane, then resumed — 51 commits re-walked, 50 revs minted (the stranded commit re-derived to NOTHING through the lazy top guard) and the lane byte-identical to the unfaulted control.

Tests: lite/index 123 -> 129 checks, all green (leg 3 + lazy.js: the repro, the file-chain bound, dense rev chains, one rev per path per commit). The new leg is RED on the pristine tree (reads 2503/2507 and 5007/5009 = the whole lane) and GREEN here (41 and 82). Full suite failure counts identical to a pristine clone of #96b7909d: blob 12/13, cat 16/35, list 17/78, pager 1/33, tree 25/47 pre-existing, everything else PASS, resume leg 16/16 unamended.

Suggested commit: LITE-028: index catch-up reads by key, not the whole lane