Skip to content

Desktop: Main thread hangs 2s+ on dock tile update — 11.4K events, 199 users #6194

@beastoin

Description

@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:

  • 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

  1. Batch unreadCount updates — set final count once after processing all messages, not per-message increment
  2. Debounce view updates — use Combine .debounce(for: 0.5) on the publisher or move to explicit notification rather than @published
  3. Move dock tile update off main thread — dispatch NSDockTile.display() asynchronously or use NSApp.dockTile.badgeLabel (lighter weight)
  4. Pause polling when app is backgrounded — only poll when NSApp.isActive
  5. 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

Metadata

Metadata

Assignees

Labels

p1Priority: Critical (score 22-29)ux-polishLayer: UI layout, animations, wording

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions