Skip to content

Commit e8e2efa

Browse files
committed
Coalesce idempotent refreshSession calls triggered by event handlers
On operations like workspace changes, AeroSpace receives a stampede of accessibility events, each of which triggers a serial call to `refreshSession()` on the main thread. Since these `refreshSession()` calls are idempotent, we can coalesce these calls so that multiple accessibility events can be handled by a single `refreshSession()`. With this change, operations like workspace switch go from O(num_source_windows + num_dest_windows) to O(1) `refreshSession()` calls (with similar improvements for operations like closing windows or resizing windows).
1 parent 46b5f5e commit e8e2efa

File tree

4 files changed

+25
-6
lines changed

4 files changed

+25
-6
lines changed

Sources/AppBundle/GlobalObserver.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class GlobalObserver {
1111
}
1212
let notifName = notification.name.rawValue
1313
MainActor.assumeIsolated {
14-
refreshAndLayout(.globalObserver(notifName), screenIsDefinitelyUnlocked: false)
14+
scheduleRefreshAndLayout(.globalObserver(notifName), screenIsDefinitelyUnlocked: false)
1515
}
1616
}
1717

@@ -67,7 +67,7 @@ class GlobalObserver {
6767
// Detect close button clicks for unfocused windows. Yes, kAXUIElementDestroyedNotification is that unreliable
6868
// And trigger new window detection that could be delayed due to mouseDown event
6969
default:
70-
refreshAndLayout(.globalObserverLeftMouseUp, screenIsDefinitelyUnlocked: true)
70+
scheduleRefreshAndLayout(.globalObserverLeftMouseUp, screenIsDefinitelyUnlocked: true)
7171
}
7272
}
7373
}

Sources/AppBundle/layout/refresh.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,25 @@ func refreshAndLayout(_ event: RefreshSessionEvent, screenIsDefinitelyUnlocked:
4747
refreshSession(event, screenIsDefinitelyUnlocked: screenIsDefinitelyUnlocked, startup: startup, body: {})
4848
}
4949

50+
@MainActor private var havePendingRefresh = false
51+
@MainActor private var pendingRefreshScreenIsDefinitelyUnlocked = false
52+
53+
@MainActor
54+
func scheduleRefreshAndLayout(_ event: RefreshSessionEvent, screenIsDefinitelyUnlocked: Bool = false) {
55+
if havePendingRefresh {
56+
if screenIsDefinitelyUnlocked {
57+
pendingRefreshScreenIsDefinitelyUnlocked = true
58+
}
59+
return
60+
}
61+
havePendingRefresh = true
62+
pendingRefreshScreenIsDefinitelyUnlocked = screenIsDefinitelyUnlocked
63+
DispatchQueue.main.async { @MainActor in
64+
havePendingRefresh = false
65+
refreshSession(event, screenIsDefinitelyUnlocked: pendingRefreshScreenIsDefinitelyUnlocked, startup: false, body: {})
66+
}
67+
}
68+
5069
@MainActor
5170
func refreshModel() {
5271
gc()
@@ -81,7 +100,7 @@ func refreshObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: Unsaf
81100
check(Thread.isMainThread)
82101
let notif = notif as String
83102
MainActor.assumeIsolated {
84-
refreshAndLayout(.ax(notif), screenIsDefinitelyUnlocked: false)
103+
scheduleRefreshAndLayout(.ax(notif), screenIsDefinitelyUnlocked: false)
85104
}
86105
}
87106

Sources/AppBundle/mouse/moveWithMouse.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ func movedObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: UnsafeM
99
if let windowId, let window = Window.get(byId: windowId), TrayMenuModel.shared.isEnabled {
1010
moveWithMouseIfTheCase(window)
1111
}
12-
refreshAndLayout(.ax(notif), screenIsDefinitelyUnlocked: false)
12+
scheduleRefreshAndLayout(.ax(notif), screenIsDefinitelyUnlocked: false)
1313
}
1414
}
1515

Sources/AppBundle/mouse/resizeWithMouse.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ func resizedObs(_ obs: AXObserver, ax: AXUIElement, notif: CFString, data: Unsaf
99
if let windowId, let window = Window.get(byId: windowId), TrayMenuModel.shared.isEnabled {
1010
resizeWithMouseIfTheCase(window)
1111
}
12-
refreshAndLayout(.ax(notif), screenIsDefinitelyUnlocked: false)
12+
scheduleRefreshAndLayout(.ax(notif), screenIsDefinitelyUnlocked: false)
1313
}
1414
}
1515

@@ -21,7 +21,7 @@ func resetManipulatedWithMouseIfPossible() {
2121
for workspace in Workspace.all {
2222
workspace.resetResizeWeightBeforeResizeRecursive()
2323
}
24-
refreshAndLayout(.resetManipulatedWithMouse, screenIsDefinitelyUnlocked: true)
24+
scheduleRefreshAndLayout(.resetManipulatedWithMouse, screenIsDefinitelyUnlocked: true)
2525
}
2626
}
2727

0 commit comments

Comments
 (0)