Reduce battery drain from OpenAI web extras#529
Reduce battery drain from OpenAI web extras#529cbrane wants to merge 5 commits intosteipete:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f909939f2
ℹ️ 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".
| // Activity Monitor, so only run it on explicit/manual refreshes and submenu-driven stale checks. | ||
| self.syncOpenAIWebState() | ||
| if forceTokenUsage { | ||
| await self.refreshOpenAIDashboardIfNeeded(force: true) |
There was a problem hiding this comment.
Let regular dashboard refreshes honor the new failure cooldown
This call now always passes force: true, and after this change there are no remaining call paths that invoke refreshOpenAIDashboardIfNeeded with force: false (the other call sites in this file also pass true). That means shouldSkipOpenAIWebRefresh/lastOpenAIDashboardAttemptAt never throttle real refresh traffic, so when the dashboard is stale and fetches are failing, repeated user-driven refreshes can still repeatedly load the hidden ChatGPT WebView without cooldown, which undercuts the battery-drain mitigation described in this PR.
Useful? React with 👍 / 👎.
|
Great work on this, @cbrane. The root cause analysis is thorough, and the product decision to default-off the WebView scraping path is the right call. The measured 86% reduction in 12-hr power is impressive. I ran an independent 7.5-hour monitor (1,261 samples at 10s intervals) on v0.17.0 / macOS Tahoe / Apple Silicon that confirms the issue this PR addresses:
The main spike hit at 03:18 and lasted ~2 minutes at 62–65% CPU, then dropped abruptly to 0%. Consistent with a WebView or CLI probe timing out. Spike detail (03:17 – 03:21)Happy to re-run the same monitor after this PR lands to provide a before/after comparison. |
| // Keep the OpenAI web account state in sync with the current Codex identity, but avoid loading the | ||
| // full ChatGPT dashboard on the background timer. That scrape is expensive enough to show up in | ||
| // Activity Monitor, so only run it on explicit/manual refreshes and submenu-driven stale checks. | ||
| self.syncOpenAIWebState() |
There was a problem hiding this comment.
If OpenAI web extras are enabled, what path repopulates the dashboard data after a normal refresh() from app launch, account changes, or relogin? I only see syncOpenAIWebState() here unless this is a forced refresh.
There was a problem hiding this comment.
Intentional: a normal background refresh() no longer repopulates the OpenAI dashboard data on app launch/account-change/relogin by itself. syncOpenAIWebState() only keeps the OpenAI-web state aligned with the current Codex identity (for example, clearing stale dashboard data and marking account changes) so we do not silently spin up the hidden ChatGPT WebView on the timer/startup path.
The repopulation paths are all explicit/foreground now:
- manual Refresh:
refreshNow()->refresh(forceTokenUsage: true)->refreshOpenAIDashboardIfNeeded(force: true) - manual cookie import:
importOpenAIDashboardBrowserCookiesNow()->refreshOpenAIDashboardIfNeeded(force: true) - dashboard submenu open when stale:
menuWillOpen->requestOpenAIDashboardRefreshIfStale(reason: "submenu open")->refreshOpenAIDashboardIfNeeded(force: true)
So after app launch, account change, or relogin, we intentionally wait until the user explicitly refreshes or opens a dashboard-backed submenu before repopulating the dashboard snapshot. That tradeoff is what removes the hidden background ChatGPT tab from the steady-state refresh path. Happy to expand the inline comment if that would make the intent clearer in the code too.
There was a problem hiding this comment.
Thanks, that clarifies the intended tradeoff. I still may be missing the intended entry point, though: I can see refreshNow() in code, but I’m not finding a visible UI action that calls it. In manual testing, the Providers refresh button only hits refreshProvider(...), so the explicit repopulation path doesn’t seem discoverable from the current UI.
There was a problem hiding this comment.
Good catch, you were right that the explicit refresh path was not discoverable enough from the current UI.
I pushed a small follow-up in b6f4adc to close that gap:
- added a visible
Refreshaction back to the main menu, which calls the existing explicit refresh path - routed the Codex Providers-pane refresh button through the full explicit refresh path when OpenAI web extras are enabled
- kept disabled-provider refreshes on the provider-only path
That keeps the battery fix intact by avoiding timer-driven background dashboard scraping, but still gives users an obvious manual way to repopulate the dashboard-only OpenAI web data.
I also added coverage for both pieces:
StatusMenuTestschecks thatRefreshis actually present in the menuProvidersPaneCoverageTestschecks that Codex uses the full explicit refresh path when OpenAI web extras are enabled
Validation:
pnpm checkswift test --filter StatusMenuTests --filter ProvidersPaneCoverageTests./Scripts/compile_and_run.sh
|
@Astro-Han - thank you for running a monitor to help confirm that it addresses the root cause! Took a while and happy to know that you saw results as well. |
…issue # Conflicts: # Sources/CodexBar/UsageStore.swift
|
@ratulsarna in addition to the small follow-up above, I also took care of all the merge conflicts as well. Let me know if this is merge ready, and if not, let me know anything else that I need to adjust! I can get that taken care of. |
…issue # Conflicts: # Tests/CodexBarTests/OpenAIDashboardWebViewCacheTests.swift # Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift # Tests/CodexBarTests/SettingsStoreTests.swift # Tests/CodexBarTests/StatusMenuTests.swift # Tests/CodexBarTests/UsageStoreCoverageTests.swift
|
@ratulsarna there were some issues with CI. I went ahead and fixed those and for now it is all good! |
…issue # Conflicts: # Tests/CodexBarTests/ClaudeOAuthDelegatedRefreshCoordinatorTests.swift
|
@ratulsarna I went ahead and fixed the CI again with another set of conflicts from more commits that were added! If you could re-review and merge ASAP since it fixes a major issue that would be great, or let know if there's anything I need to do. Thanks :) |
Closes #139
Summary
This PR reduces the severe battery drain caused by CodexBar's optional OpenAI web extras.
The root issue was that CodexBar could keep loading and retrying the full
chatgpt.com/codex/settings/usagesingle-page app in a hiddenWKWebView. That is far too expensive for a menu bar app background feature, and in real usage it showed up in Activity Monitor as extreme energy impact.This change keeps the normal Codex card working while making the expensive dashboard-only path safer and less likely to run unintentionally.
Root Cause
The OpenAI web extras feature was implemented by loading
https://chatgpt.com/codex/settings/usagein a hiddenWKWebViewand scraping the hydrated DOM.That caused three problems:
chatgpt.comstill loaded, which kept an offscreen ChatGPT tab around longer than necessary.We also found a separate background-refresh issue where scheduled work still walked broader provider lists than necessary.
What Changed
OpenAI web refresh and WebView lifecycle
about:blankso a hiddenchatgpt.compage is not left active between uses.Provider refresh scope
Product/default behavior
OpenAI web extrasis now off by default for new installs.Manual refresh discoverability
Refreshaction back to the main menu so the explicit refresh path is actually reachable from the UI.Documentation
Why Default-Off Is The Right Product Decision
The OpenAI web extras path is optional and not required for the main Codex experience.
Users still keep the normal Codex data they check every day without the hidden ChatGPT dashboard:
What the optional web extras add is dashboard-only enrichment:
Given the implementation cost of loading a hidden ChatGPT SPA, default-on was hard to defend. Default-off for new installs makes the app safer by default while still preserving the feature for people who explicitly want it.
Measured Before/After
These were real Activity Monitor observations while validating this branch:
Before
CodexBar 12 hr Power:305.16CodexBar Energy Impact:83.2https://chatgpt.comchild process Energy Impact:4608.6After
After running the fixed branch for more than 48 hours:
CodexBar 12 hr Power:41.84CodexBar Energy Impact:0.0That is roughly an
86.3%reduction versus the original12 hr Powerreading.Validation
Passed
pnpm checkswift test --filter OpenAIWebRefreshGateTests --filter OpenAIDashboardWebViewCacheTests --filter OpenAIWebAccountSwitchTests --filter UsageStoreCoverageTests --filter SettingsStoreTests --filter ProviderSettingsDescriptorTests --filter StatusMenuTests86 tests in 7 suites passedswift test --filter StatusMenuTests --filter ProvidersPaneCoverageTests26 tests in 2 suites passed./Scripts/compile_and_run.shBroad suite note
A broad
swift testrun in this environment still ends with unrelated failures outside the menu/OpenAI web refresh path touched here.Files Of Interest
Sources/CodexBar/UsageStore.swiftSources/CodexBar/UsageStore+OpenAIWeb.swiftSources/CodexBar/UsageStore+BackgroundRefresh.swiftSources/CodexBar/MenuDescriptor.swiftSources/CodexBar/PreferencesProvidersPane.swiftSources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swiftSources/CodexBarCore/OpenAIWeb/OpenAIDashboardWebViewCache.swiftSources/CodexBar/SettingsStore.swiftSources/CodexBar/Providers/Codex/CodexProviderImplementation.swiftTests/CodexBarTests/OpenAIWebRefreshGateTests.swiftTests/CodexBarTests/OpenAIDashboardWebViewCacheTests.swiftTests/CodexBarTests/UsageStoreCoverageTests.swiftTests/CodexBarTests/StatusMenuTests.swiftTests/CodexBarTests/ProvidersPaneCoverageTests.swiftdocs/solutions/performance-issues/openai-web-extras-default-off-codexbar-20260307.mdFollow-Up
If OpenAI exposes a stable lightweight endpoint for the dashboard-only extras, the long-term fix would be to replace the hidden WebView scrape with direct HTTP requests using imported cookies, similar to the Claude web path.