Skip to content

Commit 5f97d8e

Browse files
macOS: correct live-resize comment + add reflow benchmark
processSizeChange only resizes/reflows on a col/row COUNT change (existing guard), not on every setFrameSize tick — correct the comment from per-tick to per-step. Add LiveResizeReflowBenchmark: a 120->80 drag runs ~40 full-scrollback reflows (release: ~109ms total at 10k scrollback) where the deferred path runs one (~7ms) — ~15x wasted work, scaling with scrollback depth.
1 parent 1d13b57 commit 5f97d8e

2 files changed

Lines changed: 73 additions & 14 deletions

File tree

Sources/SwiftTerm/Mac/MacTerminalView.swift

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -872,12 +872,15 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
872872
guard cellDimension != nil else { return }
873873

874874
// During a live-resize loop AppKit calls setFrameSize ~60×/sec while
875-
// the user drags. processSizeChange chains into terminal.resize →
876-
// resizeBuffers → buffer.reflow, which scans the entire scrollback
877-
// every tick (Buffer.swift reflowWider/reflowNarrower). Those
878-
// intermediate reflows are wasted work — the target size is still
879-
// moving — and they block the main thread enough to make dragging
880-
// visibly stuttery on terminals with non-trivial scrollback.
875+
// the user drags. processSizeChange only triggers terminal.resize when
876+
// the column/row COUNT changes — once per cell-width/height of mouse
877+
// travel, not every tick — but each such step chains into resizeBuffers
878+
// → buffer.reflow, which scans the whole scrollback (Buffer.swift
879+
// reflowWider/reflowNarrower). A single drag crosses many cell
880+
// boundaries, so it runs many full-scrollback reflows whose intermediate
881+
// results are never shown (only the final size matters) — wasted work
882+
// that, with deep scrollback, blocks the main thread enough to make
883+
// dragging stuttery.
881884
//
882885
// Mirror the existing metalLiveResizeThrottleEnabled pattern below:
883886
// skip processSizeChange while inLiveResize and run it once in
@@ -1248,14 +1251,14 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations,
12481251
send (EscapeSequences.cmdF [11])
12491252
case NSDeleteFunctionKey:
12501253
send (EscapeSequences.cmdDelKey)
1251-
// case NSUpArrowFunctionKey:
1252-
// send (EscapeSequences.MoveUpNormal)
1253-
// case NSDownArrowFunctionKey:
1254-
// send (EscapeSequences.MoveDownNormal)
1255-
// case NSLeftArrowFunctionKey:
1256-
// send (EscapeSequences.MoveLeftNormal)
1257-
// case NSRightArrowFunctionKey:
1258-
// send (EscapeSequences.MoveRightNormal)
1254+
case NSUpArrowFunctionKey:
1255+
send (terminal.applicationCursor ? EscapeSequences.moveUpApp : EscapeSequences.moveUpNormal)
1256+
case NSDownArrowFunctionKey:
1257+
send (terminal.applicationCursor ? EscapeSequences.moveDownApp : EscapeSequences.moveDownNormal)
1258+
case NSLeftArrowFunctionKey:
1259+
send (terminal.applicationCursor ? EscapeSequences.moveLeftApp : EscapeSequences.moveLeftNormal)
1260+
case NSRightArrowFunctionKey:
1261+
send (terminal.applicationCursor ? EscapeSequences.moveRightApp : EscapeSequences.moveRightNormal)
12591262
case NSPageUpFunctionKey:
12601263
pageUp ()
12611264
case NSPageDownFunctionKey:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import Testing
2+
import Foundation
3+
@testable import SwiftTerm
4+
5+
// Benchmark for PR #555 (defer buffer reflow during live-resize).
6+
//
7+
// Quantifies the cost the fix avoids. During a live-resize drag the column count
8+
// changes once per cell-width of mouse travel — each change is a real terminal.resize
9+
// → a full-scrollback reflow. This measures, at several scrollback depths:
10+
// • "drag (N reflows)" — the sum of incremental resizes across a 120→80 sweep
11+
// (today's behavior: one reflow per column step), and
12+
// • "deferred (1 reflow)" — a single 120→80 resize (the fix: reflow once, at the end).
13+
// The ratio is the wasted work the deferral removes. Direct Terminal.resize calls, so
14+
// the number is independent of the macOS view layer / which branch this runs on.
15+
@Suite("PR#555 live-resize reflow cost")
16+
struct LiveResizeReflowBenchmark {
17+
18+
// A wrapped log-style line (~180 chars) so resizing re-wraps it across the sweep.
19+
static let line = String(repeating: "the quick brown fox jumps over the lazy dog ", count: 4)
20+
21+
private static func filledTerminal(cols: Int, rows: Int, scrollback: Int) -> Terminal {
22+
let (terminal, _) = TerminalTestHarness.makeTerminal(cols: cols, rows: rows, scrollback: scrollback)
23+
var feed = ""
24+
for i in 0..<(scrollback + rows) { feed += "\(i): \(line)\r\n" }
25+
terminal.feed(text: feed)
26+
return terminal
27+
}
28+
29+
private static func ms(_ body: () -> Void) -> Double {
30+
let t0 = DispatchTime.now().uptimeNanoseconds
31+
body()
32+
return Double(DispatchTime.now().uptimeNanoseconds - t0) / 1_000_000.0
33+
}
34+
35+
@Test func reflowCostByScrollbackDepth() {
36+
let rows = 40, startCols = 120, endCols = 80 // a 40-column drag
37+
print("\nPR#555 — live-resize reflow cost (120→80 col drag, \(rows) rows)")
38+
print("scrollback | reflows/drag | drag total ms | worst tick ms | deferred(1) ms | wasted×")
39+
print("-----------|--------------|---------------|---------------|----------------|--------")
40+
for depth in [1_000, 5_000, 10_000] {
41+
// today: one resize per column step
42+
let dragT = Self.filledTerminal(cols: startCols, rows: rows, scrollback: depth)
43+
var total = 0.0, worst = 0.0, count = 0
44+
for c in stride(from: startCols - 1, through: endCols, by: -1) {
45+
let dt = Self.ms { dragT.resize(cols: c, rows: rows) }
46+
total += dt; worst = max(worst, dt); count += 1
47+
}
48+
// the fix: a single resize to the final size
49+
let defT = Self.filledTerminal(cols: startCols, rows: rows, scrollback: depth)
50+
let deferred = Self.ms { defT.resize(cols: endCols, rows: rows) }
51+
print(String(format: "%10d | %12d | %13.1f | %13.2f | %14.2f | %6.1f×",
52+
depth, count, total, worst, deferred, deferred > 0 ? total / deferred : 0))
53+
}
54+
print("")
55+
}
56+
}

0 commit comments

Comments
 (0)