From 690e2f6e856b5cbbc11b009859296e7f92b5e5d0 Mon Sep 17 00:00:00 2001 From: lwouis Date: Sun, 10 Nov 2024 18:37:45 +0100 Subject: [PATCH] chore: review all concurrency in the app --- src/logic/AppCenterCrashes.swift | 4 +--- src/logic/BackgroundWork.swift | 19 ++++++++++++++++++- src/logic/events/DockEvents.swift | 4 +++- src/logic/events/KeyboardEvents.swift | 12 ++++++++---- .../events/RunningApplicationsEvents.swift | 1 + 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/logic/AppCenterCrashes.swift b/src/logic/AppCenterCrashes.swift index 80d8b2e9..7c2a47b8 100755 --- a/src/logic/AppCenterCrashes.swift +++ b/src/logic/AppCenterCrashes.swift @@ -22,15 +22,13 @@ class AppCenterCrash: NSObject, CrashesDelegate { if defaults.string(forKey: "crashPolicy") == nil { defaults.register(defaults: ["crashPolicy": "1"]) } - if BackgroundWork.crashReportsQueue == nil { - BackgroundWork.crashReportsQueue = DispatchQueue.globalConcurrent("crashReportsQueue", .utility) - } } // periphery:ignore func confirmationHandler(_ errorReports: [ErrorReport]) -> Bool { self.initNecessaryFacilities() let shouldSend = checkIfShouldSend() + BackgroundWork.startCrashReportsQueue() BackgroundWork.crashReportsQueue.async { AppCenter.networkRequestsAllowed = shouldSend Crashes.notify(with: shouldSend ? .send : .dontSend) diff --git a/src/logic/BackgroundWork.swift b/src/logic/BackgroundWork.swift index 2a556573..b0ade154 100644 --- a/src/logic/BackgroundWork.swift +++ b/src/logic/BackgroundWork.swift @@ -8,6 +8,7 @@ class BackgroundWork { static var crashReportsQueue: DispatchQueue! static var accessibilityEventsThread: BackgroundThreadWithRunLoop! static var mouseEventsThread: BackgroundThreadWithRunLoop! + static var keyboardEventsThread: BackgroundThreadWithRunLoop! static var systemPermissionsThread: BackgroundThreadWithRunLoop! static var repeatingKeyThread: BackgroundThreadWithRunLoop! static var missionControlThread: BackgroundThreadWithRunLoop! @@ -19,17 +20,33 @@ class BackgroundWork { // swift static variables are lazy; we artificially force the threads to init static func start() { + // TODO: clarify how this works mainQueueConcurrentWorkQueue = DispatchQueue.globalConcurrent("mainQueueConcurrentWorkQueue", .userInteractive) + // calls to act on windows (e.g. AXUIElementSetAttributeValue, AXUIElementPerformAction) are done off the main thread accessibilityCommandsQueue = DispatchQueue.globalConcurrent("accessibilityCommandsQueue", .userInteractive) + // calls to the AX APIs are blocking. We dispatch those on a globalConcurrent queue axCallsQueue = DispatchQueue.globalConcurrent("axCallsQueue", .userInteractive) - crashReportsQueue = DispatchQueue.globalConcurrent("crashReportsQueue", .utility) + // we observe app and windows notifications. They arrive on this thread, and are handled off the main thread initially accessibilityEventsThread = BackgroundThreadWithRunLoop("accessibilityEventsThread", .userInteractive) + // we observe mouse clicks when thumbnailsPanel is open. They arrive on this thread, and are handled off the main thread initially mouseEventsThread = BackgroundThreadWithRunLoop("mouseEventsThread", .userInteractive) + // some instances of events can be handled off the main thread; maybe not worth moving to a background thread + keyboardEventsThread = BackgroundThreadWithRunLoop("keyboardEventsThread", .userInteractive) + // not 100% sure this shouldn't be on the main-thread; it doesn't do anything except dispatch to main.async repeatingKeyThread = BackgroundThreadWithRunLoop("repeatingKeyThread", .userInteractive) + // not 100% sure this shouldn't be on the main-thread; it doesn't do anything except dispatch to main.async missionControlThread = BackgroundThreadWithRunLoop("missionControlThread", .userInteractive) } + static func startCrashReportsQueue() { + if crashReportsQueue == nil { + // crash reports can be sent off the main thread + crashReportsQueue = DispatchQueue.globalConcurrent("crashReportsQueue", .utility) + } + } + static func startSystemPermissionThread() { + // not 100% sure this shouldn't be on the main-thread; it doesn't do anything except dispatch to main.async systemPermissionsThread = BackgroundThreadWithRunLoop("systemPermissionsThread", .utility) } } diff --git a/src/logic/events/DockEvents.swift b/src/logic/events/DockEvents.swift index c36d135f..782382a3 100644 --- a/src/logic/events/DockEvents.swift +++ b/src/logic/events/DockEvents.swift @@ -27,5 +27,7 @@ class DockEvents { fileprivate let handleEvent: AXObserverCallback = { _, _, notificationName, _ in logger.d(notificationName) - MissionControl.setState(MissionControlState(rawValue: notificationName as String)!) + DispatchQueue.main.async { + MissionControl.setState(MissionControlState(rawValue: notificationName as String)!) + } } diff --git a/src/logic/events/KeyboardEvents.swift b/src/logic/events/KeyboardEvents.swift index ccf430da..fb7fb796 100644 --- a/src/logic/events/KeyboardEvents.swift +++ b/src/logic/events/KeyboardEvents.swift @@ -71,6 +71,7 @@ class KeyboardEvents { addCgEventTapForModifierFlags() } + // TODO: handle this on a background thread? private static func addLocalMonitorForKeyDownAndKeyUp() { NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .keyUp]) { (event: NSEvent) in let someShortcutTriggered = handleEvent(nil, nil, event.type == .keyDown ? UInt32(event.keyCode) : nil, cocoaToCarbonFlags(event.modifierFlags), event.type == .keyDown ? event.isARepeat : false) @@ -91,7 +92,7 @@ class KeyboardEvents { userInfo: nil) if let eventTap = eventTap { let runLoopSource = CFMachPortCreateRunLoopSource(nil, eventTap, 0) - CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .commonModes) + CFRunLoopAddSource(BackgroundWork.keyboardEventsThread.runLoop, runLoopSource, .commonModes) } else { App.app.restart() } @@ -145,10 +146,13 @@ fileprivate func handleEvent(_ id: EventHotKeyID?, _ shortcutState: ShortcutStat fileprivate let cgEventFlagsChangedHandler: CGEventTapCallBack = {_, type, cgEvent, _ in if type == .flagsChanged { - let modifiers = cocoaToCarbonFlags(NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))) - handleEvent(nil, nil, nil, modifiers, false) + DispatchQueue.main.async { + let modifiers = cocoaToCarbonFlags(NSEvent.ModifierFlags(rawValue: UInt(cgEvent.flags.rawValue))) + handleEvent(nil, nil, nil, modifiers, false) + } } else if (type == .tapDisabledByUserInput || type == .tapDisabledByTimeout) { CGEvent.tapEnable(tap: eventTap!, enable: true) } - return Unmanaged.passUnretained(cgEvent) // focused app will receive the event + // we always return this because we want to let these event pass through to the currently focused app + return Unmanaged.passUnretained(cgEvent) } diff --git a/src/logic/events/RunningApplicationsEvents.swift b/src/logic/events/RunningApplicationsEvents.swift index 6d9d7f41..b5f6f48a 100644 --- a/src/logic/events/RunningApplicationsEvents.swift +++ b/src/logic/events/RunningApplicationsEvents.swift @@ -9,6 +9,7 @@ class RunningApplicationsEvents { appsObserver = NSWorkspace.shared.observe(\.runningApplications, options: [.old, .new], changeHandler: handleEvent) } + // TODO: handle this on a separate thread? private static func handleEvent(_: NSWorkspace, _ change: NSKeyValueObservedChange) { let workspaceApps = Set(NSWorkspace.shared.runningApplications) // TODO: symmetricDifference has bad performance