perf(tui): eliminate streaming render and persistence hotspots#4
Draft
Blankeos wants to merge 5 commits into
Draft
perf(tui): eliminate streaming render and persistence hotspots#4Blankeos wants to merge 5 commits into
Blankeos wants to merge 5 commits into
Conversation
Deploying crabcode with
|
| Latest commit: |
b475102
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://875d095d.crabcode.pages.dev |
| Branch Preview URL: | https://cursor-optimize-tui-renderin.crabcode.pages.dev |
replace_messages ran a delete plus one autocommitted insert per message on the UI thread every 250ms streaming snapshot, each with its own journal rewrite and fsync. Wrap the rewrite in a single transaction with a cached prepared statement and switch the database to WAL with synchronous=NORMAL so snapshots become one cheap log append. Co-authored-by: Carlo Taleon <Blankeos@users.noreply.github.com>
…lines Tool-heavy streaming messages (the common shape for subagent transcripts) reformatted every tool part on each layout refresh: a JSON clone, a serialize + reparse round-trip, row formatting, wrapping, and syntect diff highlighting per part. Cache each rendered tool row keyed by a structural hash of its payload, scoped to the actively streaming message so memory stays bounded; skip building parsed tool info for tools that can never join exploration/task groups. Chat::render also deep-cloned every visible line (including all span strings) on each frame at up to 25fps while streaming. Borrow the visible lines from the layout cache instead, and make the highlight/selection helpers lifetime-generic with Cow-preserving span splitting. Also fix an unbounded scan in editor_location_for_selection when the selection extends past the cached location table (it could spin on usize::MAX), and clear the ordered markdown cache in Chat::clear which previously leaked across session resets. Co-authored-by: Carlo Taleon <Blankeos@users.noreply.github.com>
The event loop cloned the entire terminal cell grid after every full frame to feed the isolated subagent spinner fast-path, even when that path could never run. Keep the copy only while the fast-path is active and reuse its allocation via clone_from; spinner frames also restore the base buffer in place instead of cloning it twice. isolated_subagent_spinner_interval built the full subagent tab list (strings, theme colors, session walks) on every loop iteration just to answer a boolean, and the spinner render built it again for one color. Use a cheap parent-id check and a new allocation-free SessionManager::descendant_position for the tab color instead. Chats stored away as background session views now release their rebuildable line caches, so memory no longer scales with every session visited during a run. Co-authored-by: Carlo Taleon <Blankeos@users.noreply.github.com>
The session-to-persistence message conversion cloned every part's JSON payload; combined with the caller's own clone this deep-copied the whole transcript twice per streaming snapshot. Consume the owned parts instead. Co-authored-by: Carlo Taleon <Blankeos@users.noreply.github.com>
Blankeos
force-pushed
the
cursor/optimize-tui-rendering-acff
branch
from
July 18, 2026 19:11
0523a62 to
6f6b9eb
Compare
… usage walk Switching between subagent tabs (ctrl+x down, left/right) rebuilt the entire transcript layout on every switch because render caches were released whenever a chat was stored away. Retain caches for the current session family (shared root) so cycling subagent tabs is a warm-cache render again, and release only chats outside the family, keeping the memory bound from the earlier change. While viewing a streaming session, the status-bar usage text re-walked the whole transcript on every layout refresh, re-serializing tool args (entire file bodies for edit/write) and re-parsing tool payloads to estimate tokens. Cache the completed-message token base keyed by session, message count, and streaming index; only the streaming message's counter changes per refresh. Editor-location inference over the streaming message also deep-cloned tool args per refresh via ParsedToolMessage and allocated three display strings per path candidate per line. Read payloads by reference, precompute mention needles once per pass, and reuse one line-text buffer. Co-authored-by: Carlo Taleon <Blankeos@users.noreply.github.com>
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.
Summary
Streaming sessions — especially tool-heavy subagent transcripts — lagged because several O(transcript) costs ran on the UI thread on every refresh, and each frame did avoidable deep copies. This PR removes the hotspots without changing any rendered output or behavior.
Streaming persistence (biggest win)
HistoryDAO::replace_messagesran aDELETEplus one autocommittedINSERTper message on the UI thread every 250ms streaming snapshot (per concurrently streaming session). Each statement was its own journal rewrite + fsync, so cost grew linearly with transcript length. It now runs in a single transaction with a cached prepared statement.synchronous=NORMAL, turning snapshots into one cheap log append and keeping readers non-blocking.Tool-heavy / subagent transcript layout
(message, part), validated by a structural hash of the payload, and scoped to the actively streaming message so memory stays bounded (cache drops when the stream ends or moves to a new message).edit/write, whose args embed whole file contents) no longer build parsed tool info at all on the grouping check, avoiding a deep JSON clone per part per refresh.Subagent tab switching (
ctrl+x down, left/right)Per-frame allocations
Chat::renderdeep-cloned every visible line (all span strings) each frame, at up to 25fps while streaming. The viewport now borrows lines from the layout cache; highlight/selection helpers were made lifetime-generic withCow-preserving span splitting so cold paths still work unchanged.clone_from; spinner frames restore the base buffer in place instead of cloning it twice.isolated_subagent_spinner_intervalbuilt the full subagent tab list (session walk + string/theme allocations) every event-loop iteration just to answer "is this a child session"; it now does an O(1) parent-id check. The spinner render resolves its one color via a new allocation-freeSessionManager::descendant_positioninstead of materializing all tabs.Memory
Chat::clearnow clears the ordered markdown part cache, which previously survived session resets.Bug fixes found along the way
editor_location_for_selectioniteratedstart_line..=end_linewhere an unclamped selection can haveend_line == usize::MAX, spinning a core at 100% (this also madecargo testhang on the pre-existingchat_selection_action_bar_shows_before_mouse_uptest). The scan is now bounded by the cached location table.Testing
release_render_cachesrebuilds byte-identical output;descendant_positionmatchesdescendant_sessionsordering; the selection-location scan terminates on unclamped selections.ollamacatalog tests,test_handle_models_shows_ollama_without_connection,test_edit_tool_renders_codex_style_diff_summary) fail identically onmainin this environment (hardcoded developer cwd / shared-cache flakiness) and are unrelated.cargo fmtandcargo clippyare clean relative tomain(same warning count, none in touched code).