A fix for the per-page index rebuild is written and measured, on a branch. I have not opened a pull request; I would open one once triage confirms the direction.
Branch: https://github.com/bompus/t3code/tree/perf/history-page-index (one commit, 0e6192fc34, on upstream/main 33aa461478; 4 files, +106/−30).
Change. getThreadHistoryPage and getTimelinePage keep the rebuilt timeline index for the 4 most recently read threads and reuse it while the newest event of the thread and of every thread it forks from is unchanged. The check is the same revision query ThreadFind already uses for its scan cache; it moved into threadRevision.ts so ProjectionStore.ts does not statically import ThreadFind.ts (open #18220 loads that lazily). Threads with no events are never cached. This is the "per-thread timeline-index cache" direction from Diagnosis; I did not try the ordinal-window query.
Results (synthetic thread, as in the evidence above). Before and after were run back to back, 3 runs each, on upstream/main and on the branch.
| Items | Before | After | Change |
|---|---|---|---|
| 13,000, whole scrollback (72 pages) | 2,891 ms | 242 ms | 92% lower (11.9×) |
| 13,000, median page | 38.7 ms | 2.60 ms | 93% lower (14.9×) |
| 5,000, whole scrollback (28 pages) | 489 ms | 96 ms | 80% lower (5.1×) |
| 1,000, whole scrollback (5 pages) | 34 ms | 23 ms | 32% lower (11 ms) |
Limits: the thread is synthetic (assistant messages with a user message every 9 items, 500 characters of filler each), so this measures the index cost and not a mix of item types. The baseline's three runs spread by up to 5.4%. I have not measured it on a real thread or the event-loop gap beside the wall time. The first page after any write to the thread still pays the full rebuild, and a thread that is streaming invalidates its entry on every event, so the gain is for scrolling back through a thread that is not being written to.
Checks. ThreadFind.test.ts, ProjectionStore.test.ts, threadHistoryPaging.test.ts and ThreadSearch.test.ts pass (90 tests), including a new one that reads a timeline while the thread and its fork source gain items and fails if the cache never invalidates. Server typecheck, lint and format are clean on the changed files. The branch merges cleanly with #20104.
Two decisions for whoever reviews it.
- The cache holds 4 threads. That bounds the count, not the bytes; each entry keeps one small descriptor per item of the thread and its fork ancestors. Actual size is unmeasured.
ProjectionMaintenance.rebuildreplays projections without appending events, so a warm cache would not notice it. Nothing in the server calls it today. A comment says a future caller needs a fresh store; clearing the cache fromrebuildis the alternative.