perf(tui): off-load snapshot refresh from event loop#240
Merged
Conversation
The dashboard's first paint and every tick refresh were blocking the render loop on `gh pr list` (~1s) and the parallel HTTP fan-out (~25ms), serially. With 50 plans this shows up as a 1-2s black screen at startup and a 1s freeze every 5s during use, plus 200ms keystroke latency. Three changes: 1. Decouple `Client::snapshot()` from the render loop via `mpsc`. The first paint no longer waits for the snapshot — the dashboard shows a skeleton frame immediately and folds data in as it arrives. The periodic tick and `r` keystroke spawn a tokio task; an in-flight flag debounces overlapping refreshes. Adds `AppState::apply_snapshot` so the loop can push a pre-fetched `Snapshot` without re-awaiting. The new method (and the existing `refresh` wrapper) live in a fresh `state_lifecycle` module to keep `state.rs` under the 300-line cap. 2. `gh pr list` runs on `tokio::process::Command` (was `std::process::Command`, which pinned a runtime worker for the duration of the shell-out) and runs concurrently with the HTTP fan-out via `tokio::join!`. Results are memoised for 30s — most refreshes pay zero gh cost. `--limit` reduced from 100 to 50. 3. Drop `EnableMouseCapture` (no handler exists; it stole native scroll and spammed the input poll). Shorten the key-poll window from 200ms to 50ms so keystrokes feel snappy. Measurements against a live daemon (50 plans, 351 tasks, loopback): | State | Before | After | |------------------|---------|-----------| | First paint | ~1.5s | <100ms | | Refresh freeze | ~1s | 0 (async) | | Cached refresh | ~1s | ~20ms | | Keystroke lag | ≤200ms | ≤50ms | Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
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.
Problem
cvg dashis unusable on real workloads. With 50 plans / 351 tasks thedashboard shows a 1-2s black screen at startup, freezes for ~1s every 5s
during the periodic refresh, and lags up to 200ms per keystroke.
Root causes (
crates/convergio-tui/src/lib.rs:120and friends):state.refresh(&client).awaitis called before the firstterm.draw, so the user sees nothing until the first snapshot — andthe snapshot is dominated by
gh pr list --state all --limit 100(~1s on a warm cache).
refresh().awaitsits inside thetokio::select!eventloop, so every tick /
rkeystroke blocks key handling and rendering.gh pr listran onstd::process::Command::output(), pinning aruntime worker for the entire shell-out.
EnableMouseCapturewas on with no mouse handler — it stole nativescroll and spammed the input poll with
Noopevents.event::poll(200ms)introduced up to 200ms of input latency.Why
Dashboard responsiveness regression is felt on the first session of the
day, the loop tick, and every keypress. Constitution P3 asks the TUI
to be usable on a basic 80×24 terminal — it must also be usable in real
time.
What changed
Decouple
Client::snapshot()from the render loop viampsc.First paint shows a skeleton frame immediately; data folds in as it
arrives. Periodic tick and
rspawn a tokio task; an in-flightflag debounces overlapping refreshes. Adds
AppState::apply_snapshotso the loop can push a pre-fetched
Snapshotwithout re-awaiting.The new method (and the existing
refreshwrapper) live in a newstate_lifecyclemodule to keepstate.rsunder the 300-line cap.gh pr listruns ontokio::process::Commandand joins the HTTPfan-out via
tokio::join!. Results are memoised for 30s — mostrefreshes pay zero gh cost.
--limitreduced from 100 to 50.Drop
EnableMouseCapture, shorten the key-poll window from200ms to 50ms.
Validation
Measured on the live dashboard (50 plans, 351 tasks, daemon on loopback):
Snapshot bench against the live daemon (release):
Local pipeline:
cargo fmt --all -- --check— cleanRUSTFLAGS="-Dwarnings" cargo clippy --workspace --all-targets -- -D warnings— cleanRUSTFLAGS="-Dwarnings" cargo test --workspace— 1043 passed, 0 failedImpact
convergio-tuionly — read-only client, no daemon-side change.AppState::apply_snapshot(Result<Snapshot>).AppState::refresh(&Client)is preserved for the existing test path.the rendered list well below 50.
rstill triggersa fresh refresh; PR pane labels show last refresh timestamp via the
existing footer.
🤖 Generated with Claude Code