Skip to content

perf(buffer): rotate lines instead of copying cells when scrolling - #142

Open
YLzone wants to merge 2 commits into
charmbracelet:mainfrom
YLzone:perf-scroll-line-rotation
Open

perf(buffer): rotate lines instead of copying cells when scrolling#142
YLzone wants to merge 2 commits into
charmbracelet:mainfrom
YLzone:perf-scroll-line-rotation

Conversation

@YLzone

@YLzone YLzone commented Jul 13, 2026

Copy link
Copy Markdown

Problem

Buffer.DeleteLineArea / Buffer.InsertLineArea copy every cell of the area one by one. For the common case — a terminal scrolling by one line — that is O(width × height) Cell struct copies per scrolled line, even though the content of the lines does not change; only their order does.

I ran into this while profiling a charmbracelet/x/vt emulator fed with a plain-text stream: DeleteLineArea was the top hotspot by a wide margin, because every \n on the bottom row copies the whole buffer.

Change

Add a fast path to both methods: when the area spans the full width of every affected line and the fill cell is a plain single-width cell, rotate the line slices instead of copying cells. Per scrolled line this turns O(width × height) cell copies into O(height) slice-header moves plus one O(width) blank fill of the recycled line.

The canRotateLines guard keeps the existing cell-by-cell path wherever rotation would not be equivalent:

  • a wide fill cell (Width != 1), which needs the zero-width placeholder fix-ups in Line.Set;
  • an area narrower than the buffer;
  • lines that are not all the buffer's width — rotating those would change what Buffer.Width() reports, since it reads len(b.Lines[0]).

Benchmarks

One line scrolled off the top of a full buffer (go test -bench . -count 6, Go 1.25):

buffer before after speedup
80×24 11.6 µs/op 0.37 µs/op 31×
120×40 29.4 µs/op 0.59 µs/op 50×
200×60 91.0 µs/op 0.96 µs/op 95×

0 allocs/op before and after. The win grows with the buffer size because the old cost is O(width × height) and the new one is O(height + width).

Tests

InsertLineArea / DeleteLineArea had no test coverage, so this PR adds some: full-buffer scroll, multi-line insert/delete, clamping when n exceeds the area, scroll regions, a styled fill cell, a narrow sub-width area (which must keep taking the slow path), and a wide fill cell whose placeholders must still be written through Line.Set.

Those tests pass unchanged on main before the fast path is applied, so they characterise existing behaviour rather than new behaviour. go test ./... passes with the change.

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