From 4a88cc5948295019d85f09ad77bcc303b7aba69a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 31 Jul 2026 20:44:52 -0700 Subject: [PATCH] terminal: skip reflow pin scans for rows without pins Reflow scanned the full tracked pin list for every source cell it copied, twice per cell in the wide-character case, even though pins are rare and at most a handful exist. Each check also went through node.page(), which can restore a compressed page just to compare pointers. reflowRow now determines once per row whether any tracked pin is on the source row and skips the per-cell pin scans entirely when there is none, which is the overwhelmingly common case. The comparisons use node identity instead of pages: a node owns exactly one page, so they are equivalent, and this avoids the restore hazard. 1.09x faster on ghostty-bench +terminal-resize --mode=cols (120x80 terminal, 10k-line scrollback, shrink/grow column reflow cycles). --- src/terminal/PageList.zig | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index b8149544c..532e26fea 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1566,12 +1566,21 @@ const ReflowCursor = struct { if (cols_len == 0 and src_row.semantic_prompt != .none) cols_len = 1; } - // Handle tracked pin adjustments. + // Handle tracked pin adjustments. We also note whether any + // tracked pin is on this row at all so that the per-cell loop + // below can skip pin scans entirely for the overwhelmingly + // common case of a row with no pins. Note we compare nodes + // rather than pages since a node owns exactly one page; this + // is cheaper and avoids `page()` restoring unrelated + // compressed nodes purely for a comparison. + var row_has_pins = false; { const pin_keys = list.tracked_pins.keys(); for (pin_keys) |p| { - if (p.node.page() != src_page or - p.y != src_y) continue; + if (p.node != row.node or p.y != src_y) continue; + + // This row has pins + row_has_pins = true; if (cursor_pin != null and p == cursor_pin.?) continue; @@ -1593,7 +1602,7 @@ const ReflowCursor = struct { // If the cursor is after blanks on the right, those cells are still // before the next write and must reflow with it. if (cursor_pin) |p| { - if (p.node.page() == src_page and p.y == src_y) { + if (p.node == row.node and p.y == src_y) { cols_len = @max(cols_len, p.x + 1); } } @@ -1641,10 +1650,10 @@ const ReflowCursor = struct { } // Move any tracked pins from the source. - { + if (row_has_pins) { const pin_keys = list.tracked_pins.keys(); for (pin_keys) |p| { - if (p.node.page() != src_page or + if (p.node != row.node or p.y != src_y or p.x != x) continue; @@ -1667,16 +1676,15 @@ const ReflowCursor = struct { .skip_next => { // Remap any tracked pins at the skipped position (x+1) // since we won't process that cell in the loop. - const pin_keys = list.tracked_pins.keys(); - for (pin_keys) |p| { - if (p.node.page() != src_page or + if (row_has_pins) for (list.tracked_pins.keys()) |p| { + if (p.node != row.node or p.y != src_y or p.x != x + 1) continue; p.node = self.node; p.x = self.x; p.y = self.y; - } + }; x += 2; },