Skip to content

fix(perf): pre-warm software-list icons off-main (Sentry BURROW-20)#87

Merged
caezium merged 1 commit into
mainfrom
fix/software-list-hang
Jun 17, 2026
Merged

fix(perf): pre-warm software-list icons off-main (Sentry BURROW-20)#87
caezium merged 1 commit into
mainfrom
fix/software-list-hang

Conversation

@caezium

@caezium caezium commented Jun 17, 2026

Copy link
Copy Markdown
Owner

What

BURROW-20 ("App Hanging" / top frame InstalledApp) — the Uninstall/Software list stalls the main thread on first paint of a large app list.

Root cause

AppRow renders each row by calling SoftwareIcons.icon(app.path) and SoftwareIcons.version(app.path) in the row body, on the main thread:

  • icon()NSWorkspace.shared.icon(forFile:) reads the app bundle
  • version() reads each bundle's Contents/Info.plist

The cache filled on first access, so the first paint of a full /Applications list did O(rows) disk reads on the main thread during layout → an App Hang. Same class as the process-table app-icon walk fixed in #83, but a different surface that PR didn't cover.

Fix

  • SoftwareIcons is now NSLock-guarded and resolves icons + versions on a .utility queue. icon()/version() are main-safe cache reads — a hit returns immediately; a miss returns a generic placeholder / nil and schedules an off-main fill (the row picks it up on the next redraw).
  • fetch() pre-warms the cache off-main (it always runs on a background queue), so rows render from pure cache reads — no disk on main during layout.
  • InstalledApp: Equatable so unchanged rows diff cheaply.

Verification

  • xcodebuild … buildBUILD SUCCEEDED, no new warnings.
  • Hand-test: open Uninstall on a machine with many apps — the list should paint without the multi-second stall; icons/versions are already filled (pre-warmed).

Notes

Kept separate from #83 (process table / clean reports) to keep each PR focused. Don't auto-resolve BURROW-20 in Sentry until a release carrying this has soaked — the top hangs regressed after a premature resolve last time.

The uninstall/software list rendered each AppRow by calling
SoftwareIcons.icon()/version() in the row body on the main thread:
NSWorkspace.icon(forFile:) hits the app bundle and version() reads
Info.plist, so the first paint of a large /Applications list did
O(rows) disk reads on the main thread during layout — an App Hang
(Sentry BURROW-20, top frame "InstalledApp").

- SoftwareIcons is now NSLock-guarded and resolves icons + versions on
  a utility queue; icon()/version() are main-safe cache reads that
  return a placeholder/nil and fill asynchronously on a miss.
- fetch() pre-warms the cache off-main (it always runs on a background
  queue), so rows render from a pure cache read.
- InstalledApp is Equatable so unchanged rows diff cheaply.

Mirrors the off-main app-icon resolution for the process table in #83.
@caezium caezium merged commit 63ccd8b into main Jun 17, 2026
1 check passed
@caezium caezium deleted the fix/software-list-hang branch June 17, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant