Files
ddidderr 4cfeee71cc docs: add TODO for smooth resize with large scrollback
With a large scrollback-limit (e.g. 1 GiB), resizing the window or adding
a split freezes the UI for a moment. Investigation showed the column
reflow rewrites the entire scrollback synchronously while holding the
terminal lock, which blocks the renderer and also the GTK main thread
(focus, mouse and key paths lock the terminal). Window drags trigger a
full reflow every 25 ms coalescing step, and the reflow inner loop scans
all tracked pins per cell. The design is inherited from upstream Ghostty.

TODO.md records the analysis and the options: a resize benchmark, reflow
fast paths, removing main-thread terminal locking on hot paths, deferring
history reflow until a drag settles, an incremental chunked reflow, and a
config escape hatch, with a recommended order. No behaviour change.
2026-10-12 23:25:38 +02:00

4.8 KiB
Raw Permalink Blame History

TODO

Smooth resize with very large scrollback

Symptom: with a large scrollback-limit (e.g. 1 GiB) and a pane holding a lot of history, resizing the window or adding a split freezes the UI for a moment.

Why it freezes

  1. Every resize rewrites the whole scrollback. Any column change (window drag, new split) runs PageList::resize_cols → reflow_pages (crates/ghostty-terminal/src/page_list.rs), which rewrites every page cell by cell into new pages. With ~1 GiB of history that is 100M+ cells and roughly 2 GiB of memory traffic: a single synchronous O(scrollback) job.
  2. It holds the terminal lock the whole time. Termio::resize (crates/ghostty-termio/src/termio.rs) runs the reflow inside lock_terminal(), so:
    • the renderer thread can't build a frame and that pane stops updating;
    • the GTK main thread blocks whenever it touches that terminal. Adding a split changes focus, and focus_callback (crates/ghostty-core/src/surface/mod.rs) does self.lock_terminal().flags.focused = …. Mouse motion, the mouse cursor shape and key handling also take the lock. So the whole window freezes, not just the pane.
  3. A drag means many full reflows in a row. Resizes are only coalesced for 25 ms (COALESCE_MIN_MS, crates/ghostty-termio/src/thread.rs). Each coalesced step reflows everything, so during a drag the IO thread holds the lock almost all the time.
  4. The inner loop does extra work. reflow_row and remap_cell scan every tracked pin for every cell, which is O(cells × pins). Every cell also goes through write_cell, even on plain short lines that would fit unchanged.

Upstream Zig Ghostty has the same design (including the per-cell pin scan in reflowRow), so this was inherited faithfully, not introduced by the rewrite.

Key point: making the reflow faster helps but can't get to "smooth at all times". Even at memory bandwidth, rewriting ~1 GiB takes 100 ms or more, far beyond a 16 ms frame. To truly never stall, the full reflow must not happen under the lock in one go.

Options, from cheapest to most thorough

  • 0. Resize benchmark in ghostty-bench (fill N GiB, resize the columns) to get real numbers before and after each change.

  • A. Speed up the reflow (low risk, contained in page_list.rs)

    • Skip the pin scan for pages with no tracked pins (nearly all scrollback), or pre-sort pins per page. This removes the O(cells × pins) factor.
    • Fast path for rows that fit at the new width and aren't wrapped: copy the whole row at once (cells plus style/grapheme/hyperlink refs) instead of cell by cell.
    • Page-level fast path: when shrinking, a page with no wrapped rows whose content already fits keeps its storage and just gets a smaller size.cols, with no copy.
    • Likely several times faster, but only the benchmark will tell. Still won't be frame-time at 1 GiB.
  • B. Stop the GTK main thread from blocking on the terminal lock (moderate, independent of A)

    • Hot main-thread paths shouldn't need the lock: the focus flag via an IO message or atomic; the mouse-reporting mode and mouse shape cached in atomics.
    • Result: the window, other panes, split creation and dragging stay smooth. Only the pane being reflowed briefly shows its last frame.
    • Probably the biggest perceived improvement for the effort.
  • C. Defer the history reflow until the drag settles (moderate)

    • During a drag, reflow only the active screen. Reflow history once the size has been stable for about 100–200 ms.
    • Avoids repeated full reflows mid-drag; one stall at the end remains unless combined with D.
  • D. Incremental, chunked reflow (the real fix, biggest change)

    • Reflow from the newest content toward the oldest, a few milliseconds of work per lock hold, releasing the lock in between so the renderer and pty keep running.
    • Requires the page list to allow pages of different widths temporarily, so the renderer, selection, search and scrollbar must cope with "not yet reflowed" rows. If another resize arrives mid-way, the job just switches to the new target width.
    • Variant: only reflow the last N rows eagerly and do older history in the background, or when it's scrolled into view.
  • E. Config escape hatch (trivial)

    • A setting such as "reflow only the last N MB/rows of scrollback" and leave older history unreflowed. Useful as a stopgap but changes behaviour.
  1. B + A first. They're mostly contained, low risk, and remove the whole-window freeze plus most of the per-pane stall.
  2. Then C for live drags.
  3. D only if the remaining single-pane hitch still matters. It's the only option that fully guarantees smoothness, but it's a real design change and diverges from upstream.