@@ -10,6 +10,50 @@ import Cocoa
1010import SwiftTerm
1111import 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+
1357private 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