perf(tui): fix horrendous scrolling on very large sessions#3710
Merged
Conversation
Assisted-By: Claude
Grow the renderedItems LRU to cover all items before full rebuilds so a cache smaller than the item count can't evict every entry during the sequential scan. Cache reasoning blocks once animations settle, delete cache entries unconditionally in invalidateItem (cacheability is state-dependent), and drop the unused renderedItem.view field. EnsureCapacity is added to pkg/lrucache to support grow-only resizing. Assisted-By: Claude <[email protected]>
…nges Key the cache on a cheap session fingerprint (width + item count + tokens/cost) so usage streamed in while the dialog is open refreshes the view; clear it on ThemeChangedMsg since cached lines carry styled ANSI output. Adds unit coverage for lrucache.EnsureCapacity and a live-refresh regression test. Assisted-By: Claude <[email protected]>
gtardif
approved these changes
Jul 17, 2026
docker-agent
left a comment
Contributor
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The performance fixes in this PR are well-structured and correct. The EnsureCapacity method on the LRU cache is a clean, safe addition. The cost dialog cache fingerprint logic correctly captures all the values that affect rendered output (width, item count, tokens, cost). The invalidateItem unconditional delete and the !NeedsTick() gate for reasoning block caching both look correct for their intended semantics. No bugs introduced by the changed lines.
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 very large sessions (profiled against a real ~2,450-item history), every frame that touched the
/costdialog or changed hover/selection state was spending tens to hundreds of milliseconds rebuilding output that had not changed. This made scrolling and navigation feel broken.The
/costdialog now caches its rendered content lines. The cache is keyed on a cheap fingerprint — content width, item count, and aggregate token/cost values — so it stays fresh during live streaming and is invalidated on theme changes. A regression test covers both cases. The expensive per-frame path of re-gathering cost data and re-styling ~1,200 usage lines drops from ~55–90 ms to ~0.6 ms per frame.The message list was thrashing its 500-entry rendered-item LRU whenever a hover or selection changed. A session larger than the cache means a sequential rebuild scan misses on every entry, re-parsing all markdown each frame (~59 ms for hover, ~292 ms for focused up/down). Three targeted fixes address this:
lrucache.EnsureCapacitylets the cache grow to cover all items before a full rebuild scan (memory cost is negligible since the lines are already retained byrenderedLines); reasoning blocks are now cached once their spinners and fades have settled (!NeedsTick()); andinvalidateItemunconditionally deletes the cache entry since cacheability is state-dependent. Hot paths after the fix are ~0.5 ms. Plain scrolling was already fast and is unchanged.No behaviour changes outside the rendering path.