Skip to content

Commit 5bfd1a6

Browse files
committed
Join gc and new window detection into a single request to MacApp & make it parallel
Reduce thread context switching
1 parent 2b8dab4 commit 5bfd1a6

File tree

8 files changed

+88
-98
lines changed

8 files changed

+88
-98
lines changed

Sources/AppBundle/command/impl/CloseCommand.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ struct CloseCommand: Command {
1212
}
1313
// Access ax directly. Not cool :(
1414
if try await args.quitIfLastWindow.andAsyncMainActor(try await window.macAppUnsafe.getAxWindowsCount() == 1) {
15-
if window.macAppUnsafe.nsApp.terminate() {
16-
window.asMacWindow().macApp.destroy(skipClosedWindowsCache: true)
15+
let app = window.macAppUnsafe
16+
if app.nsApp.terminate() {
17+
for workspace in Workspace.all {
18+
for window in workspace.allLeafWindowsRecursive where window.app.pid == app.pid {
19+
(window as! MacWindow).garbageCollect(skipClosedWindowsCache: true)
20+
}
21+
}
1722
return true
1823
} else {
1924
return io.err("Failed to quit '\(window.app.name ?? "Unknown app")'")

Sources/AppBundle/getNativeFocusedWindow.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ private var focusedApp: (any AbstractApp)? {
1111
return appForTests
1212
} else {
1313
check(appForTests == nil)
14-
return try await NSWorkspace.shared.frontmostApplication?.macApp
14+
if let frontmostApplication = NSWorkspace.shared.frontmostApplication {
15+
return try await MacApp.getOrRegister(frontmostApplication)
16+
} else {
17+
return nil
18+
}
1519
}
1620
}
1721
}

Sources/AppBundle/layout/refresh.swift

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ func runRefreshSession(
1818
func runRefreshSessionBlocking(_ event: RefreshSessionEvent) async throws {
1919
if !TrayMenuModel.shared.isEnabled { return }
2020
try await $refreshSessionEventForDebug.withValue(event) {
21-
try await gc()
21+
try await refresh()
2222
gcMonitors()
23-
try await detectNewAppsAndWindows()
2423

2524
let nativeFocused = try await getNativeFocusedWindow()
2625
if let nativeFocused { try await debugWindowsIfRecording(nativeFocused) }
@@ -87,18 +86,19 @@ func refreshModel() async throws {
8786
}
8887

8988
@MainActor
90-
private func gc() async throws {
89+
private func refresh() async throws {
9190
// Garbage collect terminated apps and windows before working with all windows
92-
MacApp.gcTerminatedApps()
91+
let mapping = try await MacApp.refreshAllAndGetAliveWindowIds(frontmostAppBundleId: NSWorkspace.shared.frontmostApplication?.bundleIdentifier)
92+
let aliveWindowIds = mapping.values.flatMap { $0 }
9393

94-
let frontmostAppBundleId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier
95-
var aliveIds: Set<UInt32> = []
96-
for (_, app) in MacApp.allAppsMap {
97-
aliveIds.formUnion(try await app.gcDeadWindowsAndGetAliveIds(frontmostAppBundleId: frontmostAppBundleId))
98-
}
9994
for window in MacWindow.allWindows {
100-
if !aliveIds.contains(window.windowId) {
101-
window.garbageCollect(skipClosedWindowsCache: false, unregisterAxWindow: false)
95+
if !aliveWindowIds.contains(window.windowId) {
96+
window.garbageCollect(skipClosedWindowsCache: false)
97+
}
98+
}
99+
for (app, windowIds) in mapping {
100+
for windowId in windowIds {
101+
try await MacWindow.getOrRegister(windowId: windowId, macApp: app)
102102
}
103103
}
104104

@@ -171,12 +171,3 @@ private func normalizeContainers() {
171171
workspace.normalizeContainers()
172172
}
173173
}
174-
175-
@MainActor
176-
private func detectNewAppsAndWindows() async throws {
177-
for app in try await detectNewApps() { // todo parallelize
178-
for id in try await app.detectNewWindowsAndGetIds() {
179-
_ = try await MacWindow.getOrRegister(windowId: id, macApp: app as! MacApp)
180-
}
181-
}
182-
}

Sources/AppBundle/tree/AbstractApp.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ protocol AbstractApp: AnyObject, Hashable, AeroAny {
88
var name: String? { get }
99
var execPath: String? { get }
1010
var bundlePath: String? { get }
11-
@MainActor func detectNewWindowsAndGetIds() async throws -> [UInt32]
1211
}
1312

1413
extension AbstractApp {

Sources/AppBundle/tree/MacApp.swift

Lines changed: 53 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ final class MacApp: AbstractApp {
88
/*conforms*/ let pid: Int32
99
/*conforms*/ let bundleId: String?
1010
let nsApp: NSRunningApplication
11-
private let axApp: ThreadGuardedValue<AXUIElement>
1211
let isZoom: Bool
12+
private let axApp: ThreadGuardedValue<AXUIElement>
1313
private let appAxSubscriptions: ThreadGuardedValue<[AxSubscription]> // keep subscriptions in memory
1414
private let windows: ThreadGuardedValue<[UInt32: AxWindow]> = .init([:])
1515
private var thread: Thread?
@@ -34,7 +34,8 @@ final class MacApp: AbstractApp {
3434
}
3535

3636
@MainActor
37-
static func get(_ nsApp: NSRunningApplication) async throws -> MacApp? {
37+
@discardableResult
38+
static func getOrRegister(_ nsApp: NSRunningApplication) async throws -> MacApp? {
3839
// Don't perceive any of the lock screen windows as real windows
3940
// Otherwise, false positive ax notifications might trigger that lead to gcWindows
4041
if nsApp.bundleIdentifier == lockScreenAppBundleId {
@@ -236,68 +237,67 @@ final class MacApp: AbstractApp {
236237
} ?? ""
237238
}
238239

239-
@MainActor func detectNewWindowsAndGetIds() async throws -> [UInt32] {
240-
try await thread?.runInLoop { [axApp, windows, nsApp] job in
241-
guard let newWindows = axApp.threadGuarded.get(Ax.windowsAttr, signpostEvent: nsApp.idForDebug) else { return Array(windows.threadGuarded.keys) }
242-
var result: [UInt32] = []
243-
for window in newWindows {
244-
try job.checkCancellation()
245-
if let windowId = windows.threadGuarded.getOrRegisterAxWindow(window, nsApp)?.windowId {
246-
result.append(windowId)
240+
@MainActor
241+
static func refreshAllAndGetAliveWindowIds(frontmostAppBundleId: String?) async throws -> [MacApp: [UInt32]] {
242+
try await withThrowingTaskGroup(of: Void.self) { group in
243+
// Register new apps
244+
for nsApp in NSWorkspace.shared.runningApplications where nsApp.activationPolicy == .regular {
245+
try group.addTaskOrCancelAll { @Sendable @MainActor in
246+
_ = try await getOrRegister(nsApp)
247+
}
248+
}
249+
try await group.waitForAll()
250+
}
251+
return try await withThrowingTaskGroup(of: (pid_t, [UInt32]).self, returning: [MacApp: [UInt32]].self) { group in
252+
// gc dead apps. refresh underlying windows
253+
for (_, app) in MacApp.allAppsMap {
254+
try group.addTaskOrCancelAll { @Sendable @MainActor in
255+
(app.pid, try await app.refreshAndGetAliveWindowIds(frontmostAppBundleId: frontmostAppBundleId))
256+
}
257+
}
258+
var result: [MacApp: [UInt32]] = [:]
259+
for try await (pid, windowIds) in group {
260+
if let app = allAppsMap[pid] {
261+
result[app] = windowIds
247262
}
248263
}
249264
return result
250-
} ?? []
265+
}
251266
}
252267

253268
@MainActor
254-
func gcDeadWindowsAndGetAliveIds(frontmostAppBundleId: String?) async throws -> Set<UInt32> {
255-
try await thread?.runInLoop { [nsApp, windows] (job) -> Set<UInt32> in
269+
private func refreshAndGetAliveWindowIds(frontmostAppBundleId: String?) async throws -> [UInt32] {
270+
if nsApp.isTerminated {
271+
MacApp.allAppsMap.removeValue(forKey: pid)
272+
thread?.runInLoopAsync { [windows, appAxSubscriptions, axApp] job in
273+
axApp.destroy()
274+
appAxSubscriptions.destroy()
275+
windows.destroy()
276+
CFRunLoopStop(CFRunLoopGetCurrent())
277+
}
278+
thread = nil // Disallow all future job submissions
279+
return []
280+
}
281+
guard let thread else { return [] }
282+
return try await thread.runInLoop { [nsApp, windows, axApp] (job) -> [UInt32] in
283+
var result: [UInt32: AxWindow] = windows.threadGuarded
256284
// Second line of defence against lock screen. See the first line of defence: closedWindowsCache
257285
// Second and third lines of defence are technically needed only to avoid potential flickering
258-
let _windows: [UInt32: AxWindow] = windows.threadGuarded
259-
if frontmostAppBundleId == lockScreenAppBundleId { return Set(_windows.keys) }
260-
let toKeepAlive: [UInt32: AxWindow] = try _windows.filter {
261-
try job.checkCancellation()
262-
return $0.value.ax.containingWindowId(signpostEvent: nsApp.idForDebug) != nil
286+
if frontmostAppBundleId != lockScreenAppBundleId {
287+
result = try result.filter {
288+
try job.checkCancellation()
289+
return $0.value.ax.containingWindowId(signpostEvent: nsApp.idForDebug) != nil
290+
}
263291
}
264-
windows.threadGuarded = toKeepAlive
265-
return Set(toKeepAlive.keys)
266-
} ?? []
267-
}
268292

269-
@MainActor
270-
func unregisterWindow(_ windowId: UInt32) {
271-
thread?.runInLoopAsync { [windows] job in
272-
windows.threadGuarded.removeValue(forKey: windowId)
273-
}
274-
}
275-
276-
@MainActor
277-
static func gcTerminatedApps() {
278-
for app in allAppsMap.values where app.nsApp.isTerminated {
279-
app.destroy(skipClosedWindowsCache: true)
280-
}
281-
}
293+
for window in axApp.threadGuarded.get(Ax.windowsAttr, signpostEvent: nsApp.idForDebug) ?? [] {
294+
try job.checkCancellation()
295+
result.getOrRegisterAxWindow(window, nsApp)
296+
}
282297

283-
@MainActor
284-
func destroy(skipClosedWindowsCache: Bool) {
285-
MacApp.allAppsMap.removeValue(forKey: nsApp.processIdentifier)
286-
for (_, window) in MacWindow.allWindowsMap where window.app.pid == self.pid {
287-
window.garbageCollect(skipClosedWindowsCache: skipClosedWindowsCache, unregisterAxWindow: false)
298+
windows.threadGuarded = result
299+
return Array(result.keys)
288300
}
289-
thread?.runInLoopAsync { [windows, appAxSubscriptions, axApp] job in
290-
axApp.destroy()
291-
appAxSubscriptions.destroy()
292-
windows.destroy()
293-
CFRunLoopStop(CFRunLoopGetCurrent())
294-
}
295-
thread = nil // Disallow all future job submissions
296-
}
297-
298-
private func getThreadOrCancel() throws -> Thread { // todo convert untyped throws to throws across the whole app
299-
if let thread { return thread }
300-
throw CancellationError()
301301
}
302302

303303
@MainActor // todo swift is stupid
@@ -342,6 +342,7 @@ private class AxWindow {
342342
}
343343

344344
extension [UInt32: AxWindow] {
345+
@discardableResult
345346
fileprivate mutating func getOrRegisterAxWindow(_ axWindow: AXUIElement, _ nsApp: NSRunningApplication) -> AxWindow? {
346347
guard let id = axWindow.containingWindowId() else { return nil }
347348
if let existing = self[id] {
@@ -429,7 +430,4 @@ extension NSRunningApplication {
429430
var idForDebug: String {
430431
"PID: \(processIdentifier) ID: \(bundleIdentifier ?? executableURL?.description ?? "")"
431432
}
432-
433-
@MainActor
434-
var macApp: MacApp? { get async throws { try await MacApp.get(self) } }
435433
}

Sources/AppBundle/tree/MacWindow.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class MacWindow: Window {
1616
@MainActor static var allWindows: [MacWindow] { Array(allWindowsMap.values) }
1717

1818
@MainActor
19+
@discardableResult
1920
static func getOrRegister(windowId: UInt32, macApp: MacApp) async throws -> MacWindow {
2021
if let existing = allWindowsMap[windowId] { return existing }
2122
let rect = try await macApp.getAxRect(windowId)
@@ -79,7 +80,7 @@ final class MacWindow: Window {
7980
// skipClosedWindowsCache is an optimization when it's definitely not necessary to cache closed window.
8081
// If you are unsure, it's better to pass `false`
8182
@MainActor
82-
func garbageCollect(skipClosedWindowsCache: Bool, unregisterAxWindow: Bool = true) {
83+
func garbageCollect(skipClosedWindowsCache: Bool) {
8384
if MacWindow.allWindowsMap.removeValue(forKey: windowId) == nil {
8485
return
8586
}
@@ -104,9 +105,6 @@ final class MacWindow: Window {
104105
break // Don't switch back on popup destruction
105106
}
106107
}
107-
if unregisterAxWindow {
108-
macApp.unregisterWindow(windowId)
109-
}
110108
}
111109

112110
@MainActor override var title: String { get async throws { try await macApp.getAxTitle(windowId) ?? "" } }
@@ -119,7 +117,7 @@ final class MacWindow: Window {
119117
}
120118

121119
override func closeAxWindow() {
122-
garbageCollect(skipClosedWindowsCache: true, unregisterAxWindow: false)
120+
garbageCollect(skipClosedWindowsCache: true)
123121
macApp.closeAndUnregisterAxWindow(windowId)
124122
}
125123

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public extension ThrowingTaskGroup {
2+
mutating func addTaskOrCancelAll(priority: TaskPriority? = nil, operation: sending @escaping @isolated(any) () async throws -> ChildTaskResult) throws {
3+
let succ = addTaskUnlessCancelled(priority: priority, operation: operation)
4+
if !succ {
5+
cancelAll()
6+
throw CancellationError()
7+
}
8+
}
9+
}

Sources/AppBundle/util/appBundleUtil.swift

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,6 @@ extension String? {
5959
var isNilOrEmpty: Bool { self == nil || self?.isEmpty == true }
6060
}
6161

62-
@MainActor
63-
func detectNewApps() async throws -> [any AbstractApp] {
64-
if isUnitTest {
65-
return appForTests.asList()
66-
}
67-
var result = [any AbstractApp]()
68-
for _app in NSWorkspace.shared.runningApplications where _app.activationPolicy == .regular {
69-
if let app = try await _app.macApp {
70-
result.append(app)
71-
}
72-
}
73-
return result
74-
}
75-
7662
@MainActor
7763
func terminateApp() -> Never {
7864
NSApplication.shared.terminate(nil)

0 commit comments

Comments
 (0)