Skip to content

Commit f54d85e

Browse files
committed
Fixes the gratuitous resizing fight: before the MacTerminal when resized would change on both ends and even move the window origin
1 parent d6b61a0 commit f54d85e

4 files changed

Lines changed: 135 additions & 41 deletions

File tree

Sources/SwiftTerm/Apple/TerminalViewDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public protocol TerminalViewDelegate: AnyObject {
5151
* }
5252
* ```
5353
*
54-
* `TerminalApp/MacTerminal` implements exactly this. See <doc:Embedding>.
54+
* See <doc:Embedding> for more information.
5555
*/
5656
func sizeChanged (source: TerminalView, newCols: Int, newRows: Int)
5757

Sources/SwiftTerm/Documentation.docc/Embedding.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ func sizeChanged (source: TerminalView, newCols: Int, newRows: Int) {
6666
}
6767
```
6868

69-
`TerminalApp/MacTerminal` implements this.
70-
7169
### Why the library cannot simply fix this for you
7270

7371
Coalescing every frame change — not just live drags — would be a small win for

TerminalApp/MacTerminal/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
1313
@IBOutlet var loggingMenuItem: NSMenuItem?
1414

1515
func applicationDidFinishLaunching(_ aNotification: Notification) {
16-
// Insert code here to initialize your application
16+
ResizeTrace.reset()
1717
}
1818

