Skip to content

Commit 121515c

Browse files
committed
fix: various issues with the app freezing or being slowly
1 parent 8b52dc8 commit 121515c

File tree

10 files changed

+80
-81
lines changed

10 files changed

+80
-81
lines changed

src/logic/Application.swift

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class Application: NSObject {
5454
func removeWindowslessAppWindow() {
5555
if let windowlessAppWindow = (Windows.list.firstIndex { $0.isWindowlessApp == true && $0.application.pid == pid }) {
5656
Windows.list.remove(at: windowlessAppWindow)
57-
App.app.refreshOpenUi([])
57+
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
5858
}
5959
}
6060

@@ -72,9 +72,11 @@ class Application: NSObject {
7272
retryAxCallUntilTimeout(group, 5) { [weak self] in
7373
guard let self = self else { return }
7474
var atLeastOneActualWindow = false
75-
if let axWindows_ = try self.axUiElement!.windows(), axWindows_.count > 0 {
75+
if let axWindows_ = try self.axUiElement!.windows(), !axWindows_.isEmpty {
7676
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
77-
try Array(Set(axWindows_)).forEach { axWindow in
77+
let uniqueWindows = Array(Set(axWindows_))
78+
if uniqueWindows.isEmpty { return }
79+
for axWindow in uniqueWindows {
7880
if let wid = try axWindow.cgWindowId() {
7981
let title = try axWindow.title()
8082
let subrole = try axWindow.subrole()
@@ -96,7 +98,7 @@ class Application: NSObject {
9698
window.position = position
9799
} else {
98100
let window = self.addWindow(axWindow, wid, title, isFullscreen, isMinimized, position, size)
99-
App.app.refreshOpenUi([window])
101+
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
100102
}
101103
}
102104
}
@@ -107,7 +109,7 @@ class Application: NSObject {
107109
DispatchQueue.main.async { [weak self] in
108110
guard let self = self else { return }
109111
if self.addWindowlessWindowIfNeeded() != nil {
110-
App.app.refreshOpenUi([])
112+
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
111113
}
112114
}
113115
// workaround: some apps launch but take a while to create their window(s)

src/logic/Applications.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ class Applications {
8383
Windows.cycleFocusedWindowIndex(-windowsOnTheLeftOfFocusedWindow)
8484
}
8585
if !existingWindowstoRemove.isEmpty {
86-
App.app.refreshOpenUi([])
86+
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
8787
}
8888
}
8989
}

src/logic/BackgroundWork.swift

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@ class BackgroundWork {
1212
static var repeatingKeyThread: BackgroundThreadWithRunLoop!
1313
static var missionControlThread: BackgroundThreadWithRunLoop!
1414
static var cliEventsThread: BackgroundThreadWithRunLoop!
15-
static let screenshotsDispatchGroup = DispatchGroup()
1615

1716
// swift static variables are lazy; we artificially force the threads to init
1817
static func start() {
19-
// screenshots are taken off the main thread, concurrently
20-
screenshotsQueue = DispatchQueue.globalConcurrent("screenshotsQueue", .userInteractive)
18+
// screenshots are taken on a serial DispatchQueue. They used to be taken on the .global() concurrent queue.
19+
// it could hand the app the screenshot OS calls are slow. It would hang or crash with this error:
20+
// >Processes reached dispatch thread soft limit (64)
21+
screenshotsQueue = DispatchQueue.queue("screenshotsQueue", .userInteractive, false)
2122
// calls to act on windows (e.g. AXUIElementSetAttributeValue, AXUIElementPerformAction) are done off the main thread
22-
accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive)
23-
// calls to the AX APIs are blocking. We dispatch those on a globalConcurrent queue
24-
axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive)
23+
accessibilityCommandsQueue = DispatchQueue.queue("accessibilityCommandsQueue", .userInteractive, false)
24+
// calls to the AX APIs can block for a long time (e.g. if an app is unresponsive)
25+
// We can't use a serial queue. We use the global concurrent queue
26+
axCallsQueue = DispatchQueue.queue("axCallsQueue", .userInteractive, true)
2527
// we observe app and windows notifications. They arrive on this thread, and are handled off the main thread initially
2628
accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive)
2729
// we listen to as any keyboard events as possible on a background thread, as it's more available/reliable than the main thread
@@ -37,7 +39,7 @@ class BackgroundWork {
3739
static func startCrashReportsQueue() {
3840
if crashReportsQueue == nil {
3941
// crash reports can be sent off the main thread
40-
crashReportsQueue = DispatchQueue.globalConcurrent("crashReportsQueue", .utility)
42+
crashReportsQueue = DispatchQueue.queue("crashReportsQueue", .utility, false)
4143
}
4244
}
4345

@@ -79,27 +81,14 @@ class BackgroundWork {
7981
}
8082
}
8183

82-
// we cap concurrent tasks to .processorCount to avoid thread explosion on the .global queue
83-
let backgroundWorkGlobalSemaphore = DispatchSemaphore(value: ProcessInfo.processInfo.processorCount)
84-
8584
extension DispatchQueue {
86-
static func globalConcurrent(_ label: String, _ qos: DispatchQoS) -> DispatchQueue {
87-
// label is not reflected in Instruments because the target is .global
88-
// if we want to see our custom labels, we need our private queue.
89-
// However, we want to be efficient and use the OS thread pool, so we use .global
90-
DispatchQueue(label: label, attributes: .concurrent, target: .global(qos: qos.qosClass))
91-
}
92-
93-
func asyncWithCap(_ deadline: DispatchTime? = nil, _ fn: @escaping () -> Void) {
94-
let block = {
95-
fn()
96-
backgroundWorkGlobalSemaphore.signal()
97-
}
98-
backgroundWorkGlobalSemaphore.wait()
99-
if let deadline = deadline {
100-
asyncAfter(deadline: deadline, execute: block)
101-
} else {
102-
async(execute: block)
85+
static func queue(_ label: String, _ qos: DispatchQoS, _ globalParallel: Bool) -> DispatchQueue {
86+
if globalParallel {
87+
// label is not reflected in Instruments because the target is .global
88+
// if we want to see our custom labels, we need our private queue.
89+
// However, we want to be efficient and use the OS thread pool, so we use .global
90+
return DispatchQueue(label: label, attributes: [.concurrent], target: .global(qos: qos.qosClass))
10391
}
92+
return DispatchQueue(label: label, qos: qos)
10493
}
10594
}

src/logic/Window.swift

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,17 @@ class Window {
9797
CFRunLoopAddSource(BackgroundWork.accessibilityEventsThread.runLoop, AXObserverGetRunLoopSource(axObserver), .commonModes)
9898
}
9999

100-
func refreshThumbnail(_ screenshot: NSImage?) {
100+
func refreshThumbnail(_ screenshot: NSImage) {
101101
thumbnail = screenshot
102-
thumbnailFullSize = screenshot?.size
103-
if App.app.appIsBeingUsed && shouldShowTheUser {
104-
if let index = (Windows.list.firstIndex { $0.cgWindowId == cgWindowId }) {
105-
let view = ThumbnailsView.recycledViews[index]
106-
if !view.thumbnail.isHidden {
107-
view.thumbnail.image = thumbnail?.copyToSeparateContexts()
108-
let thumbnailSize = ThumbnailView.thumbnailSize(thumbnail, false)
109-
view.thumbnail.setSize(thumbnailSize)
110-
}
102+
thumbnailFullSize = screenshot.size
103+
if !App.app.appIsBeingUsed || !shouldShowTheUser { return }
104+
if let view = (ThumbnailsView.recycledViews.first { $0.window_?.cgWindowId == cgWindowId }) {
105+
if !view.thumbnail.isHidden {
106+
view.thumbnail.image = thumbnail?.copyToSeparateContexts()
107+
let thumbnailSize = ThumbnailView.thumbnailSize(thumbnail, false)
108+
view.thumbnail.setSize(thumbnailSize)
111109
}
110+
App.app.previewPanel.updateImageIfShowing(cgWindowId, screenshot, screenshot.size)
112111
}
113112
}
114113

@@ -121,7 +120,7 @@ class Window {
121120
NSSound.beep()
122121
return
123122
}
124-
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
123+
BackgroundWork.accessibilityCommandsQueue.async { [weak self] in
125124
guard let self = self else { return }
126125
if self.isFullscreen {
127126
self.axUiElement.setAttribute(kAXFullscreenAttribute, false)
@@ -141,12 +140,12 @@ class Window {
141140
NSSound.beep()
142141
return
143142
}
144-
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
143+
BackgroundWork.accessibilityCommandsQueue.async { [weak self] in
145144
guard let self = self else { return }
146145
if self.isFullscreen {
147146
self.axUiElement.setAttribute(kAXFullscreenAttribute, false)
148147
// minimizing is ignored if sent immediatly; we wait for the de-fullscreen animation to be over
149-
BackgroundWork.accessibilityCommandsQueue.asyncWithCap(.now() + .seconds(1)) { [weak self] in
148+
BackgroundWork.accessibilityCommandsQueue.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
150149
guard let self = self else { return }
151150
self.axUiElement.setAttribute(kAXMinimizedAttribute, true)
152151
}
@@ -161,7 +160,7 @@ class Window {
161160
NSSound.beep()
162161
return
163162
}
164-
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
163+
BackgroundWork.accessibilityCommandsQueue.async { [weak self] in
165164
guard let self = self else { return }
166165
self.axUiElement.setAttribute(kAXFullscreenAttribute, !self.isFullscreen)
167166
}
@@ -186,7 +185,7 @@ class Window {
186185
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
187186
// but quickly switches back to another window in that space
188187
// You can reproduce this buggy behaviour by clicking on the dock icon, proving it's an OS bug
189-
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
188+
BackgroundWork.accessibilityCommandsQueue.async { [weak self] in
190189
guard let self = self else { return }
191190
var psn = ProcessSerialNumber()
192191
GetProcessForPID(self.application.pid, &psn)

src/logic/Windows.swift

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@ class Windows {
273273
lazy var cgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes)
274274
lazy var visibleCgsWindowIds = Spaces.windowsInSpaces(spaceIdsAndIndexes, false)
275275
for window in list {
276-
// TODO: can the CGS call inside detectTabbedWindows introduce latency when WindowServer is busy?
277276
detectTabbedWindows(window, cgsWindowIds, visibleCgsWindowIds)
278277
updatesWindowSpace(window)
279278
refreshIfWindowShouldBeShownToTheUser(window)
@@ -303,38 +302,30 @@ class Windows {
303302
}
304303

305304
// dispatch screenshot requests off the main-thread, then wait for completion
306-
static func refreshThumbnails(_ windows: [Window], _ onlyUpdateScreenshots: Bool) {
305+
static func refreshThumbnails(_ windows: [Window], _ source: RefreshCausedBy) {
307306
var eligibleWindows = [Window]()
308307
for window in windows {
309308
if !window.isWindowlessApp, let cgWindowId = window.cgWindowId, cgWindowId != CGWindowID(bitPattern: -1) {
310309
eligibleWindows.append(window)
311310
}
312311
}
313312
if eligibleWindows.isEmpty { return }
314-
screenshotEligibleWindowsAndRefreshUi(eligibleWindows, onlyUpdateScreenshots)
313+
screenshotEligibleWindowsAndRefreshUi(eligibleWindows, source)
315314
}
316315

317-
private static func screenshotEligibleWindowsAndRefreshUi(_ eligibleWindows: [Window], _ onlyUpdateScreenshots: Bool) {
318-
eligibleWindows.forEach { _ in BackgroundWork.screenshotsDispatchGroup.enter() }
316+
private static func screenshotEligibleWindowsAndRefreshUi(_ eligibleWindows: [Window], _ source: RefreshCausedBy) {
319317
for window in eligibleWindows {
320318
BackgroundWork.screenshotsQueue.async { [weak window] in
321-
backgroundWorkGlobalSemaphore.wait()
322-
defer {
323-
backgroundWorkGlobalSemaphore.signal()
324-
BackgroundWork.screenshotsDispatchGroup.leave()
325-
}
326-
if let cgImage = window?.cgWindowId?.screenshot() {
319+
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
320+
if let wid = window?.cgWindowId, let cgImage = wid.screenshot() {
321+
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
327322
DispatchQueue.main.async { [weak window] in
323+
if source == .refreshOnlyThumbnailsAfterShowUi && !App.app.appIsBeingUsed { return }
328324
window?.refreshThumbnail(NSImage.fromCgImage(cgImage))
329325
}
330326
}
331327
}
332328
}
333-
if !onlyUpdateScreenshots {
334-
BackgroundWork.screenshotsDispatchGroup.notify(queue: DispatchQueue.main) {
335-
App.app.refreshOpenUi([])
336-
}
337-
}
338329
}
339330

340331
static func refreshWhichWindowsToShowTheUser() {

src/logic/events/AccessibilityEvents.swift

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) thro
4242
let window = (appFocusedWindow != nil && wid != nil) ? Windows.updateLastFocus(appFocusedWindow!, wid!)?.first : nil
4343
app.focusedWindow = window
4444
App.app.checkIfShortcutsShouldBeDisabled(window, app.runningApplication)
45-
App.app.refreshOpenUi(window != nil ? [window!] : [])
45+
App.app.refreshOpenUi(window != nil ? [window!] : [], .refreshUiAfterExternalEvent)
4646
}
4747
}
4848
}
@@ -55,7 +55,11 @@ fileprivate func applicationHiddenOrShown(_ pid: pid_t, _ type: String) throws {
5555
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
5656
return $0.application.pid == pid
5757
}
58-
App.app.refreshOpenUi(windows)
58+
// if we process the "shown" event too fast, the window won't be listed by CGSCopyWindowsWithOptionsAndTags
59+
// it will thus be detected as isTabbed. We add a delay to work around this scenario
60+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
61+
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
62+
}
5963
}
6064
}
6165
}
@@ -77,7 +81,7 @@ fileprivate func windowCreated(_ element: AXUIElement, _ pid: pid_t) throws {
7781
let window = Window(element, app, wid, axTitle, isFullscreen, isMinimized, position, size)
7882
Windows.appendAndUpdateFocus(window)
7983
Windows.cycleFocusedWindowIndex(1)
80-
App.app.refreshOpenUi([window])
84+
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
8185
}
8286
}
8387
}
@@ -106,11 +110,11 @@ fileprivate func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) thro
106110
app.focusedWindow = w
107111
}
108112
if let windows = Windows.updateLastFocus(element, wid) {
109-
App.app.refreshOpenUi(windows)
113+
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
110114
} else if AXUIElement.isActualWindow(app, wid, level, axTitle, subrole, role, size) {
111115
let window = Window(element, app, wid, axTitle, isFullscreen, isMinimized, position, size)
112116
Windows.appendAndUpdateFocus(window)
113-
App.app.refreshOpenUi([window])
117+
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
114118
}
115119
}
116120
}
@@ -142,7 +146,7 @@ fileprivate func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
142146
}
143147
if Windows.list.count > 0 {
144148
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(index)
145-
App.app.refreshOpenUi([])
149+
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
146150
} else {
147151
App.app.hideUi()
148152
}
@@ -155,7 +159,7 @@ fileprivate func windowMiniaturizedOrDeminiaturized(_ element: AXUIElement, _ ty
155159
DispatchQueue.main.async {
156160
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
157161
window.isMinimized = type == kAXWindowMiniaturizedNotification
158-
App.app.refreshOpenUi([window])
162+
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
159163
}
160164
}
161165
}
@@ -167,7 +171,7 @@ fileprivate func windowTitleChanged(_ element: AXUIElement) throws {
167171
DispatchQueue.main.async {
168172
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }), newTitle != window.title {
169173
window.title = window.bestEffortTitle(newTitle)
170-
App.app.refreshOpenUi([window])
174+
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
171175
}
172176
}
173177
}
@@ -188,7 +192,7 @@ fileprivate func windowResizedOrMoved(_ element: AXUIElement) throws {
188192
window.isFullscreen = isFullscreen
189193
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
190194
}
191-
App.app.refreshOpenUi([window])
195+
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
192196
}
193197
}
194198
}

