Skip to content

perf: window the transcript render to O(viewport)#5

Merged
saucam merged 2 commits into
mainfrom
perf/scrollback-windowed-render
Jun 23, 2026
Merged

perf: window the transcript render to O(viewport)#5
saucam merged 2 commits into
mainfrom
perf/scrollback-windowed-render

Conversation

@saucam

@saucam saucam commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Fixes the mouse-wheel scroll lag on large sessions.

Diagnosis

The transcript re-processed the whole buffer every frame. Even on a cache hit, ui/scrollback.rs did:

let lines = state.scrollback_build.lines.clone();        // clone ALL N lines, every frame
Paragraph::new(lines).wrap(Wrap{trim:false}).scroll((y,0)) // ratatui re-wraps ALL N lines, every frame

ratatui's Paragraph isn't virtualized — .wrap().scroll((y,0)) word-wraps the entire transcript and then throws away everything above row y to show ~40 rows. So a multi-thousand-line session re-wrapped the whole buffer on every mouse-wheel tick → the sluggishness. The per-message RenderCache helped, but this final step was still O(transcript) per frame.

Fix — O(viewport) per frame

  • On each (rare) rebuild, also compute a prefix sum of per-line wrapped-row counts (ScrollbackBuild::row_offsets), using ratatui's own line_count per line so it stays byte-for-byte consistent with how the slice lays out.
  • On render, visible_window() binary-searches the few logical lines intersecting [y, y+viewport) and hands ratatui only that slice (plus an intra-line scroll offset). No full clone, no full re-wrap.

Net: a 10k-line session now re-wraps ~viewport rows per scroll instead of 10k. Layout is identical — only the per-frame work changed.

Tests

visible_window is pure + unit-tested (top/middle/bottom windows, short-buffer, empty-safe, and a property test that the slice always covers the requested window). Full codeoid-tui suite: 162 passed. cargo fmt clean.

Note: this supersedes render/wrap.rs's rendered_rows/total_rendered_rows for scroll math (they were already unused on main); leaving them as a separate cleanup.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactor
    • Improved scrollback scrolling performance with faster, windowed viewport rendering.
    • More accurate handling of wrapped text during vertical navigation for smoother transitions.
  • Bug Fixes
    • Fixed scrollback viewport updates on scrolling so the visible content aligns correctly with the selected scroll position (including near the top and bottom of the buffer).

The scrollback re-processed the ENTIRE transcript every frame. Even on a
cache hit it cloned the full Vec<Line> and handed it to ratatui's
Paragraph, which word-wraps all lines (then discards everything above the
scroll offset) just to show ~40 visible rows. On a multi-thousand-line
session every mouse-wheel tick re-wrapped the whole buffer — visibly
sluggish, exactly the O(transcript)-per-scroll the symptom pointed at.

Cache a prefix sum of per-line wrapped-row counts on each (already rare)
rebuild, then on render binary-search the handful of logical lines that
intersect the viewport and hand ratatui only that slice. Scrolling is now
O(viewport), not O(transcript); the per-frame full-buffer clone + re-wrap
are gone.

`visible_window` is a pure, unit-tested function (incl. a property test
that the returned slice always covers the requested window). Per-line
counts use ratatui's own line_count, so the layout stays byte-for-byte
identical to before — only the work per frame changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 47f0cae6-6cde-4c6f-a8c3-47178d257f05

📥 Commits

Reviewing files that changed from the base of the PR and between 0d37742 and f844da7.

📒 Files selected for processing (1)
  • crates/codeoid-tui/src/ui/scrollback.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/codeoid-tui/src/ui/scrollback.rs

📝 Walkthrough

Walkthrough

The scrollback cache now stores wrapped-row prefix offsets, adds a helper to map viewport rows to logical-line slices, and updates rendering to build and use that slice instead of scrolling a paragraph over the full buffered transcript.

Changes

Scrollback viewport slicing

Layer / File(s) Summary
Row offset state and window helper
crates/codeoid-tui/src/state/scrollback_build.rs
ScrollbackBuild adds row_offsets, resets them in clear, and exposes visible_window(...) with tests for middle, top, bottom, oversized viewport, coverage, and empty-offset cases.
Windowed scrollback rendering
crates/codeoid-tui/src/ui/scrollback.rs
The cache rebuild now computes per-line wrapped row counts and prefix offsets, and rendering uses visible_window(...) to render only lines[first..last] with an intra-line scroll offset. Tests validate cache-rebuild prefix sums and viewport scroll behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Render
  participant ScrollbackBuild
  participant visible_window
  participant Paragraph

  Render->>ScrollbackBuild: compute per-line wrapped row counts
  Render->>ScrollbackBuild: store row_offsets and total_rendered_rows
  Render->>visible_window: visible_window(row_offsets, y, viewport_rows)
  visible_window-->>Render: first, intra, last
  Render->>Paragraph: build paragraph from lines[first..last]
  Render->>Paragraph: scroll((intra, 0))
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: window the transcript render to O(viewport)' directly and precisely summarizes the main optimization: transforming transcript rendering from O(transcript length) to O(viewport) complexity by implementing windowed rendering.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/scrollback-windowed-render

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.41176% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 41.86%. Comparing base (d7dea06) to head (f844da7).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
crates/codeoid-tui/src/state/scrollback_build.rs 98.18% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main       #5      +/-   ##
==========================================
+ Coverage   36.79%   41.86%   +5.07%     
==========================================
  Files          30       30              
  Lines        5156     5317     +161     
==========================================
+ Hits         1897     2226     +329     
+ Misses       3259     3091     -168     
Flag Coverage Δ
tui 41.86% <99.41%> (+5.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/codeoid-tui/src/ui/scrollback.rs 66.75% <100.00%> (+66.75%) ⬆️
crates/codeoid-tui/src/state/scrollback_build.rs 80.00% <98.18%> (+80.00%) ⬆️

... and 1 file with indirect coverage changes

Add two TestBackend tests: one asserts the prefix sum is built and the
latest message shows at the bottom (following mode); the other scrolls to
the top and asserts the window tracks the offset (earliest line visible,
latest off-screen). Exercises the cache-miss build + windowed render path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@saucam saucam merged commit 28a2aa3 into main Jun 23, 2026
4 checks passed
@saucam saucam mentioned this pull request Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant