Skip to content

v0.8.6 perf: lag/freeze audit — sync git on UI thread, unbounded history Vec, file-tree blocking walk #399

@Hmbown

Description

@Hmbown

Pitch

A user just had the TUI "basically die" — sustained lag / unresponsiveness during normal use. This is an audit issue covering the live suspects. Earlier perf passes (#310, #313, #325, #326, #337, #339) closed individual hotspots, but a second-pass scan turned up several still-shipping problems that compound on long sessions or large repos.

Confirmed suspects, ranked by likelihood

S1 — Synchronous git shellouts on the UI thread (HIGH)

Where: crates/tui/src/tui/ui.rs:4838 refresh_workspace_context_if_neededcrates/tui/src/tui/ui.rs:4954 run_git_query.

Every WORKSPACE_CONTEXT_REFRESH_SECS (15s) the render loop runs git rev-parse --abbrev-ref HEAD and git status --short --untracked-files=normal synchronously via Command::new(\"git\").output(). On a busy disk or a large repo, git status regularly takes 100–500ms; that is a hard freeze of the UI thread on a 15-second cadence.

Fix: push both queries to a spawn_supervised task with a debounced result channel; render loop reads the latest Option<String> non-blockingly. Guard against pile-up by skipping if a previous query is still in flight.

S2 — On-screen history: Vec<HistoryCell> has no memory ceiling (HIGH on long sessions)

Where: crates/tui/src/tui/app.rs (history: Vec<HistoryCell>); rendered in full every frame via render → history widgets.

#326 added ceilings for api_messages / tool_log but not for the on-screen history Vec that the renderer walks. Long sessions accumulate cells indefinitely; layout cost and per-frame allocation grow linearly. Combined with #78's earlier scrolling lag, this is the most likely "slowly gets worse and then dies" mode.

Fix: soft cap (e.g. 5000 cells). When exceeded, fold the oldest N into a single "… earlier turns collapsed (use /history to view) …" placeholder cell. Keep the api_messages cap independent so model context isn't affected.

S3 — build_file_tree_inner does a sync recursive read_dir on the UI thread (NEW, uncommitted)

Where: crates/tui/src/tui/file_tree.rs:155 (currently uncommitted on feat/v0.8.6).

FileTreeState::new walks the workspace synchronously when the user hits Ctrl+E. Skips only .git/node_modules/target/.DS_Store — does not honor .gitignore, so monorepos with build outputs in non-standard dirs (dist/, .next/, build/, vendor/, .venv/) walk hundreds of thousands of files on the UI thread. Also no depth cap.

Fix: (a) parse .gitignore (we already depend on ignore transitively via ripgrep's ecosystem — or pull in ignore directly); (b) move the initial walk to spawn_blocking and show a Building file tree… placeholder; (c) lazy-load directory contents on first expand instead of walking everything up front.

S4 — history_has_live_motion linear scan per poll (LOW–MEDIUM)

Where: crates/tui/src/tui/ui.rs:1211 calls history_has_live_motion(&app.history) on every poll iteration (every ~24-48ms). Walks the entire history Vec checking for any running tool / streaming cell.

Cheap per-cell, but with S2 unfixed it's O(n) every 24ms on an unbounded Vec.

Fix: maintain an App.live_motion_count: usize incremented when a streaming/running cell is added and decremented when it finalizes. O(1) check per frame.

S5 — 120 FPS streaming cap can starve frames when history is long (MEDIUM)

Where: crates/tui/src/tui/frame_rate_limiter.rs (MIN_FRAME_INTERVAL = 8.33ms).

When streaming long responses with a deep history, terminal.draw itself can take >8ms (full re-layout + diff against backbuffer). The frame cap doesn't help if the frame budget is the bottleneck. Symptom: the terminal feels "sticky" — a keypress takes ~one frame to register.

Fix: when frame time exceeds budget for N consecutive frames, automatically downshift to low-motion mode (30 FPS + smooth-only chunking). User can override with /lowmotion off. We already have the low_motion plumbing — just add the auto-trigger.

S6 — Streaming chunk fan-out can issue many tiny SSE writes (LOW, but worth measuring)

Where: crates/tui/src/tui/ui.rs:481-594 (streaming push/commit path).

Every SSE delta calls streaming_state.push_content + commit_text and flips needs_redraw. The frame-rate limiter coalesces draws, but the per-delta work (sanitize → push → commit → tokenize for context bar) still runs on the UI thread. On a fast network with a long response, that's hundreds of calls per second.

Fix: add a criterion bench for the push/commit path (#230 is the right place to extend). If hot, batch deltas with a 16ms accumulator before the sanitize+commit step.

Lower-priority cleanups found in passing

  • crates/tui/src/tui/ui.rs:4070 allocates a new Block for the background fill every frame — use a static or theme-cached value.
  • crates/tui/src/tui/file_mention.rs:694 does sync read_dir for mention-menu population on the UI thread; bounded by workspace size and only fires on @, but worth spawn_blocking if the user has a large flat directory.
  • crates/tui/src/tui/clipboard.rs:231 has an unwrap() on std::fs::read for a pasted file — not a perf issue but a panic hazard if the pasted path was deleted between paste and read.

Acceptance

  • S1 fixed: git status never blocks the UI thread; manual test on this repo + a fresh checkout of a large monorepo (e.g. chromium-class) shows no 15s hitch.
  • S2 fixed: a 4-hour session in /yolo shows bounded history Vec length; transcript collapse placeholder visible.
  • S3 fixed: opening Ctrl+E on a monorepo with a 500K-file node_modules returns under 500ms (or shows a placeholder while building).
  • S4 fixed: live_motion_count field replaces the linear scan.
  • S5 fixed: auto-downshift to low-motion when frame time exceeds budget for 30 consecutive frames.
  • S6 measured: criterion bench for streaming push path; only ship a fix if it shows up hot.
  • Standard verification gates pass (cargo fmt, clippy -D warnings, cargo test --workspace --all-features --locked, parity gates).

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or requestv0.8.6Targeting v0.8.6

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions