Skip to content

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

Open
caezium wants to merge 1 commit into
mainfrom
fix/sentry-hangs
Open

fix(perf): eliminate the main-thread App Hangs (Sentry triage)#83
caezium wants to merge 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.
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