feat: improve performance when many windows are open (closes #5177)

BREAKING CHANGE: major performance improvements
This commit is contained in:
Christian Battaglia authored and lwouis committed 2026-02-11 21:35:18 +01:00
1 parent 4b830e6ba7
commit 5d0a92f3f7
8 files changed
+46 -38

No files matched your search

+3 -3
View File
@@ -147,7 +147,7 @@ class Application: NSObject {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if self.addWindowlessWindowIfNeeded() != nil {
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent([])
}
}
// workaround: some apps launch but take a while to create their window(s)
@@ -175,14 +175,14 @@ class Application: NSObject {
let window = Window(self)
Windows.appendWindow(window)
focusedWindow = nil
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent([])
return window
}
func removeWindowlessAppWindow() {
guard let windowlessAppWindow = (Windows.list.first { $0.isWindowlessApp == true && $0.application.pid == pid }) else { return }
Windows.removeWindows([windowlessAppWindow], false)
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent([])
}
func hideOrShow() {
+1 -1
View File
@@ -76,7 +76,7 @@ class Applications {
// comparing pid here can fail here, as it can be already nil; we use isEqual here to avoid the issue
list.removeAll { $0.runningApplication.isEqual(tApp) }
}
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent([])
}
static func refreshBadgesAsync() {
+1 -1
View File
@@ -112,7 +112,7 @@ class Window {
view.thumbnail.updateContents(screenshot, thumbnailSize)
// if the thumbnail size has changed, we need to refresh the open UI
if newSize {
App.app.refreshOpenUi([], .refreshOnlyThumbnailsAfterShowUi)
App.app.refreshOpenUiAfterExternalEvent([])
}
}
App.app.previewPanel.updateIfShowing(cgWindowId, screenshot, position, size)
+1 -1
View File
@@ -402,7 +402,7 @@ class Windows {
if addWindowlessWindowIfNeeded {
windows.forEach { $0.application.addWindowlessWindowIfNeeded() }
}
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent, windowRemoved: true)
App.app.refreshOpenUiAfterExternalEvent([], windowRemoved: true)
}
}
+5 -5
View File
@@ -47,7 +47,7 @@ class AccessibilityEvents {
App.app.checkIfShortcutsShouldBeDisabled(nil, app)
if let windowless = (Windows.list.first { $0.isWindowlessApp && $0.application.pid == pid }) {
if let windows = Windows.updateLastFocusOrder(windowless) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent(windows)
}
}
}
@@ -62,7 +62,7 @@ class AccessibilityEvents {
// if we process the "shown" event too fast, the window won't be listed by CGSCopyWindowsWithOptionsAndTags
// it will thus be detected as isTabbed. We add a delay to work around this scenario
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent(windows)
}
}
@@ -102,7 +102,7 @@ class AccessibilityEvents {
} else if type == kAXWindowResizedNotification || type == kAXWindowMovedNotification {
windowResizedOrMoved(window)
} else {
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent([window])
}
}
}
@@ -122,12 +122,12 @@ class AccessibilityEvents {
window.application.focusedWindow = window
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
if let windows = Windows.updateLastFocusOrder(window) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent(windows)
}
}
private static func windowResizedOrMoved(_ window: Window) {
window.updateSpacesAndScreen()
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
App.app.refreshOpenUiAfterExternalEvent([window])
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ final class Debouncer {
}
// 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)
App.app.refreshOpenUiAfterExternalEvent(Windows.list)
Logger.info { "screens:\(NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })" }
Logger.info { "currentSpace:\(Spaces.currentSpaceIndex) (id:\(Spaces.currentSpaceId)) spaces:\(Spaces.screenSpacesMap)" }
})
+33 -14
View File
@@ -32,6 +32,8 @@ class App: AppCenterApplication {
private var appCenterDelegate: AppCenterCrash?
// don't queue multiple delayed rebuildUi() calls
private var delayedDisplayScheduled = 0
private var lastRefreshTimeInNanoseconds = DispatchTime.now().uptimeNanoseconds
private var nextRefreshScheduled = false
override init() {
super.init()
@@ -199,24 +201,43 @@ class App: AppCenterApplication {
CGWarpMouseCursorPosition(point)
}
func refreshOpenUi(_ windowsToScreenshot: [Window], _ source: RefreshCausedBy, windowRemoved: Bool = false) {
Windows.refreshThumbnailsAsync(windowsToScreenshot, source, windowRemoved: windowRemoved)
guard appIsBeingUsed else { return }
if source == .refreshUiAfterExternalEvent {
if !Windows.updatesBeforeShowing() { hideUi(); return }
func refreshOpenUiAfterExternalEvent(_ windowsToScreenshot: [Window], windowRemoved: Bool = false) {
Windows.refreshThumbnailsAsync(windowsToScreenshot, .refreshUiAfterExternalEvent, windowRemoved: windowRemoved)
refreshOpenUiWithThrottling {
guard self.appIsBeingUsed else { return }
if !Windows.updatesBeforeShowing() { self.hideUi(); return }
self.refreshUi()
}
guard appIsBeingUsed else { return }
}
func refreshUi() {
guard self.appIsBeingUsed else { return }
Windows.updateSelectedWindow()
guard appIsBeingUsed else { return }
thumbnailsPanel.updateContents()
guard appIsBeingUsed else { return }
guard self.appIsBeingUsed else { return }
self.thumbnailsPanel.updateContents()
guard self.appIsBeingUsed else { return }
Windows.voiceOverWindow() // at this point ThumbnailViews are assigned to the window, and ready
guard appIsBeingUsed else { return }
guard self.appIsBeingUsed else { return }
Windows.previewSelectedWindowIfNeeded()
guard appIsBeingUsed else { return }
guard self.appIsBeingUsed else { return }
Applications.refreshBadgesAsync()
}
func refreshOpenUiWithThrottling( _ block: @escaping () -> Void) {
let timeSinceLastRefreshInSeconds = Float(DispatchTime.now().uptimeNanoseconds - lastRefreshTimeInNanoseconds) / 1_000_000
if timeSinceLastRefreshInSeconds >= 200 {
lastRefreshTimeInNanoseconds = DispatchTime.now().uptimeNanoseconds
block()
return
}
guard !nextRefreshScheduled else { return }
nextRefreshScheduled = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.210) {
self.nextRefreshScheduled = false
self.refreshOpenUiWithThrottling(block)
}
}
func showUiOrCycleSelection(_ shortcutIndex: Int, _ forceDoNothingOnRelease_: Bool) {
forceDoNothingOnRelease = forceDoNothingOnRelease_
Logger.debug { "isFirstSummon:\(self.isFirstSummon) shortcutIndex:\(shortcutIndex)" }
@@ -252,7 +273,7 @@ class App: AppCenterApplication {
guard appIsBeingUsed else { return }
Appearance.update()
guard appIsBeingUsed else { return }
refreshOpenUi([], .showUi)
refreshUi()
guard appIsBeingUsed else { return }
thumbnailsPanel.show()
KeyRepeatTimer.startRepeatingKeyNextWindow()
@@ -345,8 +366,6 @@ extension App: NSApplicationDelegate {
}
enum RefreshCausedBy {
case showUi
case refreshOnlyThumbnailsAfterShowUi
case refreshUiAfterThumbnailsHaveBeenRefreshed
case refreshUiAfterExternalEvent
}
+1 -12
View File
@@ -3,7 +3,6 @@ import Cocoa
class ThumbnailsPanel: NSPanel {
var thumbnailsView = ThumbnailsView()
override var canBecomeKey: Bool { true }
private var didDisplayOnce = false
static var maxPossibleThumbnailSize = NSSize.zero
static var maxPossibleAppIconSize = NSSize.zero
@@ -45,7 +44,6 @@ class ThumbnailsPanel: NSPanel {
}
override func orderOut(_ sender: Any?) {
didDisplayOnce = false
if Preferences.fadeOutAnimation {
NSAnimationContext.runAnimationGroup(
{ _ in animator().alphaValue = 0 },
@@ -56,16 +54,6 @@ class ThumbnailsPanel: NSPanel {
}
}
override func displayIfNeeded() {
super.displayIfNeeded()
if !didDisplayOnce {
didDisplayOnce = true
DispatchQueue.main.async {
Applications.manuallyRefreshAllWindows()
}
}
}
func show() {
updateAppearance()
alphaValue = 1
@@ -134,6 +122,7 @@ extension ThumbnailsPanel: NSWindowDelegate {
// this avoids command+q from quitting AltTab itself, or command+p from printing
DispatchQueue.main.async {
MainMenu.toggle(enabled: false)
Applications.manuallyRefreshAllWindows()
}
}
}