Skip to content

perf(shell): skip reading unchanged rows when rendering#2918

Open
domenkozar wants to merge 2 commits into
mainfrom
fix/shell-dirty-row-rendering
Open

perf(shell): skip reading unchanged rows when rendering#2918
domenkozar wants to merge 2 commits into
mainfrom
fix/shell-dirty-row-rendering

Conversation

@domenkozar

Copy link
Copy Markdown
Member

Summary

devenv shell mediates all PTY output through a libghostty VT and previously re-read every cell of every row on each frame, so rendering cost scaled with screen area. Nested full-screen TUIs (neovim, claude-code) that emit frequent updates triggered full-screen re-reads even for single-line changes, causing noticeable lag on larger terminals.

The renderer now tracks per-row dirty state via a libghostty RenderState:

  • Clean rows that already have a baseline are skipped without reading their cells.
  • The screen's dirty flags are consumed after each frame, so only rows changed since then are re-read.
  • When the render state can't be allocated, it falls back to reading every row (and now logs that fallback so the lost optimization is observable).

Output is byte-identical; this only avoids redundant per-cell FFI reads.

Also bumps the PTY read buffer from 4 KB to 64 KB so a full-screen repaint burst feeds the VT as one frame instead of fragmenting across syscalls.

Correctness note

The optimization relies on libghostty's per-row dirty flags being a superset of all visible changes. This holds because a global Dirty::Full (palette / reverse-video / scroll-region / default-color changes) forces every per-row bit dirty in ghostty's render-state update, so reading only the per-row flags is safe. A viewport scroll marks dirtiness at the page level, which the render-state snapshot reflects (the bare per-row bit would miss it) — covered by a regression test.

Tests

Deterministic unit tests (no timing):

  • A single-line repaint reads only a handful of rows.
  • An unchanged frame reads no rows.
  • A full-screen scroll re-reads every shifted row via the snapshot dirty flag (asserts on rows_read, so it fails if the page-level dirty path regresses — not just a substring check).

🤖 Generated with Claude Code

devenv shell mediated all PTY output through a libghostty VT and re-read
every cell of every row on each frame, so rendering cost scaled with
screen area. Nested full-screen TUIs (neovim, claude-code) that emit
frequent updates triggered full-screen re-reads even for single-line
changes, causing noticeable lag on larger terminals.

The renderer now tracks per-row dirty state via a libghostty RenderState:
clean rows that already have a baseline are skipped without reading their
cells, and the screen's dirty flags are consumed after each frame so only
rows changed since then are re-read. When the render state can't be
allocated it falls back to reading every row. Output is byte-identical;
this only avoids redundant per-cell FFI reads.

Also bumps the PTY read buffer from 4KB to 64KB so a full-screen repaint
burst feeds the VT as one frame instead of fragmenting across syscalls.

Adds a rows_read counter and deterministic unit tests asserting a
single-line repaint reads only a handful of rows, and an unchanged frame
reads none.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 9, 2026

Copy link
Copy Markdown

Deploying devenv with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0e29c58
Status: ✅  Deploy successful!
Preview URL: https://5411dea0.devenv.pages.dev
Branch Preview URL: https://fix-shell-dirty-row-renderin.devenv.pages.dev

View logs

@domenkozar

Copy link
Copy Markdown
Member Author

I'm still not happy with this, claude scrollbar is slow

@AodhanHayter

Copy link
Copy Markdown

I took this branch for a test drive, it does feel better, but really starts to struggle as soon as I hit the bounds of a pane and some kind of scroll happens. I owe you a video as it's more clear what kind of renders it struggles with watching a session.

It's definitely not to the point that I'd willingly run nvim or any other TUI within the devenv TUI itself, there's a perfectly fine workaround with devenv --no-tui --no-reload. Although, I could see this catching new users off guard and them not understanding why their system is feeling sluggish all of the sudden. It took me a bit to narrow down devenv as the culprit myself.

I appreciate you burning some tokens for my sake. I can also live with just using devenv a bit differently than I have in the past.

@domenkozar

Copy link
Copy Markdown
Member Author

I want to fix this before we get 2.2 out.

@domenkozar domenkozar added this to the 2.2 milestone Jun 16, 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.

2 participants