Problem
The desktop app UI freezes for 2+ seconds when the macOS dock tile is updated. This is the second most common Sentry issue:
- OMI-DESKTOP-Y + 5 related clusters
- 11,400+ events across 199+ users
- Stack trace:
WindowDockTileInvalidator.updateDockTile -> NSDockTile display -> CoreDockUpdateWindow -> mach_msg2_trap
The main thread blocks on a synchronous mach message to the Dock server.
Root Cause Analysis
Traced through CrispManager.swift and SidebarView.swift:
1. CrispManager polling triggers cascading view invalidation
CrispManager.swift line 63: A Timer.scheduledTimer fires every 120 seconds on the main thread, calling pollForMessages(). When the API returns results, @Published unreadCount is incremented (line 112).
2. SidebarView observes CrispManager
SidebarView.swift line 83: @ObservedObject private var crispManager = CrispManager.shared. Every unreadCount change invalidates the entire SidebarView — a core structural component containing navigation items, status displays, device connection widgets, and update indicators.
3. View invalidation cascades to dock tile update
When SidebarView re-evaluates, the window content changes, macOS detects the update, and triggers WindowDockTileInvalidator.updateDockTile(). This calls NSDockTile.display() which sends a synchronous mach_msg to the Dock server. If the Dock server is busy, the main thread blocks for 2+ seconds.
4. No debouncing
unreadCount is incremented per-message in a loop (line 112: unreadCount += 1 inside for msg in messages). Multiple increments trigger multiple view invalidations in rapid succession. No debouncing or batching.
5. Polling regardless of app state
The 120-second timer fires regardless of whether:
- User has the app focused
- Window is visible
- New messages actually exist
Proposed Fix
- Batch unreadCount updates — set final count once after processing all messages, not per-message increment
- Debounce view updates — use
Combine .debounce(for: 0.5) on the publisher or move to explicit notification rather than @published
- Move dock tile update off main thread — dispatch
NSDockTile.display() asynchronously or use NSApp.dockTile.badgeLabel (lighter weight)
- Pause polling when app is backgrounded — only poll when
NSApp.isActive
- Decouple SidebarView from CrispManager — SidebarView doesn't need to observe all CrispManager state, only the badge count. Use a derived lightweight binding instead of @ObservedObject on the entire manager
Key Files
desktop/Desktop/Sources/MainWindow/CrispManager.swift — lines 42-68 (timer), 95-112 (unread update)
desktop/Desktop/Sources/MainWindow/SidebarView.swift — line 83 (@ObservedObject)
desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift — SidebarView embedding
desktop/Desktop/Sources/MainWindow/HelpPage.swift — lines 8-11 (isViewingHelp toggle)
by AI for @beastoin
Problem
The desktop app UI freezes for 2+ seconds when the macOS dock tile is updated. This is the second most common Sentry issue:
WindowDockTileInvalidator.updateDockTile->NSDockTile display->CoreDockUpdateWindow->mach_msg2_trapThe main thread blocks on a synchronous mach message to the Dock server.
Root Cause Analysis
Traced through
CrispManager.swiftandSidebarView.swift:1. CrispManager polling triggers cascading view invalidation
CrispManager.swiftline 63: ATimer.scheduledTimerfires every 120 seconds on the main thread, callingpollForMessages(). When the API returns results,@Published unreadCountis incremented (line 112).2. SidebarView observes CrispManager
SidebarView.swiftline 83:@ObservedObject private var crispManager = CrispManager.shared. EveryunreadCountchange invalidates the entire SidebarView — a core structural component containing navigation items, status displays, device connection widgets, and update indicators.3. View invalidation cascades to dock tile update
When SidebarView re-evaluates, the window content changes, macOS detects the update, and triggers
WindowDockTileInvalidator.updateDockTile(). This callsNSDockTile.display()which sends a synchronousmach_msgto the Dock server. If the Dock server is busy, the main thread blocks for 2+ seconds.4. No debouncing
unreadCountis incremented per-message in a loop (line 112:unreadCount += 1insidefor msg in messages). Multiple increments trigger multiple view invalidations in rapid succession. No debouncing or batching.5. Polling regardless of app state
The 120-second timer fires regardless of whether:
Proposed Fix
Combine.debounce(for: 0.5)on the publisher or move to explicit notification rather than @publishedNSDockTile.display()asynchronously or useNSApp.dockTile.badgeLabel(lighter weight)NSApp.isActiveKey Files
desktop/Desktop/Sources/MainWindow/CrispManager.swift— lines 42-68 (timer), 95-112 (unread update)desktop/Desktop/Sources/MainWindow/SidebarView.swift— line 83 (@ObservedObject)desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift— SidebarView embeddingdesktop/Desktop/Sources/MainWindow/HelpPage.swift— lines 8-11 (isViewingHelp toggle)by AI for @beastoin