terminal: release style refs per run instead of per cell in clearCells

clearCells released the style reference of every styled cell
individually: an array index, a ref decrement, and a liveness
check per cell. Styled cells overwhelmingly come in runs sharing
the same style id (a colored status bar, a highlighted region, a
full row painted in one color), so most of that work is repeated
bookkeeping on the same style entry.

This groups consecutive cells with the same style id and releases
each run with a single releaseMultiple call. Rows with alternating
styles degrade to the same per-cell cost as before; uniform rows,
the common case, do one ref-count update per run. The
releaseMultiple assertion that the ref count is at least the run
length holds by construction since every cell in the run held a
reference.

Measured with ghostty-bench terminal-stream (120x80, M4 Max,
ReleaseFast, hyperfine means of 5 runs). The erase corpus paints a
full screen of styled rows and erases it with ED 2 in a loop,
which is the pattern full-screen TUIs produce on clear/redraw:

| stream                     | before  | after   | change |
|----------------------------|---------|---------|--------|
| real 2.6 GB session corpus | 8.055 s | 7.965 s | +1.1%  |
| styled paint + ED 2 (100 MB) | 260 ms | 123 ms | 2.1x   |
This commit is contained in:
Mitchell Hashimoto
2026-07-06 12:50:03 -07:00
parent cb2d785871
commit 8d663a76e9

View File

@@ -1432,9 +1432,21 @@ pub fn clearCells(
}
if (row.styled) {
for (cells) |*cell| {
if (cell.hasStyling())
page.styles.release(page.memory, cell.style_id);
// Styled cells overwhelmingly come in runs sharing the same
// style (e.g. a colored status bar or a highlighted region),
// so group them and release each run with a single ref-count
// update rather than per cell.
var i: usize = 0;
while (i < cells.len) {
const id = cells[i].style_id;
if (id == style.default_id) {
i += 1;
continue;
}
var j = i + 1;
while (j < cells.len and cells[j].style_id == id) j += 1;
page.styles.releaseMultiple(page.memory, id, @intCast(j - i));
i = j;
}
// If we have no left/right scroll region we can be sure