perf: debounce screen/space events to minimize work

This commit is contained in:
lwouis committed 2026-01-04 14:18:17 +01:00
1 parent ff66a0664d
commit e2b47790a9
2 files changed
+50 -18

No files matched your search

+42 -9
View File
@@ -1,19 +1,52 @@
import Cocoa
class ScreensEvents {
static let debouncerScreenAndSpace = Debouncer(.main)
static func observe() {
NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: NSApplication.didChangeScreenParametersNotification, object: nil)
}
@objc private static func handleEvent(_ notification: Notification) {
Logger.debug { notification.name.rawValue }
Spaces.refresh()
// a screen added or removed, or screen resolution change can mess up layout; we reset components
App.app.resetPreferencesDependentComponents()
// a screen added or removed can shuffle windows around Spaces; we refresh them
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" }
Logger.info { "spaces:\(Spaces.screenSpacesMap)" }
Logger.info { "current space:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId))" }
// screen notifications often arrive in groups (e.g. 2 in a row in a short time)
// we debounce to minimize work
debouncerScreenAndSpace.debounce(.screenEvent) {
Logger.debug { notification.name.rawValue }
Spaces.refresh()
// a screen added or removed, or screen resolution change can mess up layout; we reset components
App.app.resetPreferencesDependentComponents()
}
}
}
final class Debouncer {
private let queue: DispatchQueue
private var workItem: DispatchWorkItem?
private var contextsBlocks = [DebounceContext: () -> Void]()
init(_ queue: DispatchQueue) {
self.queue = queue
}
func debounce(_ context: DebounceContext, _ block: @escaping () -> Void) {
contextsBlocks[context] = block
workItem?.cancel()
let item = DispatchWorkItem(block: {
for block in self.contextsBlocks.values {
block()
}
// ScreensEvents: a screen added or removed can shuffle windows around Spaces; we refresh them
// SpacesEvents: if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" }
Logger.info { "currentSpace:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId)) spaces:\(Spaces.screenSpacesMap)" }
})
workItem = item
queue.asyncAfter(deadline: .now() + humanPerceptionDelay, execute: item)
}
}
enum DebounceContext {
case spaceEvent
case screenEvent
}
+8 -9
View File
@@ -6,14 +6,13 @@ class SpacesEvents {
}
@objc private static func handleEvent(_ notification: Notification) {
Logger.debug { notification.name.rawValue }
// if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
Logger.info { "current space:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId))" }
// Workaround for Safari full-screen videos
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
// for such cases, we refresh isFullscreen on Space change
Windows.updateIsFullscreenOnCurrentSpace()
ScreensEvents.debouncerScreenAndSpace.debounce(.spaceEvent) {
Logger.debug { notification.name.rawValue }
// Workaround for Safari full-screen videos
// when full-screening a video, Safari spawns a second full-screen window called "Safari"
// this window doesn't emit resize/move events. It doesn't pass isActualWindow on creation. It's added on focusedWindowChanged
// for such cases, we refresh isFullscreen on Space change
Windows.updateIsFullscreenOnCurrentSpace()
}
}
}