Skip to content

Recycle lines when scrolling within margins - #6192

Open
slbls wants to merge 1 commit into
xtermjs:masterfrom
slbls:recycle-scroll-region-lines
Open

slbls wants to merge 1 commit into
xtermjs:masterfrom
slbls:recycle-scroll-region-lines

Conversation

@slbls

@slbls slbls commented Sep 26, 2026

Copy link
Copy Markdown

Scrolling within DECSTBM margins allocates a new BufferLine on every line feed, even though the line leaving the region is discarded. This change reuses that line instead, as the full-screen path already does with recycle():

  • With a top margin, shiftElements overwrites the region's top line. That line is now captured first, reset with copyFrom(newLine, true), and placed at the bottom.
  • With only a bottom margin and a full buffer, splice trims the oldest line, which is now reused as the inserted line.

Benchmarks

Using xterm-benchmark in the same way as Terminal.benchmark.ts, I wrote a 4.3 MB ls -lR /System/Library/Frameworks capture into an 80×25 CoreBrowserTerminal, once at full screen, once inside a top margin of 2;25, and once inside a bottom margin of 1;24. Figures are medians across 6 sessions of 30 runs per build, alternating builds in ABBA order, with 95% bootstrap intervals. This was on Node 26 and an Apple M5.

Case master This PR Ratio
Full screen 35.1 ms 35.0 ms 1.00 [0.98, 1.01]
Top margin 54.9 ms 38.6 ms 0.70 [0.68, 0.71]
Bottom margin 61.6 ms 48.3 ms 0.78 [0.77, 0.80]

An --eval run against a master baseline reported +41% and +29% throughput for the margin cases and −0.4% for full screen.

The existing Terminal.benchmark.ts cases were unchanged across the same sessions, at 1.00 [0.99, 1.01] for write/string and 1.00 [0.81, 1.23] for write/utf8. On macOS, ls -lR /usr/lib produces only about 50 KB, so single --eval runs of those cases varied by up to 9% even between two master builds.

Replaying vtebench's region scrolling payloads, which scroll one short line at a time, through headless bundles built with esbuild-package-headless-only showed larger gains. These are medians across 6 processes of 8 samples of 8 MiB each on a 158×41 grid:

Payload Margins master This PR Wall time ratio CPU time ratio
scrolling_bottom_region 1;40 1287 ms 406 ms 0.32 [0.31, 0.33] 0.19 [0.19, 0.19]
scrolling_bottom_small_region 1;20 1379 ms 547 ms 0.40 [0.38, 0.40] 0.26 [0.24, 0.26]
scrolling_top_region 2;41 1863 ms 680 ms 0.36 [0.34, 0.38] 0.26 [0.24, 0.27]
scrolling_top_small_region 20;41 1600 ms 514 ms 0.32 [0.31, 0.33] 0.21 [0.21, 0.22]

On master, CPU time exceeds wall time for these payloads, since the allocations keep V8's GC threads busy. The other 8 vtebench payloads don't reach these paths and stayed within 0.96–1.02.

If it would help, I can add margin variants of the ls -lR /usr/lib case to Terminal.benchmark.ts in a follow-up.

Tests

Two new tests in BufferService.test.ts cover repeated scrolls within top and bottom margins, writing into the recycled row between scrolls, and a bottom-only margin with a full buffer. Both also pass on master. Inserting the cached blank line directly instead of recycling fails the first new test, along with the existing DECSTBM and alt-screen tests.

In the demo, scrolling 2,000 lines inside a top margin and 30,000 inside a bottom margin with a full scrollback left the same buffer on master and on this branch.

Overlap with #5797

#5797 rewrites scroll() around LogicalLine, so this will conflict. Its region path still allocates a line per scroll, so the same reuse could apply there. I'm happy to port this onto it or hold off, whichever you prefer.

runewav1 added a commit to runewav1/xterm.js that referenced this pull request Oct 6, 2026
apply a change introduced by a pull request on the main xterm.js repository, proposed by @slbls (thanks!) to make scrolling operations faster/more efficient within margin by recycling
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