Skip to content

feat: reduce TUI idle CPU with event-driven UI wakeups#139

Merged
inureyes merged 3 commits into
mainfrom
feature/issue-135-event-driven-tui-wakeups
Apr 6, 2026
Merged

feat: reduce TUI idle CPU with event-driven UI wakeups#139
inureyes merged 3 commits into
mainfrom
feature/issue-135-event-driven-tui-wakeups

Conversation

@inureyes

@inureyes inureyes commented Apr 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace fixed 100ms event::poll() loop with tokio::select!-based event coordinator that sleeps fully when idle
  • Add UiEventCoordinator with dedicated blocking terminal reader task, Notify channel from data collectors, and isolated animation ticks
  • Wire data collectors to notify the UI immediately on fresh data via Arc<Notify>, eliminating polling-interval latency for screen refresh
  • Isolate animation cadence so ticks only fire when animated elements (loading indicator, marquee scroll) are visible

Test plan

  • Verify local mode (all-smi) starts up, renders GPU/CPU/memory info, and responds to keyboard navigation
  • Verify remote mode (all-smi view) starts up, renders multi-host data, and tab switching works
  • Confirm keyboard/mouse input feels immediate (no perceptible delay from old 100ms poll gate)
  • Confirm loading animation and marquee text scrolling still work during startup
  • Confirm terminal resize triggers immediate re-render
  • Verify idle CPU usage is measurably lower compared to the fixed-poll baseline
  • Run cargo test --features cli and confirm all existing tests pass
  • Run cargo clippy --features cli -- -D warnings with no warnings

Closes #135

Replace the 100ms event::poll() cadence with a tokio::select!-based
coordinator that sleeps fully when idle. The UI now wakes only for
terminal input, resize, fresh collector data, or animation ticks.

- Add UiEventCoordinator with dedicated blocking terminal reader task
- Add Notify channel from data collectors to UI loop for prompt refresh
- Isolate animation ticks to fire only when animated elements are visible
- Keyboard/mouse input is now independent of the polling interval

Closes #135
@inureyes inureyes added priority:high High priority issue type:performance Performance improvements status:review Under review labels Apr 6, 2026
Move the mpsc sender into the spawned blocking task instead of cloning
it, so the channel closes naturally when the terminal reader exits.
Add a TerminalClosed event variant to detect this in the select loop
and shut down gracefully, preventing a potential hang when the terminal
is lost and animations are inactive.
@inureyes

inureyes commented Apr 6, 2026

Copy link
Copy Markdown
Member Author

Code Review Summary: PR #139

Overall Assessment

This PR implements a well-designed transition from a fixed 100ms poll loop to an event-driven tokio::select!-based architecture. The core design is sound: the UiEventCoordinator cleanly separates terminal I/O (blocking, in spawn_blocking) from async data notifications (Arc<Notify>) and animation ticks, unifying them through a single next_event() select point.

Architecture Review

What works well:

  • The tokio::select! with conditional animation branch (if self.animations_active) is the correct approach for pausing animation ticks without busy-spinning
  • Using mpsc::channel(64) as a bounded bridge from the blocking terminal reader is appropriate -- prevents unbounded memory growth while accommodating input bursts
  • MissedTickBehavior::Skip is correct for animations -- burst-firing missed ticks would cause visual artifacts
  • The Notify::notify_one() semantics are appropriate here: multiple notifications between renders collapse into a single "data changed" signal, which is the desired behavior since the UI reads data_version to detect actual changes
  • The render throttle (MIN_RENDER_INTERVAL_MS) correctly gates expensive re-renders even when events arrive faster

No deadlock risk: The Arc<Mutex<AppState>> is only locked in one direction (collector locks briefly to write, UI locks to read/render), and the Notify is fire-and-forget -- there is no lock ordering issue.

Findings Addressed

# Severity Finding Status
1 MEDIUM next_event() did not handle terminal reader channel closure (recv() returning None). The original code used Some(evt) pattern in select, which silently disabled the branch when the channel closed. Combined with a stored term_tx clone keeping the channel artificially alive, this masked a potential hang condition on terminal loss. Fixed
2 LOW UiEventCoordinator stored a clone of term_tx as a field, preventing the channel from closing naturally when the terminal reader task exited. This prevented clean shutdown detection. Fixed (part of fix 1)

Fix Applied (commit e09b1d0)

  • Added UiEvent::TerminalClosed variant for graceful shutdown on terminal loss
  • Changed term_tx from mpsc::Sender<Event> to Option<mpsc::Sender<Event>>, moving the sender into the spawned task via take() instead of cloning
  • Changed spawn_terminal_reader(&self) to spawn_terminal_reader(&mut self) to support the take() pattern
  • Added TerminalClosed handler in ui_loop.rs that breaks out of the main loop
  • When the terminal reader exits (error, terminal gone), the channel closes naturally, recv() returns None, and the UI exits gracefully

Items Reviewed With No Issues Found

  • Security: No user input handling, no network-facing code in the changed files. The event coordinator only processes crossterm terminal events and internal notifications. No injection surface.
  • Race conditions: Notify::notify_one() stores at most one permit, but this is acceptable -- multiple data updates between renders are coalesced by design (the UI checks data_version).
  • Performance: The architecture genuinely achieves the stated goal. Idle CPU usage drops because the thread sleeps in tokio::select! rather than polling every 100ms. The blocking reader uses 50ms poll timeout which is a reasonable tradeoff between shutdown responsiveness and CPU usage.
  • Memory: Bounded channel (64 events) prevents unbounded growth. No new allocations in the hot path.
  • Correctness of animation gating: set_animations_active() is called after each render based on current state. Minor delay (one event cycle) in re-enabling animations is acceptable for a TUI monitoring tool.

Verification

  • cargo clippy --features cli -- -D warnings: zero warnings
  • cargo test --features cli: 389 tests pass, 0 failures

@inureyes inureyes removed the status:review Under review label Apr 6, 2026
Cover all UiEvent variants (DataReady, AnimationTick, TerminalClosed,
Resize, TerminalInput), UiEventCoordinator construction and toggle
methods, the data-ready notification path end-to-end with DataCollector,
and sanity checks for the new ANIMATION_TICK_MS / TERMINAL_READER_POLL_MS
AppConfig constants.
@inureyes

inureyes commented Apr 6, 2026

Copy link
Copy Markdown
Member Author

PR Finalization Complete

Summary

  • Tests: Added 14 unit tests in src/view/ui_events.rs covering all UiEvent variants, UiEventCoordinator construction, set_animations_active toggling, the DataReady notification path (with DataCollector wiring), the TerminalClosed path when the channel closes, event mapping logic for Resize and TerminalInput, and sanity/value assertions for the new ANIMATION_TICK_MS and TERMINAL_READER_POLL_MS AppConfig constants.
  • Documentation: No documentation gaps found; the new module has module-level and per-item doc comments already.
  • Lint/Format: cargo clippy --features cli -- -D warnings and cargo fmt --check both pass cleanly.

All 193 tests passing (179 lib + 14 new view::ui_events). Ready for merge.

@inureyes inureyes added the status:done Completed label Apr 6, 2026
@inureyes inureyes merged commit 78511b5 into main Apr 6, 2026
2 checks passed
@inureyes inureyes deleted the feature/issue-135-event-driven-tui-wakeups branch April 6, 2026 04:41
@inureyes inureyes self-assigned this Apr 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:high High priority issue status:done Completed type:performance Performance improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce TUI idle CPU and improve responsiveness with event-driven UI wakeups

1 participant