Skip to content

perf(core): stop rescanning stable content on scroll-only frames - #24

Merged
GeekCmore merged 1 commit into
mainfrom
perf/transcript-scroll-frame
Sep 20, 2026
Merged

GeekCmore merged 1 commit into
mainfrom
perf/transcript-scroll-frame

Conversation

@GeekCmore

Copy link
Copy Markdown
Collaborator

Summary

  • Root cause: pi-tui rebuilds its render cache every frame, so a wheel scroll re-rendered the whole content tree — and FrameClampedContainer re-ran visibleWidth over every transcript row (~20-60ms/frame once past the 512-entry width cache), blowing the 16ms frame budget and producing the stutter + visible repaint artifacts.
  • Per-child incremental clamp: FrameClampedContainer now clamps each child independently, keyed on the row array it returned. A stable transcript is never rescanned; a changed sibling pays only for its own rows. Frame-relative overflow indexes are preserved.
  • Identity-stable wrappers: LocalActivityRegion / StableSlotHost / GutterComponent / BannerComponent reuse their row arrays while children and width are unchanged; TranscriptModelComponent returns a fresh array per live update instead of mutating a reused frame buffer (same-identity mutation is now invisible to the clamp).
  • Surface layout memoization: SurfaceManager.layout()/linearLayout() memoize per viewport until registry, user-state, focus, activation, or collapse flags change; setCollapsed invalidates on flag flips so collapse hysteresis stays identical to fresh computation. The dock's surface budget reuses lane row counts via measureRows() instead of rendering panes a second time.
  • Scroll-only doRender() on the real composition drops from ~20-60ms to <1ms on synthetic 1.5k-4k-row transcripts; remaining per-frame cost is pi-tui's viewport diff write.

Test plan

  • pnpm run verify:changed — related specs, 100% coverage on touched files
  • pnpm run verify:full — 3378 tests, typecheck, lint, build, happy smoke
  • pnpm run check:pack — all 3 tarballs clean
  • pnpm run smoke:pty (40x24 narrow PTY session) and pnpm run smoke:pty:mouse (wheel path)
  • Human acceptance on dsh --profile mayfly-scroll-perf: long-transcript wheel/PgUp scrolling, live streaming at tail, manual scroll away + End follow, narrow-width collapse, /new + session-switch lifecycle

Generated with Devin

pi-tui rebuilds its render cache every frame, so a wheel scroll re-rendered
the whole content tree — and the frame clamp re-ran visibleWidth over every
transcript row (~20-60ms/frame past the 512-entry width cache). Make the
render path identity-stable so scroll frames only diff the viewport:

- FrameClampedContainer clamps each child independently, keyed on the row
  array it returned; a stable transcript is never rescanned, and a changed
  sibling only pays for its own rows.
- TranscriptModelComponent returns a fresh array per live update instead of
  mutating a reused frame buffer (same-identity mutation is now invisible
  to the clamp cache).
- LocalActivityRegion / StableSlotHost / GutterComponent / BannerComponent
  reuse their row arrays while children and width are unchanged.
- SurfaceManager memoizes layout() and linearLayout() per viewport until
  registry, user-state, focus, activation, or collapse flags change.
- The dock's surface budget reuses the lanes' own rendered row counts via
  measureRows() instead of rendering panes a second time.

Verified: verify:full, check:pack, smoke:pty, smoke:pty:mouse.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@GeekCmore
GeekCmore merged commit 9d98401 into main Sep 20, 2026
7 checks passed
@GeekCmore
GeekCmore deleted the perf/transcript-scroll-frame branch September 20, 2026 08:05
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