perf: fix status item icon observer CPU loop, background web scrapes, fix switcher first-click#1073
perf: fix status item icon observer CPU loop, background web scrapes, fix switcher first-click#1073ptstory wants to merge 4 commits into
Conversation
Add a derived signature guard to observeStoreIconChanges() so updateIcons() only runs when icon-relevant state actually changes. Remove isRefreshing from iconObservationToken — it toggles every refresh cycle and has no bearing on the rendered menu bar icon, causing the observer to fire continuously on the main thread. With both changes, the main dispatch queue path drops from ~38/42 samples in a 5-second spindump to idle between refresh intervals.
…eshes Move refreshOpenAIDashboardIfNeeded() and refreshCreditsIfNeeded() into unstructured tasks for non-forced refreshes so the UI never blocks on slow web scrapes during the regular polling cycle. Forced refreshes (manual pull-to-refresh) still await inline.
The plain NSButton inside CodexAccountSwitcherView was dropping the first click because the menu popover wasn't key. Add acceptsFirstMouse, swallow child hit testing so the parent view handles mouse events directly, and forward mouseDown/mouseUp with press tracking to prevent drag-off-then-release misfires.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9e9fb8d1d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Thanks for this. The direction makes sense: normal refresh should not feel stuck behind slow Codex credits/OpenAI dashboard work, and reducing menu-bar icon churn is worth looking at. I am not comfortable merging this as-is yet:
What would make this mergeable:
I ran:
|
|
Thanks for the detailed review. Agreed on all points. Plan:
I'll convert this PR to draft and link the split follow up PRs here. |
|
Split into three focused PRs per review feedback:
Closing this draft in favor of the focused PRs. |
Override acceptsFirstMouse, swallow child hit testing, and implement mouseDown/mouseUp with correct coordinate space conversion for multi-row account layouts.
Root cause: the runtime-click tests synthesized hit points before NSStackView laid out the switcher rows, so every button still sat at {0,0} and the simulated click always resolved to the first account.
Split from steipete#1073.
* fix: accept first click in Codex account switcher
Override acceptsFirstMouse, swallow child hit testing, and implement mouseDown/mouseUp with correct coordinate space conversion for multi-row account layouts.
Root cause: the runtime-click tests synthesized hit points before NSStackView laid out the switcher rows, so every button still sat at {0,0} and the simulated click always resolved to the first account.
Split from #1073.
* fix: preserve Codex switcher tooltip hit testing
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Remove isRefreshing from iconObservationToken and add observer-side signature guard to skip updateIcons() when icon-relevant state is unchanged. The existing render signature inside applyIcon() avoided redundant icon rendering, but not redundant observer work. This change reduces observer callback churn before render-signature checking is reached. Before: updateIcons() called 10 times per refresh cycle (1 rendered) After: updateIcons() called 6 times per refresh cycle (1 rendered) Split from steipete#1073. Related: steipete#678.
* perf: suppress redundant icon observer callbacks Remove isRefreshing from iconObservationToken and add observer-side signature guard to skip updateIcons() when icon-relevant state is unchanged. The existing render signature inside applyIcon() avoided redundant icon rendering, but not redundant observer work. This change reduces observer callback churn before render-signature checking is reached. Before: updateIcons() called 10 times per refresh cycle (1 rendered) After: updateIcons() called 6 times per refresh cycle (1 rendered) Split from #1073. Related: #678. * docs: update changelog for icon observer perf --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
Summary
Fixes a CPU hot loop where
StatusItemController.observeStoreIconChanges()waspegging a full core at ~99% continuously, even when the rendered menu bar icon
was unchanged. Also backgrounds blocking web scrape work during regular refresh
cycles and fixes a first-click acceptance bug in the Codex account switcher.
Related: #678 — this PR addresses a separate root cause (icon observer loop, not
WebKit scrape) for the same user-facing symptom (CodexBar making the machine slow).
Problem
A 5-second spindump of a stuck CodexBar process (v0.25, build 60) showed 38 of
42 main-thread dispatch samples landing in
observeStoreIconChanges()→updateIcons(). The root issue isUsageStore.iconObservationToken, whichregisters Swift Observation dependencies on 10+ store fields.
isRefreshingtoggles on every refresh cycle regardless of icon state, causing the observer to
re-fire immediately after re-registration.
The existing render-signature caching inside
applyIcon()mitigated redundantimage/title mutation but did not suppress the observer callback itself —
updateIcons()still ran on every fire, computing render inputs, checking menuattachment, and updating animation/blink state.
Separately, the regular refresh cycle was
await-ingrefreshOpenAIDashboardIfNeeded()andrefreshCreditsIfNeeded()inline, whichcould block the main thread for seconds during web scrapes.
Changes
Fix 1 — Icon observer loop (
StatusItemController.swift,UsageStore.swift)isRefreshingfromiconObservationToken(noisy, not icon-relevant).storeIconObservationSignature()that derives a string signature fromactual icon-render inputs (snapshot, status indicator, animation state, display
text, provider visibility).
updateIcons()when the signature isunchanged.
Fix 2 — Background web scrapes (
UsageStore.swift)refreshCreditsIfNeeded()andrefreshOpenAIDashboardIfNeeded()as unstructured background tasks so the UInever blocks on slow network operations.
cancelled. Acceptable for a menu bar app on a 2-minute poll cycle.
Fix 3 — Switcher first-click (
StatusItemController+SwitcherViews.swift)acceptsFirstMouse(for:)→true.mouseDown/mouseUpwith press tracking to prevent drag-offmisfires.
applySelection(_:)to deduplicate the action path.Testing
./Scripts/lint.sh lintswift test --skip-build --filter StatusItemIconObservationSignatureTestsswift test --skip-build --filter CodexManagedOpenAIWebRefreshTestsswift test --skip-build --filter StatusMenuCodexSwitcherTestsswift testcurrently hits a pre-existing unrelated failure inOpenAIDashboardWebViewCacheTests(Preserved page expiry is scheduled without future cache activity).Reproduction
openAIWebAccessEnabled = trueandhistoricalTrackingEnabled = true.spindump <PID> 5confirms hot path inobserveStoreIconChanges().