Skip to content

perf(main loop): input first, coalesce buffered keys, bracketed paste - #95

Merged
amanusk merged 3 commits into
masterfrom
perf/input-priority
Sep 14, 2026
Merged

amanusk merged 3 commits into
masterfrom
perf/input-priority

Conversation

@amanusk

@amanusk amanusk commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Closes #91

Problem

The main loop's tokio::select! was unbiased, so when a key and a network response were both pending, tokio picked one at random. While monitoring a busy contract there is almost always a response pending, so roughly every other keystroke waited for a 64-action drain plus a redraw before it applied. Bracketed paste was not enabled either, so a pasted address arrived as one key event per character, each costing a full frame.

Change

  • biased; select with the terminal arm first. Responses cannot starve because key input is finite and bursty.
  • After each terminal event, drain input the terminal already buffered before redrawing, so a paste or key-repeat burst costs one frame. This uses crossterm's sync zero-timeout poll on purpose: EventStream registers the task waker only when it spawns its wake task, so a throwaway poll on the stream would capture a dead waker and stall input until the next network response.
  • Enable bracketed paste and handle Event::Paste. In search mode the whole text is inserted at the cursor with one suggestion refresh; control characters such as a trailing newline are dropped. Outside search mode a paste is ignored, where previously each pasted character ran as a normal-mode command.
  • Key, mouse, and paste dispatch moved into one handle_terminal_event shared by the stream arm and the drain.

Behavior note

Pasting text with a trailing newline used to submit the search immediately, because the newline arrived as an Enter key. The newline is now stripped and Enter is pressed explicitly.

Verification

  • cargo test: 4 new unit tests in src/app/input.rs cover paste at the cursor, control-char stripping, normal-mode ignore, and parity with typing the same characters.
  • Drove the release binary in tmux with an isolated cache and log dir, using the public ETH token address. Both bracketed pastes arrived as a single 66-byte event (Applying paste len=66) and were visible in the search bar within 20 ms idle and within 10 ms while the address view was streaming. Enter resolved the address, typed input still produced suggestions, Ctrl+C exited cleanly.
  • The frame benchmark from perf(ui): render only the visible page of address-tab rows #93/perf(registry): hash-indexed point lookups instead of linear scans #94 measures draw cost, which this change does not touch.

Follow-up

#92 attacks the same symptom from the other side by making each WebSocket-driven action cheap on large Calls lists.

🤖 Generated with Claude Code

The select was unbiased, so under a response burst roughly every other
keystroke waited for a 64-action drain plus a redraw before it applied.
Each key also cost a full frame, so a pasted address rendered one
character at a time.

- `biased;` select with the terminal arm first
- drain input the terminal already buffered before redrawing (sync
  zero-timeout poll: a throwaway stream poll would register a dead waker)
- enable bracketed paste; a paste lands in the search bar as one insert
  with one suggestion refresh, and is ignored outside search mode

Refs #91

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@amanusk amanusk added performance Performance: latency, throughput, resource usage snbeat Main snbeat TUI binary labels Sep 14, 2026
@amanusk
amanusk requested a lite review from Copilot September 14, 2026 05:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A critical UTF-8 cursor issue and two moderate input-handling issues remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Improves terminal responsiveness with input prioritization, buffered-event coalescing, and bracketed paste support.

Changes:

  • Prioritizes and drains terminal input before redraws.
  • Adds bracketed-paste handling and search insertion tests.
  • Enables the crossterm bracketed-paste feature.
File summaries
File Summary
src/main.rs Input dispatch and draining changes; moderate concerns about response starvation and stale errors after paste.
src/app/input.rs Paste insertion logic and tests; critical UTF-8 cursor-boundary issue.
Cargo.toml Enables bracketed-paste support.
Review details

Suppressed comments (2)

src/main.rs:478

  • Putting the terminal arm first makes response processing unboundedly postponable: a held key, continuous mouse-motion events, or another continuously readable input source can keep event_stream.next() ready on every iteration, so the response arm is never selected. The 256-event drain only bounds one batch; add a fairness/budget point that services pending responses after an input burst if background updates must not starve.
        tokio::select! {
            // Input first: a pending key must never queue behind a response burst.
            biased;

src/main.rs:540

  • Paste is now the fast path for characters that previously arrived as Key events, but this branch bypasses the app.error_message = None performed for every key. After a failed request, pasting into search leaves the stale error visible until another key is pressed; clear the error here to preserve the previous typing behavior.
            app::input::handle_paste(app, &text);
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/app/input.rs
Left, Right, and Backspace moved `search_cursor` by one byte, so after a
non-ASCII key or paste the next insert or remove hit a non-boundary index
and panicked. Step by whole chars instead, and derive the terminal cursor
column from the prefix's display width rather than its byte length.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical input-starvation and moderate stale-error findings must be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/main.rs:541

  • A paste in Search mode does not clear app.error_message, unlike the key path immediately above. If an async Action::Error arrives while the user is already searching, the pasted query updates the input but the stale error still takes priority in status_bar::draw, hiding the normal status; clear the error for search pastes before dispatching.
  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/main.rs
With the biased select an input event pending at every iteration would
never let the response arm run. Take one pending response after handling
input so responses always progress while input keeps priority.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Paste handling in src/main.rs and src/app/input.rs does not clear stale error messages.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

src/app/input.rs:529

  • Typing a character clears app.error_message in handle_terminal_event, but this paste path does not. If an Action::Error arrives while search mode is open, pasting leaves the stale error visible even though typing the same text would clear it; clear the error before search_insert (the early return already keeps normal-mode paste ignored).

src/main.rs:545

  • Unlike typed search input, a bracketed paste never clears app.error_message here. If a background response sets an error while search mode is open, the paste updates the query but leaves the stale error status visible until another key event; clear it for the same user action before delegating to handle_paste.
        Event::Paste(text) => {
            debug!(len = text.len(), "Applying paste");
            app::input::handle_paste(app, &text);
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@amanusk
amanusk merged commit 8d832f4 into master Sep 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance: latency, throughput, resource usage snbeat Main snbeat TUI binary

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(main loop): let terminal input win over response bursts and coalesce buffered key events before redrawing

2 participants