1919
func applicationWillTerminate(_ aNotification: Notification) {

TerminalApp/MacTerminal/ViewController.swift

Lines changed: 133 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,50 @@ import Cocoa
1010
import SwiftTerm
1111
import UniformTypeIdentifiers
1212

13+
@MainActor
14+
enum ResizeTrace {
15+
static let path = "/tmp/MacTerminal-resize.log"
16+
17+
private static var fileHandle: FileHandle?
18+
private static var startTime = ProcessInfo.processInfo.systemUptime
19+
20+
static func reset() {
21+
try? fileHandle?.close()
22+
_ = FileManager.default.createFile(atPath: path, contents: nil)
23+
fileHandle = FileHandle(forWritingAtPath: path)
24+
startTime = ProcessInfo.processInfo.systemUptime
25+
log("trace-start pid=\(ProcessInfo.processInfo.processIdentifier)")
26+
}
27+
28+
static func log(_ message: String) {
29+
if fileHandle == nil {
30+
reset()
31+
return
32+
}
33+
let elapsed = ProcessInfo.processInfo.systemUptime - startTime
34+
let line = String(format: "%10.6f %@\n", elapsed, message)
35+
guard let data = line.data(using: .utf8) else { return }
36+
try? fileHandle?.write(contentsOf: data)
37+
try? fileHandle?.synchronize()
38+
}
39+
40+
static func rect(_ value: NSRect) -> String {
41+
String(
42+
format: "{x=%.1f y=%.1f w=%.1f h=%.1f maxX=%.1f maxY=%.1f}",
43+
value.origin.x,
44+
value.origin.y,
45+
value.size.width,
46+
value.size.height,
47+
value.maxX,
48+
value.maxY
49+
)
50+
}
51+
52+
static func point(_ value: NSPoint) -> String {
53+
String(format: "{x=%.1f y=%.1f}", value.x, value.y)
54+
}
55+
}
56+
1357
private actor InputLatencyPendingFrame {
1458
private var awaitingNanoseconds: UInt64?
1559
private var recordedMilliseconds: Double?
@@ -45,46 +89,21 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
4589

4690
private var reverseVideoTestState: ReverseVideoTestState?
4791

48-
var changingSize = false
4992
var logging: Bool = false
5093
var zoomGesture: NSMagnificationGestureRecognizer?
5194
var postedTitle: String = ""
5295
var postedDirectory: String? = nil
96+
private var resizeTraceObservers = [NSObjectProtocol]()
97+
private var resizeTraceLastFrame: NSRect?
98+
private var resizeTraceHasOriginStack = false
5399

54100
func sizeChanged(source: LocalProcessTerminalView, newCols: Int, newRows: Int) {
55-
if changingSize {
56-
return
57-
}
58-
guard let window = view.window else { return }
59-
var newFrame = terminal.getOptimalFrameSize ()
60-
let windowFrame = window.frame
61-
newFrame = CGRect (x: windowFrame.minX, y: windowFrame.minY, width: newFrame.width, height: windowFrame.height - view.frame.height + newFrame.height)
62-
63-
// Two changes from the obvious version, both measured on the
64-
// resize-under-flood case (Docs/io-baselines.md, G5b):
65-
//
66-
// 1. **No animation.** Animating the snap-to-cell-size makes AppKit
67-
// emit a stream of intermediate frames, each of which resizes the
68-
// terminal and calls back here. Main-thread stall p99 over a resize
69-
// churn: 14–35 ms animated against 6–17 ms not. It is also the
70-
// reason SwiftTerm cannot coalesce resizes outside a live drag —
71-
// with the animation gone, coalescing every frame change costs
72-
// nothing (6.45–15.32 ms), and with it the same change costs
73-
// 188–216 ms.
74-
// 2. **Idempotent rather than re-entrant.** `changingSize` only stops
75-
// the loop when the frame change re-enters this method inside the
76-
// same call stack. Comparing against the frame we already have
77-
// stops it either way, including when the callback arrives a frame
78-
// later, and costs nothing.
79-
let epsilon: CGFloat = 0.5
80-
if abs(newFrame.width - windowFrame.width) < epsilon,
81-
abs(newFrame.height - windowFrame.height) < epsilon {
82-
return
83-
}
84-
85-
changingSize = true
86-
window.setFrame(newFrame, display: true, animate: false)
87-
changingSize = false
101+
// LocalProcessTerminalView updates the PTY. Do not resize the window here.
102+
let windowFrame = source.window.map { ResizeTrace.rect($0.frame) } ?? "nil"
103+
ResizeTrace.log(
104+
"terminal-size cols=\(newCols) rows=\(newRows) "
105+
+ "view=\(ResizeTrace.rect(source.frame)) window=\(windowFrame)"
106+
)
88107
}
89108

90109
func updateWindowTitle ()
@@ -253,6 +272,7 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
253272
override func viewDidAppear() {
254273
super.viewDidAppear()
255274
guard let window = view.window else { return }
275+
installResizeTrace(for: window)
256276
if let terminalWindowCloseObserver {
257277
NotificationCenter.default.removeObserver(terminalWindowCloseObserver)
258278
}
@@ -294,7 +314,7 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
294314
}
295315

296316
override func viewWillDisappear() {
297-
//terminal = nil
317+
removeResizeTraceObservers()
298318
}
299319

300320
@objc
@@ -308,10 +328,86 @@ class ViewController: NSViewController, @MainActor LocalProcessTerminalViewDeleg
308328

309329
override func viewDidLayout() {
310330
super.viewDidLayout()
311-
changingSize = true
331+
recordLayoutTrace("viewDidLayout-before")
312332
terminal.frame = view.frame
313-
changingSize = false
314333
terminal.needsLayout = true
334+
recordLayoutTrace("viewDidLayout-after")
335+
}
336+
337+
private func installResizeTrace(for window: NSWindow) {
338+
removeResizeTraceObservers()
339+
resizeTraceLastFrame = window.frame
340+
resizeTraceHasOriginStack = false
341+
recordResizeTrace("viewDidAppear", window: window)
342+
343+
let center = NotificationCenter.default
344+
let events: [(Notification.Name, String)] = [
345+
(NSWindow.willStartLiveResizeNotification, "willStartLiveResize"),
346+
(NSWindow.didResizeNotification, "didResize"),
347+
(NSWindow.didMoveNotification, "didMove"),
348+
(NSWindow.didEndLiveResizeNotification, "didEndLiveResize"),
349+
(NSWindow.didChangeScreenNotification, "didChangeScreen"),
350+
(NSWindow.didChangeBackingPropertiesNotification, "didChangeBackingProperties"),
351+
]
352+
353+
resizeTraceObservers = events.map { name, label in
354+
center.addObserver(forName: name, object: window, queue: .main) {
355+
[weak self, weak window] _ in
356+
MainActor.assumeIsolated {
357+
guard let self, let window else { return }
358+
if name == NSWindow.willStartLiveResizeNotification {
359+
self.resizeTraceLastFrame = window.frame
360+
self.resizeTraceHasOriginStack = false
361+
}
362+
self.recordResizeTrace(label, window: window)
363+
}
364+
}
365+
}
366+
}
367+
368+
private func removeResizeTraceObservers() {
369+
resizeTraceObservers.forEach(NotificationCenter.default.removeObserver)
370+
resizeTraceObservers.removeAll()
371+
}
372+
373+
private func recordResizeTrace(_ label: String, window: NSWindow) {
374+
let frame = window.frame
375+
let previous = resizeTraceLastFrame ?? frame
376+
let delta = NSRect(
377+
x: frame.minX - previous.minX,
378+
y: frame.minY - previous.minY,
379+
width: frame.width - previous.width,
380+
height: frame.height - previous.height
381+
)
382+
let currentEvent = NSApp.currentEvent
383+
let eventName = currentEvent.map { String(describing: $0.type) } ?? "nil"
384+
let eventLocation = currentEvent.map { ResizeTrace.point($0.locationInWindow) } ?? "nil"
385+
let contentFrame = window.contentView.map { ResizeTrace.rect($0.frame) } ?? "nil"
386+
ResizeTrace.log(
387+
"window-event=\(label) live=\(window.inLiveResize) "
388+
+ "frame=\(ResizeTrace.rect(frame)) delta=\(ResizeTrace.rect(delta)) "
389+
+ "content=\(contentFrame) mouseScreen=\(ResizeTrace.point(NSEvent.mouseLocation)) "
390+
+ "event=\(eventName) eventWindow=\(eventLocation) "
391+
+ "buttons=\(NSEvent.pressedMouseButtons)"
392+
)
393+
394+
let originChanged = abs(frame.minX - previous.minX) > 0.01
395+
|| abs(frame.minY - previous.minY) > 0.01
396+
if originChanged && !resizeTraceHasOriginStack {
397+
resizeTraceHasOriginStack = true
398+
ResizeTrace.log("first-origin-change-stack " + Thread.callStackSymbols.prefix(16).joined(separator: " <- "))
399+
}
400+
resizeTraceLastFrame = frame
401+
}
402+
403+
private func recordLayoutTrace(_ label: String) {
404+
let windowFrame = view.window.map { ResizeTrace.rect($0.frame) } ?? "nil"
405+
ResizeTrace.log(
406+
"layout=\(label) viewFrame=\(ResizeTrace.rect(view.frame)) "
407+
+ "viewBounds=\(ResizeTrace.rect(view.bounds)) "
408+
+ "terminalFrame=\(ResizeTrace.rect(terminal.frame)) "
409+
+ "terminalBounds=\(ResizeTrace.rect(terminal.bounds)) window=\(windowFrame)"
410+
)
315411
}
316412

317413
@objc @IBAction

0 commit comments

Comments
 (0)