Skip to content

Reduce battery drain from OpenAI web extras#529

Open
cbrane wants to merge 5 commits intosteipete:mainfrom
cbrane:fix-battery-draining-issue
Open

Reduce battery drain from OpenAI web extras#529
cbrane wants to merge 5 commits intosteipete:mainfrom
cbrane:fix-battery-draining-issue

Conversation

@cbrane
Copy link

@cbrane cbrane commented Mar 14, 2026

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/usage single-page app in a hidden WKWebView. 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/usage in a hidden WKWebView and scraping the hydrated DOM.

That caused three problems:

  1. Failed dashboard refresh attempts were not throttled the same way successful attempts were, so stale cookies or login-required states could keep triggering repeated expensive work.
  2. Hidden WebViews could stay alive with chatgpt.com still loaded, which kept an offscreen ChatGPT tab around longer than necessary.
  3. The feature was enabled by default even though it is optional and significantly heavier than the normal Codex data path.

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

  • Added a refresh gate so recent failed OpenAI dashboard attempts are throttled too, not only successful snapshots.
  • Removed timer-driven background OpenAI dashboard scraping from the normal refresh cycle.
  • Kept OpenAI web sync for account-state tracking, but only run the dashboard scrape on explicit/manual refresh paths and submenu-driven stale checks.
  • Evict cached OpenAI dashboard WebViews on login-required, account-mismatch, and reset/error paths.
  • Blank released cached WebViews to about:blank so a hidden chatgpt.com page is not left active between uses.
  • Reduced the hidden WebView cache lifetime from 10 minutes to 60 seconds.

Provider refresh scope

  • Background refresh/status work now only runs for enabled providers.
  • Token-cost refresh sequencing now only runs for enabled providers.
  • Disabled-provider state is actively cleared so stale provider data does not keep hanging around.

Product/default behavior

  • OpenAI web extras is now off by default for new installs.
  • Existing users with explicit Codex cookie/web configuration are preserved so upgrades do not silently break them.
  • The settings copy now makes the battery/network tradeoff explicit.
  • OpenAI web submenus are hidden when the feature is disabled, even if stale dashboard data exists in memory.

Manual refresh discoverability

  • Added a visible Refresh action back to the main menu so the explicit refresh path is actually reachable from the UI.
  • Routed the Codex Providers-pane refresh button through the full explicit refresh path when OpenAI web extras are enabled, so users can intentionally repopulate dashboard-only data without bringing back timer-driven background scraping.
  • Kept disabled-provider refreshes scoped to the provider-only path.

Documentation

  • Updated Codex/provider docs to describe OpenAI web extras as optional and off by default.
  • Added a troubleshooting/writeup document capturing the product rationale for this default change.

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:

  • session usage
  • weekly usage
  • reset timers
  • account email
  • plan label
  • normal credits remaining

What the optional web extras add is dashboard-only enrichment:

  • code review remaining
  • usage breakdown
  • credits history
  • dashboard purchase link / fallback 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.16
  • CodexBar Energy Impact: 83.2
  • Hidden https://chatgpt.com child process Energy Impact: 4608.6

After

After running the fixed branch for more than 48 hours:

  • CodexBar 12 hr Power: 41.84
  • CodexBar Energy Impact: 0.0

That is roughly an 86.3% reduction versus the original 12 hr Power reading.

Validation

Passed

  • pnpm check
  • swift test --filter OpenAIWebRefreshGateTests --filter OpenAIDashboardWebViewCacheTests --filter OpenAIWebAccountSwitchTests --filter UsageStoreCoverageTests --filter SettingsStoreTests --filter ProviderSettingsDescriptorTests --filter StatusMenuTests
    • result: 86 tests in 7 suites passed
  • swift test --filter StatusMenuTests --filter ProvidersPaneCoverageTests
    • result: 26 tests in 2 suites passed
  • ./Scripts/compile_and_run.sh

Broad suite note

A broad swift test run in this environment still ends with unrelated failures outside the menu/OpenAI web refresh path touched here.

Files Of Interest

  • Sources/CodexBar/UsageStore.swift
  • Sources/CodexBar/UsageStore+OpenAIWeb.swift
  • Sources/CodexBar/UsageStore+BackgroundRefresh.swift
  • Sources/CodexBar/MenuDescriptor.swift
  • Sources/CodexBar/PreferencesProvidersPane.swift
  • Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardFetcher.swift
  • Sources/CodexBarCore/OpenAIWeb/OpenAIDashboardWebViewCache.swift
  • Sources/CodexBar/SettingsStore.swift
  • Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift
  • Tests/CodexBarTests/OpenAIWebRefreshGateTests.swift
  • Tests/CodexBarTests/OpenAIDashboardWebViewCacheTests.swift
  • Tests/CodexBarTests/UsageStoreCoverageTests.swift
  • Tests/CodexBarTests/StatusMenuTests.swift
  • Tests/CodexBarTests/ProvidersPaneCoverageTests.swift
  • docs/solutions/performance-issues/openai-web-extras-default-off-codexbar-20260307.md

Follow-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.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@Astro-Han
Copy link
Contributor

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:

Metric Average Peak
CPU 1.9% 65.2%
Energy Impact 1.9 65.2
Memory 131 MB 184 MB
Samples with CPU > 10% 28 / 1,261 (2.2%)

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)
03:17:58, 0.0%, 121MB
03:18:21, 23.3%, 182MB  ← ramp up
03:18:33, 64.9%, 182MB  ← peak starts
03:19:20, 65.2%, 130MB  ← highest
03:20:06, 63.7%, 131MB  ← peak ends
03:20:27, 0.2%, 130MB   ← abrupt drop

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()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Refresh action 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:

  • StatusMenuTests checks that Refresh is actually present in the menu
  • ProvidersPaneCoverageTests checks that Codex uses the full explicit refresh path when OpenAI web extras are enabled

Validation:

  • pnpm check
  • swift test --filter StatusMenuTests --filter ProvidersPaneCoverageTests
  • ./Scripts/compile_and_run.sh

@cbrane
Copy link
Author

cbrane commented Mar 14, 2026

@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.

@cbrane
Copy link
Author

cbrane commented Mar 15, 2026

@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.

@cbrane cbrane requested a review from ratulsarna March 16, 2026 03:17
…issue

# Conflicts:
#	Tests/CodexBarTests/OpenAIDashboardWebViewCacheTests.swift
#	Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift
#	Tests/CodexBarTests/SettingsStoreTests.swift
#	Tests/CodexBarTests/StatusMenuTests.swift
#	Tests/CodexBarTests/UsageStoreCoverageTests.swift
@cbrane
Copy link
Author

cbrane commented Mar 16, 2026

@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
@cbrane
Copy link
Author

cbrane commented Mar 18, 2026

@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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

consuming too much power on my macbook

3 participants