Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix high CPU usage #1481 #1484

Closed
wants to merge 12 commits into from
2 changes: 1 addition & 1 deletion src/api-wrappers/AXUIElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func retryAxCallUntilTimeout_(_ group: DispatchGroup?, _ timeoutInSeconds: Doubl
} catch {
let timePassedInSeconds = Double(DispatchTime.now().uptimeNanoseconds - startTime.uptimeNanoseconds) / 1_000_000_000
if timePassedInSeconds < timeoutInSeconds {
BackgroundWork.axCallsQueue.asyncAfter(deadline: .now() + .milliseconds(10)) {
BackgroundWork.axCallsQueue.asyncAfter(deadline: .now() + .milliseconds(250)) {
retryAxCallUntilTimeout_(group, timeoutInSeconds, fn, startTime)
}
}
Expand Down
26 changes: 25 additions & 1 deletion src/logic/Applications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Applications {
addInitialRunningApplications()
addInitialRunningApplicationsWindows()
WorkspaceEvents.observeRunningApplications()
WorkspaceEvents.registerFrontAppChangeNote()
}

static func addInitialRunningApplications() {
Expand Down Expand Up @@ -104,7 +105,30 @@ class Applications {
private static func isActualApplication(_ app: NSRunningApplication) -> Bool {
// an app can start with .activationPolicy == .prohibited, then transition to != .prohibited later
// an app can be both activationPolicy == .accessory and XPC (e.g. com.apple.dock.etci)
return (isNotXpc(app) || isAndroidEmulator(app)) && !app.processIdentifier.isZombie()
return (isNotXpc(app) || isAndroidEmulator(app)) && !app.processIdentifier.isZombie() && isAnWindowApplication(app)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If your comment below is correct (that apps that trigger a didActivateApplicationNotification event must have windows), then all these checks are no longer useful for these apps, no?

I imagine that they would only be useful for the wasLaunchedBeforeAltTab == true apps. So maybe we should add that boolean check first (left-side), to avoid the more expansive checks in the case where it's wasLaunchedBeforeAltTab == false, no?

}

private static func isAnWindowApplication(_ app: NSRunningApplication) -> Bool {
if (app.isActive) {
// Because we only add the application when we receive the didActivateApplicationNotification.
// So here is actually the handling for the case wasLaunchedBeforeAltTab=false. For applications where wasLaunchedBeforeAltTab=true, the majority of isActive is false.
// The reason for not using axUiElement.windows() here as a way to determine if it is a window application is that
// When we receive the didActivateApplicationNotification notification, the application may still be loading and axUiElement.windows() will throw an exception
// So we use isActive to determine if it is a window application, even if the application is not frontmost, isActive is still true at this time
return true;
} else {
do {
// For wasLaunchedBeforeAltTab=true, we assume that those apps are all launched, if they are programs with windows.
// Even if it has 0 windows at this point, axUiElement.windows() will not throw an exception. If they are programs without windows, then axUiElement.windows() will throw an exception.
// Here I consider there is an edge case where AltTab is starting up and this program has been loading, then it is possible that axUiElement.windows() will throw an exception.
// I'm not quite sure if this happens, but even if it does, then after restarting this application, AltTab captures its window without any problem. I think this happens rarely.
let axUiElement = AXUIElementCreateApplication(app.processIdentifier)
try axUiElement.windows()
return true
} catch {
return false
}
}
}

private static func isNotXpc(_ app: NSRunningApplication) -> Bool {
Expand Down
23 changes: 21 additions & 2 deletions src/logic/events/WorkspaceEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,30 @@ class WorkspaceEvents {
let diff = Array(workspaceApps.symmetricDifference(previousValueOfRunningApps))
if change.kind == .insertion {
debugPrint("OS event", "apps launched", diff.map { ($0.processIdentifier, $0.bundleIdentifier) })
Applications.addRunningApplications(diff)
} else if change.kind == .removal {
debugPrint("OS event", "apps quit", diff.map { ($0.processIdentifier, $0.bundleIdentifier) })
Applications.removeRunningApplications(diff)
previousValueOfRunningApps = workspaceApps
}
}

static func registerFrontAppChangeNote() {
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(receiveFrontAppChangeNote(_:)), name: NSWorkspace.didActivateApplicationNotification, object: nil)
}

// We add apps when we receive a didActivateApplicationNotification notification, not when we receive an apps launched, because any app will have an apps launched notification.
// But we are only interested in apps that have windows. We think that since an app can be activated, it must have a window, and subscribing to its window event makes sense and is likely to work, even if it requires multiple retries to subscribe.
// I'm not very sure if there is an edge case, but so far testing down the line has not revealed it.
// When we receive the didActivateApplicationNotification notification, NSRunningApplication.isActive=true, even if the app is not the frontmost window anymore.
// If we go to add the application when we receive the message of apps launched, at this time NSRunningApplication.isActive may be false, and try axUiElement.windows() may also throw an exception.
// For those background applications, we don't receive notifications of didActivateApplicationNotification until they have their own window. For example, those menu bar applications.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is true, then your approach here to listen to .didActivateApplicationNotification is genius! Did you think or it yourself or was it used by other projects?

Also, if I summarize this change, it seems you found an API that gives us properly filtered apps (only apps with windows), but that's only for apps launched after AltTab is started. For apps launched before AltTab is started, we still have to filter out processes with activationPolicy, XPC, etc. But luckily, apps launched before AltTab should not throw an exception on .windows().

And then you mention the edge-case of an app launching at the same time as AltTab, which would be annoying. This is actually a very important edge-case, unfortunately. That's what happens when you log into macOS. All the startup apps launch at once, concurrently. This means AltTab is launched, and all sorts of app at the very same time.

I guess for that scenario, didActivateApplicationNotification will not help us, and we will need to rely on the other existing checks (XPC, etc), and with the new 250ms delay, it may be an acceptable situation?

Am I understanding correctly the whole approach?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you think or it yourself or was it used by other projects?

At first I was thinking that when we open an application, as long as they show a window, then a change in window switching is bound to happen, and if we can find a way to observe this change, we will be able to tell that the program is equipped with a window. Then I used some window management software, such as automatic window layout APP, like AppGrid, Rectangle, and found that they can observe this change very well, so I went to study their code, and then found in Rectangle's code that it subscribes to .didActivateApplicationNotification notifications. I was curious, so I wrote some test code to see the effect. Turns out it worked and was able to solve half of the problem, the other half still had to be figured out.

And then you mention the edge-case of an app launching at the same time as AltTab, which would be annoying. This is actually a very important edge-case, unfortunately. That's what happens when you log into macOS. All the startup apps launch at once, concurrently. This means AltTab is launched, and all sorts of app at the very same time.

That's my concern, and I still need to do some testing on this situation.

I guess for that scenario, didActivateApplicationNotification will not help us, and we will need to rely on the other existing checks (XPC, etc), and with the new 250ms delay, it may be an acceptable situation?

This edge case only happens when wasLaunchedBeforeAltTab=true, at which point we go to the axUiElement.windows() test. If it happens that the program takes a long time to load, then axUiElement.windows() will most likely throw an exception, and at that point we do need to detect it by other means (e.g. XPC, etc), otherwise we will lose the window. Of course, reopening the program that lost the window, AltTab will capture its window without any problem.

Copy link
Contributor Author

@metacodes metacodes Apr 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For that edge-case, we probably don't need to restart the applications that are missing windows, we can do a check when we get the .didActivateApplicationNotification notification, and if the application is not in the AltTab list of existing windows, then we add it. We will receive a .didActivateApplicationNotification notification every time a window switch occurs between different applications.

@objc static func receiveFrontAppChangeNote(_ notification: Notification) {
if let application = notification.userInfo?["NSWorkspaceApplicationKey"] as? NSRunningApplication {
debugPrint("OS event", "didActivateApplicationNotification", application.bundleIdentifier)
let workspaceApps = Set(NSWorkspace.shared.runningApplications)
let diff = Array(workspaceApps.symmetricDifference(previousValueOfRunningApps))
Applications.addRunningApplications(diff)
previousValueOfRunningApps = workspaceApps
}
previousValueOfRunningApps = workspaceApps
}
}