src/logic/events/ScreensEvents.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class ScreensEvents {
88
@objc private static func handleEvent(_ notification: Notification) {
99
Logger.debug(notification.name.rawValue)
1010
// a screen added or removed can shuffle windows around Spaces; we refresh them
11-
App.app.refreshOpenUi(Windows.list)
11+
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
1212
Logger.info("screens", NSScreen.screens.map { ($0.uuid() ?? "nil" as CFString, $0.frame) })
1313
Logger.info("spaces", Spaces.screenSpacesMap)
1414
Logger.info("current space", Spaces.currentSpaceIndex, Spaces.currentSpaceId)

src/logic/events/SpacesEvents.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class SpacesEvents {
88
@objc private static func handleEvent(_ notification: Notification) {
99
Logger.debug(notification.name.rawValue)
1010
// if UI was kept open during Space transition, the Spaces may be obsolete; we refresh them
11-
App.app.refreshOpenUi(Windows.list)
11+
App.app.refreshOpenUi(Windows.list, .refreshUiAfterExternalEvent)
1212
Logger.info("current space", Spaces.currentSpaceIndex, Spaces.currentSpaceId)
1313
// from macos 12.2 beta onwards, we can't get other-space windows; grabbing windows when switching spaces mitigates the issue
1414
// also, updating windows on Space transition works around an issue with Safari where its fullscreen windows spawn not in fullscreen.

0 commit comments

Comments
 (0)