Skip to content

feat: snapshot AppState before rendering to shorten critical section#140

Merged
inureyes merged 3 commits into
mainfrom
feature/issue-136-render-snapshot
Apr 6, 2026
Merged

feat: snapshot AppState before rendering to shorten critical section#140
inureyes merged 3 commits into
mainfrom
feature/issue-136-render-snapshot

Conversation

@inureyes

@inureyes inureyes commented Apr 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Introduce RenderSnapshot struct that captures only frame-needed data from AppState under the mutex lock, then releases the lock immediately before expensive frame composition
  • Extract all frame assembly logic into a new FrameRenderer module that operates purely on the immutable snapshot, never holding the AppState lock
  • Refactor UiLoop::run() critical section to cover only mutable bookkeeping (scroll offsets, frame counter) and snapshot capture; all rendering, differential output, and stdout flushing happen after lock release
  • RenderSnapshot::as_app_state() provides backward compatibility bridge with existing UI functions that accept &AppState
  • VecDeque history data converted to Vec during snapshot capture for lighter cloning

Closes #136

Test plan

  • All 204 existing unit tests pass (including UI event coordinator, app state, renderer tests)
  • 8 new unit tests for RenderSnapshot: flag preservation, tab state, scroll state, sort state, data version, VecDeque-to-Vec conversion, snapshot independence from source mutation, and roundtrip as_app_state() conversion
  • cargo clippy -- -D warnings passes with no warnings
  • cargo fmt -- --check passes
  • Manual verification: run all-smi in local mode and confirm identical visual output
  • Manual verification: run all-smi view in remote mode and confirm tab switching, scrolling, and disconnection notification work correctly
  • Verify responsiveness under load: background collectors should not block on render-time critical sections

Introduce a RenderSnapshot captured quickly from AppState under the
mutex lock, then release the lock before expensive frame composition.
This reduces contention between UI rendering and background data
collectors.

Key changes:
- Add RenderSnapshot struct (render_snapshot.rs) with capture() method
  that clones only frame-needed data; VecDeque histories converted to
  Vec for lighter clone
- Extract all frame assembly logic into FrameRenderer (frame_renderer.rs)
  operating purely on the snapshot without holding any lock
- Refactor UiLoop::run() so the critical section only covers mutable
  bookkeeping (scroll offsets, frame counter) and snapshot capture;
  all rendering, differential output, and stdout flushing happen after
  the lock is dropped
- RenderSnapshot::as_app_state() provides backward compatibility with
  existing UI functions that accept &AppState
- Add 8 unit tests covering snapshot capture, independence, and
  roundtrip conversion

Closes #136
@inureyes inureyes added status:review Under review type:performance Performance improvements priority:high High priority issue labels Apr 6, 2026
…rame renderer

- Use saturating_sub for all arithmetic in render_disconnection_notification
  to prevent subtraction overflow panic with long hostnames
- Truncate text that exceeds box inner width instead of panicking
- Add early return guard for degenerate box_width < 6
- Pass shared view_state reference into render_gpu_section to eliminate a
  redundant as_app_state() allocation per frame
- Pass cols/rows through to render_local_devices instead of making a
  redundant crossterm::terminal::size() syscall mid-render
- Mark DEFAULT_TERMINAL_WIDTH/HEIGHT as dead_code now that the fallback
  is no longer needed in the render path
@inureyes

inureyes commented Apr 6, 2026

Copy link
Copy Markdown
Member Author

Code Review Summary -- PR #140

Findings Addressed (commit d928467)

1. [MEDIUM] Subtraction overflow panic in render_disconnection_notification

  • File: src/view/frame_renderer.rs (lines 356-411)
  • Issue: Bare arithmetic box_width - 4 - text.len() would panic on unsigned underflow when a hostname is long enough (56+ chars) to exceed the box inner width. Similarly, width - 4 panics if terminal width is less than 4.
  • Fix: Replaced all bare subtraction with saturating_sub(). Added text truncation via Cow when text exceeds inner width. Added early-return guard for degenerate box_width < 6.

2. [MEDIUM] Redundant as_app_state() allocation in GPU render path

  • File: src/view/frame_renderer.rs (line 188, now removed)
  • Issue: render_gpu_section called snapshot.as_app_state() to create a second full AppState clone, when render_main had already created one. Each call allocates all Vecs, HashMaps, and VecDeques. For a performance-focused PR, this defeats part of the purpose.
  • Fix: Pass &AppState reference from render_main into render_gpu_section, eliminating one full allocation per frame.

3. [LOW] Redundant crossterm::terminal::size() syscall in render_local_devices

  • File: src/view/frame_renderer.rs (line 460, now removed)
  • Issue: Called crossterm::terminal::size() again inside the render path, even though cols/rows are already available from the caller. This added a syscall and introduced a subtle inconsistency (terminal size could change between the two calls within the same frame).
  • Fix: Changed render_local_devices signature to accept cols: u16, rows: u16 from the caller instead.

Findings Not Requiring Code Changes

4. [LOW] update_scroll_offsets method signature improvement (already correct)

  • The change from &self method to associated function Self:: is a good cleanup.

Architecture Review -- No Issues Found

  • Snapshot completeness: The RenderSnapshot::capture() correctly clones all fields read during rendering. The known_hosts field is intentionally omitted because it is only used by the data collection layer, never during rendering.
  • as_app_state() roundtrip: The conversion is complete and correct. Fields like nvml_notification_shown and tenstorrent_notification_shown are not included in the snapshot because they are only read/written by the data collectors, not the rendering path. The AppState::new() defaults are safe for these.
  • Critical section correctness: The lock is held only for bookkeeping updates + snapshot capture, then released. No rendering code touches shared state afterward. No deadlock risk since there is only one mutex and it is never held across an await point after the snapshot.
  • VecDeque-to-Vec conversion: Using .iter().copied().collect() is correct and avoids cloning the ring buffer internals. The rendering path only iterates forward, so Vec is the right choice.
  • No race conditions: The snapshot is immutable after capture. Background collectors can freely update AppState while rendering proceeds on the snapshot.

Verification

  • cargo clippy --features cli -- -D warnings passes
  • cargo fmt -- --check passes
  • All tests pass (unit + doc tests)

@inureyes

inureyes commented Apr 6, 2026

Copy link
Copy Markdown
Member Author

PR Finalization Complete

Summary

  • Tests: Added 12 new unit tests for FrameRenderer in src/view/frame_renderer.rs:
    • Smoke tests for render_loading (local, remote, with startup status lines)
    • Smoke tests for render_main (empty state, header timestamp, version string)
    • Box geometry tests for render_disconnection_notification (narrow width, normal width, max-width cap, long hostname truncation)
    • Smoke test for render_help
    • Zero-sized struct assertion for FrameRenderer
    • render_snapshot.rs already had 8 unit tests (all passing)
  • Documentation: No documentation changes required (no public API surface changes)
  • Lint/Format: cargo fmt -- --check and cargo clippy --features cli -- -D warnings both pass with no issues

Total test count: 216 unit tests (up from 204), all passing.

@inureyes inureyes added status:done Completed and removed status:review Under review labels Apr 6, 2026
@inureyes inureyes merged commit 6bb29c0 into main Apr 6, 2026
2 checks passed
@inureyes inureyes deleted the feature/issue-136-render-snapshot branch April 6, 2026 05:04
@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.

Snapshot AppState before rendering to shorten the TUI critical section

1 participant