Skip to content

fix(perf): eliminate the main-thread App Hangs (Sentry triage)#83

Merged
caezium merged 1 commit into
mainfrom
fix/sentry-hangs
Jun 17, 2026
Merged

fix(perf): eliminate the main-thread App Hangs (Sentry triage)#83
caezium merged 1 commit into
mainfrom
fix/sentry-hangs

Conversation

@caezium

@caezium caezium commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What

Every unresolved Burrow Sentry issue is an AppHang (main thread blocked ≥2 s) — zero real crashes. The 40 issues are ~5 root causes wearing different top-frame hats; the two dominant buckets (BURROW-1 layout, BURROW-N generic app.run()) both regressed after a prior 0.7.1 resolve. This PR fixes the structural root causes.

Changes

A. App icons off the main thread — StatusView.swift (BURROW-R, BURROW-T)

AppIcon.image(for:) walked NSWorkspace.runningApplications on the main thread on every cache miss, once per process row, each 2 s refresh — hundreds of O(running-apps) walks. Now:

  • AppIcon.resolve(for:) runs inside the existing off-main process pass, walking the app list at most once and indexing it (O(apps + procs), not O(apps × procs)).
  • Views read cachedImage(for:) — a pure, lock-guarded cache read; glyph fallback until the icon lands.
  • The popup's handful of rows use image(for:) = cache read + async off-main fill.
  • The main thread never walks the app list again.

B. Bounded process table — StatusView.swift, MoleStatus.swift (BURROW-1 + layout tail)

The table's ForEach iterated the full process set (hundreds), rebuilding/diffing every identity each tick and forcing repeated sizeThatFits over the nested ScrollViews (the ScrollViewLayoutComputer → placeChildren recursion in the trace). Now:

  • ForEach is capped to the top 100 rows by the current sort, with a "Show all (N more)" affordance.
  • ProcRow has a fixed .frame(height: 30) so LazyVStack skips per-child measurement and the ScrollView size cache stays stable.
  • ProcessInfo is now Equatable so unchanged rows don't re-render.

C. Throttled clean/optimize report — OperationFlow.swift (BURROW-1G, BURROW-1F)

OperationFlow (@MainActor) re-parsed the whole accumulated transcript (parseTaskReport/mergeSummaryFields) on the main actor for every streamed line — O(n²) over a long run. The live re-parse is now throttled to ~4×/s; terminal events still do a final, authoritative reduce, so the result screen is never stale.

Audited — no change needed

  • Modals (BURROW-1K): every NSAlert.runModal() already routes through runModalQuiet(), which pauses Sentry ANR tracking for user-paced modals.
  • Blocking waits: the disk-scan (group.wait) and CLI-process (waitUntilExit) calls run off-main.
  • Sparklines (BURROW-1M): MiniChart already caps to 120 drawn points; this fingerprint is downstream of the main-thread pressure that A/B/C relieve.
  • The framework-frame singletons (CharacterSet, swift_slowAlloc, _platform_memmove, chkstk, AGGraph…) are symptoms of a busy main thread and should shrink as a side effect — not worth chasing individually.

Test plan

  • xcodebuild Debug build succeeds, no new warnings.
  • Hand-test: open the window with many processes; confirm scrolling/refresh stays smooth, "Show all" works, icons appear.
  • Instruments (Hangs + Time Profiler): confirm main-thread time per 2 s tick drops sharply.
  • Run a long mo clean and confirm the live report still updates and the final result is correct.

Notes

  • ⚠️ Do not auto-resolve the Sentry issues until a release soaks — the top two regressed after a prior resolve; let production confirm.
  • Branched off origin/main. Companion PR (customizable menu-bar metrics, 关于菜单栏可以自定义指标的建议 #82) to follow.

…ean reports

Every unresolved Burrow Sentry issue is an AppHang (main thread blocked >=2s), not a crash. This addresses the structural root causes behind the top buckets:

- App icons (BURROW-R/T): AppIcon walked NSWorkspace.runningApplications on the MAIN thread on every cache miss, once per process row, each 2s refresh - hundreds of O(running-apps) walks. Now resolved off-main in the existing process pass (AppIcon.resolve) which walks the app list at most once; views read a pure cache (cachedImage). The popup's few rows use a cache+async-fill path. The main thread never walks the app list.

- Process table (BURROW-1, the regressed #1, + layout-frame tail): the table's ForEach iterated the FULL process set (hundreds), rebuilding/diffing every identity each tick and forcing repeated sizeThatFits over the nested ScrollViews. Cap the ForEach to the top 100 by the current sort (with a 'Show all' affordance), fix ProcRow at 30pt so LazyVStack skips child measurement and the size cache stays stable, and make ProcessInfo Equatable so unchanged rows don't re-render.

- Clean/optimize reports (BURROW-1G/1F): OperationFlow is @mainactor and re-parsed the whole accumulated transcript (parseTaskReport/mergeSummaryFields) on the main actor for EVERY streamed line - O(n^2). Throttle the live re-parse to ~4x/s; terminal events still do a final authoritative reduce.

Audited the rest: all NSAlert.runModal() already route through runModalQuiet() (pauses Sentry ANR tracking during user-paced modals); the disk-scan and CLI-process waits run off-main; sparklines are already capped at 120 drawn points (BURROW-1M is downstream of the main-thread pressure the above fix). The framework-frame singletons (CharacterSet, swift_slowAlloc, etc.) are symptoms that shrink as a side effect.

Verified: xcodebuild Debug build succeeds with no new warnings. Do NOT auto-resolve the Sentry issues until a release soaks - the top two regressed after a prior resolve.
@caezium caezium merged commit d99be0e into main Jun 17, 2026
1 check passed
@caezium caezium deleted the fix/sentry-hangs branch June 17, 2026 09:15
pull Bot pushed a commit to CrazyForks/Burrow that referenced this pull request Jun 17, 2026
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 caezium#83.
pull Bot pushed a commit to gclm/Burrow that referenced this pull request Jun 17, 2026
…m#82)

Replaces the fixed "42% · 5.2G" metrics string with a configurable, reorderable row of metric widgets, the way a desktop system monitor lets you pick what to glance at.

- Store.menuBarItems: an ordered [MenuBarItem] (metric + style + colour), JSON-persisted. Defaults to the historical CPU + memory pair, and only ever shows in Metrics mode (which stays opt-in - the default is still the icon), so nothing changes for existing users until they customise.

- MenuBarWidgets.swift (new): MenuBarMetric (CPU/RAM/GPU/disk/net/IO/fan/temp/battery), MenuBarWidgetStyle (value / label+value / bar / sparkline / speed-down-up / battery glyph), MenuBarColorMode (utilization/accent/mono/pressure), and MenuBarRenderer which draws the whole row into an NSImage via a drawing handler so monochrome text tracks the light/dark menu bar. Re-implemented in our own MIT Swift + Brand tokens.

- StatusBarController renders the row from the live feed: a snapshot sink (CPU/RAM/GPU/disk/fan/temp/battery + cpu/mem/gpu sparkline rings) and a 1 Hz sink (live net/disk rates + their sparklines off the ring). Drawing is a few strings + shapes; it never walks the running-app list or does other blocking work on main - consistent with the hang fixes in caezium#83.

- SettingsView > Menu Bar gains a metrics editor (add / remove / reorder, per-metric style + colour pickers) shown when Display = Metrics; edits persist and re-render the status item live.

Build: xcodebuild Debug succeeds, no new warnings. Satisfies caezium#82.
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