fix: cpu and memory leaks (see discussion in #117)
This commit is contained in:
1 parent
b3fb222d7f
commit
52626aa724
6 files changed
+95
-28
No files matched your search
@@ -49,14 +49,40 @@ extension AXUIElement {
|
||||
return attribute(kAXSubroleAttribute, String.self)
|
||||
}
|
||||
|
||||
func subscribeWithRetry(_ axObserver: AXObserver, _ notification: String, _ pointer: UnsafeMutableRawPointer?, _ callback: (() -> Void)? = nil, _ previousResult: AXError? = nil) {
|
||||
let result = AXObserverAddNotification(axObserver, self, notification as CFString, pointer)
|
||||
if result == .success || result == .notificationUnsupported || result == .notificationAlreadyRegistered {
|
||||
callback?()
|
||||
func subscribeWithRetry(_ axObserver: AXObserver, _ notification: String, _ pointer: UnsafeMutableRawPointer?, _ callback: (() -> Void)? = nil, _ runningApplication: NSRunningApplication? = nil, _ wid: CGWindowID? = nil, _ attemptsCount: Int = 0) {
|
||||
if attemptsCount == 0 || attemptsCount % 1000 == 0 {
|
||||
if let runningApplication = runningApplication {
|
||||
// debugPrint("attempt pid", attemptsCount, pid, notification, Applications.appsInSubscriptionRetryLoop.filter { $0.starts(with: String(pid)) })
|
||||
}
|
||||
if let wid = wid {
|
||||
// debugPrint("attempt wid", attemptsCount, wid, notification, Windows.windowsInSubscriptionRetryLoop.filter { $0.starts(with: String(wid)) })
|
||||
}
|
||||
}
|
||||
if let runningApplication = runningApplication, Applications.appsInSubscriptionRetryLoop.first(where: { $0 == String(runningApplication.processIdentifier) + String(notification) }) == nil {
|
||||
// debugPrint("early quit pid", attemptsCount, pid, notification)
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10), execute: {
|
||||
self.subscribeWithRetry(axObserver, notification, pointer, callback, result)
|
||||
if let wid = wid, Windows.windowsInSubscriptionRetryLoop.first(where: { $0 == String(wid) + String(notification) }) == nil {
|
||||
// debugPrint("early quit wid", attemptsCount, wid, notification)
|
||||
return
|
||||
}
|
||||
let result = AXObserverAddNotification(axObserver, self, notification as CFString, pointer)
|
||||
if result == .success || result == .notificationUnsupported || result == .notificationAlreadyRegistered {
|
||||
debugPrint("subbed", attemptsCount, runningApplication, wid, Applications.list.first(where: { $0.runningApplication.processIdentifier == runningApplication?.processIdentifier }))
|
||||
callback?()
|
||||
if let runningApplication = runningApplication {
|
||||
Application.stopSubscriptionRetries(notification, runningApplication)
|
||||
debugPrint("app sub list", Applications.appsInSubscriptionRetryLoop)
|
||||
}
|
||||
if let wid = wid {
|
||||
Window.stopSubscriptionRetries(notification, wid)
|
||||
debugPrint("win sub list", Windows.windowsInSubscriptionRetryLoop)
|
||||
}
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10), execute: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.subscribeWithRetry(axObserver, notification, pointer, callback, runningApplication, wid, attemptsCount + 1)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,20 @@ class Application: NSObject {
|
||||
var axObserver: AXObserver?
|
||||
var isReallyFinishedLaunching = false
|
||||
|
||||
static let notifications = [
|
||||
kAXApplicationActivatedNotification,
|
||||
kAXFocusedWindowChangedNotification,
|
||||
kAXWindowCreatedNotification,
|
||||
kAXApplicationHiddenNotification,
|
||||
kAXApplicationShownNotification,
|
||||
]
|
||||
|
||||
// some apps never finish their subscription retry loop; they should be stopped to avoid infinite loop
|
||||
static func stopSubscriptionRetries(_ notification: String, _ runningApplication: NSRunningApplication) {
|
||||
debugPrint("removeObservers", runningApplication.processIdentifier, runningApplication.bundleIdentifier)
|
||||
Applications.appsInSubscriptionRetryLoop.removeAll { $0 == String(runningApplication.processIdentifier) + String(notification) }
|
||||
}
|
||||
|
||||
init(_ runningApplication: NSRunningApplication) {
|
||||
self.runningApplication = runningApplication
|
||||
super.init()
|
||||
@@ -16,6 +30,14 @@ class Application: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit", runningApplication.processIdentifier, runningApplication.bundleIdentifier)
|
||||
// some apps never finish launching; subscription retries should be stopped to avoid infinite loops
|
||||
Application.notifications.forEach { Application.stopSubscriptionRetries($0, runningApplication) }
|
||||
// some apps never finish launching; observer should be removed to avoid leak
|
||||
removeObserver()
|
||||
}
|
||||
|
||||
func removeObserver() {
|
||||
runningApplication.safeRemoveObserver(self, "isFinishedLaunching")
|
||||
}
|
||||
@@ -53,22 +75,20 @@ class Application: NSObject {
|
||||
private func observeEvents() {
|
||||
guard let axObserver = axObserver else { return }
|
||||
let selfPointer = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
for notification in [
|
||||
kAXApplicationActivatedNotification,
|
||||
kAXFocusedWindowChangedNotification,
|
||||
kAXWindowCreatedNotification,
|
||||
kAXApplicationHiddenNotification,
|
||||
kAXApplicationShownNotification,
|
||||
] {
|
||||
axUiElement!.subscribeWithRetry(axObserver, notification, selfPointer, {
|
||||
for notification in Application.notifications {
|
||||
debugPrint("subscribeWithRetry app", runningApplication.processIdentifier, notification, runningApplication.bundleIdentifier)
|
||||
Applications.appsInSubscriptionRetryLoop.append(String(runningApplication.processIdentifier) + String(notification))
|
||||
axUiElement!.subscribeWithRetry(axObserver, notification, selfPointer, { [weak self] in
|
||||
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
|
||||
// we consider them ready when the first subscription succeeds, and list their windows again at that point
|
||||
guard let self = self else { return }
|
||||
if !self.isReallyFinishedLaunching {
|
||||
self.isReallyFinishedLaunching = true
|
||||
self.observeNewWindows()
|
||||
}
|
||||
})
|
||||
}, runningApplication)
|
||||
}
|
||||
debugPrint("app sub list", Applications.appsInSubscriptionRetryLoop)
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(axObserver), .defaultMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import Cocoa
|
||||
class Applications {
|
||||
static var list = [Application]()
|
||||
static var appsObserver = RunningApplicationsObserver()
|
||||
static var appsInSubscriptionRetryLoop = [String]()
|
||||
|
||||
static func observeNewWindows() {
|
||||
for app in list {
|
||||
@@ -53,12 +54,11 @@ class Applications {
|
||||
|
||||
static func removeRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||
for runningApp in runningApps {
|
||||
guard let app = Applications.list.first(where: { $0.runningApplication.isEqual(runningApp) }) else { continue }
|
||||
Windows.list.removeAll(where: { $0.application.runningApplication.isEqual(runningApp) })
|
||||
// some apps never finish launching; the observer leaks for them without this
|
||||
app.removeObserver()
|
||||
Applications.list.removeAll(where: { $0.runningApplication.isEqual(runningApp) })
|
||||
Windows.list.removeAll(where: { $0.application.runningApplication.isEqual(runningApp) })
|
||||
}
|
||||
debugPrint("app sub list", Applications.appsInSubscriptionRetryLoop)
|
||||
debugPrint("win sub list", Windows.windowsInSubscriptionRetryLoop)
|
||||
guard Windows.list.count > 0 else { (App.shared as! App).hideUi(); return }
|
||||
// TODO: implement of more sophisticated way to decide which thumbnail gets focused on app quit
|
||||
Windows.focusedWindowIndex = 1
|
||||
|
||||
+27
-8
@@ -14,6 +14,18 @@ class Window {
|
||||
var application: Application
|
||||
var axObserver: AXObserver?
|
||||
|
||||
static let notifications = [
|
||||
kAXUIElementDestroyedNotification,
|
||||
kAXTitleChangedNotification,
|
||||
kAXWindowMiniaturizedNotification,
|
||||
kAXWindowDeminiaturizedNotification,
|
||||
]
|
||||
|
||||
static func stopSubscriptionRetries(_ notification: String, _ cgWindowId: CGWindowID) {
|
||||
debugPrint("removeObservers", cgWindowId)
|
||||
Windows.windowsInSubscriptionRetryLoop.removeAll { $0 == (String(cgWindowId) + String(notification)) }
|
||||
}
|
||||
|
||||
init(_ axUiElement: AXUIElement, _ application: Application) {
|
||||
// TODO: make a efficient batched AXUIElementCopyMultipleAttributeValues call once for each window, and store the values
|
||||
self.axUiElement = axUiElement
|
||||
@@ -30,17 +42,22 @@ class Window {
|
||||
observeEvents()
|
||||
}
|
||||
|
||||
deinit {
|
||||
debugPrint("deinit", cgWindowId, title)
|
||||
// some windows never finish launching; subscription retries should be stopped to avoid infinite loops
|
||||
Window.notifications.forEach { Window.stopSubscriptionRetries($0, cgWindowId) }
|
||||
}
|
||||
|
||||
private func observeEvents() {
|
||||
AXObserverCreate(application.runningApplication.processIdentifier, axObserverCallback, &axObserver)
|
||||
guard let axObserver = axObserver else { return }
|
||||
for notification in [
|
||||
kAXUIElementDestroyedNotification,
|
||||
kAXTitleChangedNotification,
|
||||
kAXWindowMiniaturizedNotification,
|
||||
kAXWindowDeminiaturizedNotification,
|
||||
] {
|
||||
axUiElement.subscribeWithRetry(axObserver, notification, nil)
|
||||
for notification in Window.notifications {
|
||||
debugPrint("subscribeWithRetry win", cgWindowId, notification, title)
|
||||
Windows.windowsInSubscriptionRetryLoop.append(String(cgWindowId) + String(notification))
|
||||
axUiElement.subscribeWithRetry(axObserver, notification, nil, nil, nil, cgWindowId)
|
||||
}
|
||||
debugPrint("app sub list", Applications.appsInSubscriptionRetryLoop)
|
||||
debugPrint("win sub list", Windows.windowsInSubscriptionRetryLoop)
|
||||
CFRunLoopAddSource(CFRunLoopGetCurrent(), AXObserverGetRunLoopSource(axObserver), .defaultMode)
|
||||
}
|
||||
|
||||
@@ -56,7 +73,8 @@ class Window {
|
||||
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
|
||||
// but quickly switches back to another window in that space
|
||||
// You can reproduce this buggy behaviour by clicking on the dock icon, proving it's an OS bug
|
||||
DispatchQueues.focusActions.async {
|
||||
DispatchQueues.focusActions.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
var elementConnection = UInt32(0)
|
||||
CGSGetWindowOwner(cgsMainConnectionId, self.cgWindowId, &elementConnection)
|
||||
var psn = ProcessSerialNumber()
|
||||
@@ -115,6 +133,7 @@ private func axObserverCallback(observer: AXObserver, element: AXUIElement, noti
|
||||
private func eventWindowDestroyed(_ app: App, _ element: AXUIElement) {
|
||||
guard let existingIndex = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
Windows.list.remove(at: existingIndex)
|
||||
debugPrint("win sub list", Windows.windowsInSubscriptionRetryLoop)
|
||||
guard Windows.list.count > 0 else { app.hideUi(); return }
|
||||
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(existingIndex)
|
||||
app.refreshOpenUi()
|
||||
|
||||
@@ -4,6 +4,7 @@ class Windows {
|
||||
// order in the array is important: most-recently-used elements are first
|
||||
static var list = [Window]()
|
||||
static var focusedWindowIndex = Array<Window>.Index(0)
|
||||
static var windowsInSubscriptionRetryLoop = [String]()
|
||||
|
||||
static func focusedWindow() -> Window? {
|
||||
return list.count > focusedWindowIndex ? list[focusedWindowIndex] : nil
|
||||
|
||||
+2
-1
@@ -122,7 +122,8 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
Windows.refreshAllThumbnails()
|
||||
Windows.focusedWindowIndex = 0
|
||||
Windows.cycleFocusedWindowIndex(step)
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay, execute: {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay, execute: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.refreshOpenUi()
|
||||
if self.uiWorkShouldBeDone { self.thumbnailsPanel?.show() }
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user