Skip to content

Fix menu-bar stats popover stuck on loading spinner (#78)#79

Open
iliyami wants to merge 2 commits into
mainfrom
fix/78-menu-stats-stuck-loading
Open

Fix menu-bar stats popover stuck on loading spinner (#78)#79
iliyami wants to merge 2 commits into
mainfrom
fix/78-menu-stats-stuck-loading

Conversation

@iliyami

@iliyami iliyami commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Closes #78.

Problem

The menu bar "Live system stats" popover could hang on its loading spinner forever. The spinner shows whenever stats == nil, and the polling loop only assigned stats after both collect() and measure() returned, serially, with no timeout and no partial rendering. A single blocking syscall on the first iteration pinned the panel at nil with no recovery.

The popover chrome (header, footer, button) still renders, which tells us the main thread is fine and the stuck work is the background collect() actor task. It is a hang, not a crash loop: the helper is an SMAppService login item with no KeepAlive, so a crash would drop the menu bar icon rather than leave it spinning.

Prime suspect: getDiskInfo() read .volumeAvailableCapacityForImportantUsageKey, which computes purgeable space and can block indefinitely on machines with heavy APFS snapshots or Time Machine backups. See the issue for the full evidence trail.

Changes

  • Disk: read free/total via statfs("/") (fast, in-kernel, no purgeable/Spotlight dependency) instead of the "important usage" resource key. New DiskUsage.fromStatfs keeps the byte-scaling and clamping math under test.
  • Polling: fence each collector step with a new withTimeout helper, assign stats the moment collect() returns so the network measurement can never gate the panel, and os_log any slow or timed-out step so a future hang can be pinned to a specific collector from a user's Console without a debug build.
  • Network: guard against NULL ifa_addr in getNetworkBytes(), a latent crash on interfaces that lack a link-layer address.
  • Helper: add withTimeout + TimeoutError to MacCleanKit, with tests.
  • Bump version to 1.11.5.

Defense in depth

The statfs swap removes the known blocker at its source. The per-step timeout is a safety net for any other call that might block in the future, and it degrades to "show what we have" plus a log line rather than a frozen panel. Note that Swift cancellation is cooperative, so the timeout keeps the UI responsive but does not kill a wedged syscall; that is why the disk read also had to be made fundamentally non-blocking.

Testing

  • New TimeoutTests: returns value when fast, throws TimeoutError when slow (verified it returns in ~55ms, not the operation's 10s), propagates the operation's own error.
  • New DiskStatsTests.testFromStatfsMultipliesBlockCountsByBlockSize.
  • Full suite: 521 tests, 0 failures (3 pre-existing skips). check-version-sync.sh, swift build, swift test all green locally.

Field confirmation still wanted

A sample MacCleanMenu 3 from the reporter while the spinner shows would confirm the disk read is the exact blocker. This fix is correct either way, and the new os_log lines will also show which step was slow once they run a build with it.

iliyami added 2 commits June 12, 2026 22:03
The Live system stats popover could hang on its loading spinner forever.
`stats` is only assigned after collect() and measure() both return, serially,
with no timeout and no partial rendering, so a single blocking syscall on the
first iteration pins the panel at nil with no recovery.

Root cause: getDiskInfo() read .volumeAvailableCapacityForImportantUsageKey,
which computes purgeable space and can block indefinitely on machines with
heavy APFS snapshots or Time Machine backups. The popover chrome still renders,
so it is a background-actor hang, not a main-thread block or a crash loop (the
helper is an SMAppService login item with no KeepAlive, so a crash would drop
the menu-bar icon, not leave it spinning).

- Disk: read free/total via statfs (fast, in-kernel) instead of the purgeable
  "important usage" resource key. New DiskUsage.fromStatfs keeps the clamping
  math under test.
- Polling: fence each collector step with withTimeout, assign stats the moment
  collect() returns (network no longer gates the panel), and os_log slow or
  timed-out steps so the culprit is diagnosable from a user's Console.
- Network: guard against NULL ifa_addr in getNetworkBytes (latent crash on
  interfaces lacking a link-layer address).
- Add a withTimeout + TimeoutError helper, with tests.

Bump version to 1.11.5.
Tags with a SemVer prerelease identifier (a hyphen, e.g. v1.11.5-beta.1) now
publish as a GitHub pre-release instead of a stable release:

- Marked --prerelease, so GitHub excludes it from /releases/latest and the
  manual in-app updater (which queries that endpoint) never offers it.
- The Homebrew cask update step is skipped, so brew users are unaffected.
- Tag/version come from the pushed tag, and the beta version is stamped into
  the binary so About/Settings report it without a committed VERSION bump.
- Stable tags (no hyphen) are guarded to match the committed VERSION file so a
  stable release can never drift from the binary's appVersion.

This is the channel used to hand the menu-bar stats fix to a reporter for
testing before it ships to everyone.
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.

Menu bar live stats popover stuck on loading spinner forever

1 participant