Skip to content

Decode provider status feeds off the main actor#1406

Merged
steipete merged 2 commits into
steipete:mainfrom
ProspectOre:perf/google-status-decode-off-main
Jun 11, 2026
Merged

Decode provider status feeds off the main actor#1406
steipete merged 2 commits into
steipete:mainfrom
ProspectOre:perf/google-status-decode-off-main

Conversation

@ProspectOre

@ProspectOre ProspectOre commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Decode provider status feeds off the main actor and reuse shared ISO8601 formatters, removing a 150–340ms main-thread stall per Google-status provider per refresh.

Context

Credit to @kcharlan's instrumentation in #1399 (finding 4): with status checks on, UsageStore.parseGoogleWorkspaceStatus decodes the full https://www.google.com/appsstatus/dashboard/incidents.json payload (hundreds of kilobytes live) on the MainActor, allocating a fresh ISO8601DateFormatter per decoded date field. Their Instruments capture measured 150–340ms of main-thread stall per decode, once per Google-status provider (Gemini and Antigravity) per refresh — and refreshes fire during menu interaction. The single flagged microhang across their sessions was exactly this stack. The payload is live data, so the cost grows whenever Google has a busy incident week, on all app versions simultaneously.

The same shape applies to the statuspage path (fetchStatus), which is also MainActor-isolated with per-field formatter allocation; it is included for consistency.

Change

  • fetchStatus and fetchWorkspaceStatus are explicitly @concurrent nonisolated, so feed fetching and decoding stay on the concurrent executor even if the package later adopts caller-inherited nonisolated async behavior. The pure parse helpers remain nonisolated; the caller (refreshStatus) already hops back to the main actor to publish results.
  • Per-date-field ISO8601DateFormatter() allocations are replaced with shared lock-guarded formatters (StatusISO8601FormatterBox), mirroring the existing CostUsageISO8601FormatterBox pattern in CodexBarCore.

Validation

  • swift test --filter GoogleWorkspaceStatus — 4 tests pass, including a regression guard that records the executor immediately before JSON decoding begins.
  • swift test — 3,532 tests in 402 suites pass.
  • make check — 0 violations; git diff --check clean.
  • autoreview — no accepted/actionable findings.

Runtime Proof

Live-feed artifact (terminal capture; decode of the real https://www.google.com/appsstatus/dashboard/incidents.json payload fetched June 10, 2026 — 420,521 bytes, matching the ~418 KB measured in #1399; 5 iterations per strategy, same process, this branch's packaged toolchain):

LIVE_FEED_PROOF payload_bytes=420521
LIVE_FEED_PROOF before(per-field formatters) ms=[69.9, 69.1, 69.4, 68.0, 71.1]
LIVE_FEED_PROOF after(shared formatters, full parse) ms=[25.8, 22.3, 22.1, 21.7, 21.6]
LIVE_FEED_PROOF gemini_status_indicator=Optional(CodexBar.ProviderStatusIndicator.major)

~3.1× faster on the live payload (which happened to carry a real active Gemini incident that parsed correctly through the new path), and the entire cost now lands on a background executor instead of the main thread. The committed regression observes the exact pre-decode boundary rather than the actor-backed transport stub:

Test "fetchWorkspaceStatus decodes off the main thread when called from the main actor" passed
Test run with 4 tests in 2 suites passed

Synthetic stress check (699 KB fixture, 300 incidents x 4 dated updates, 10 iterations): 332.6–356.4 ms → 101.2–104.3 ms per decode, same ratio. The #1399 Instruments measurement (150–340 ms live) sits between these two payload sizes, consistent with both.

Honesty / scope notes

Timing harness (drop into Tests/CodexBarTests to reproduce)
import Foundation
import Testing
@testable import CodexBar

struct StatusDecodeTimingHarness {
    @Test
    func `workspace status decode timing harness`() throws {
        // Synthetic feed shaped like incidents.json: ~300 incidents, each with several
        // updates and date fields, sized to match the measured live payload (~420 KB).
        var incidents: [[String: Any]] = []
        for index in 0..<300 {
            var updates: [[String: Any]] = []
            for u in 0..<4 {
                updates.append([
                    "when": "2026-05-\(String(format: "%02d", (index % 27) + 1))T0\(u):15:00+00:00",
                    "status": "SERVICE_INFORMATION",
                    "text": String(repeating: "status update body text ", count: 12),
                ])
            }
            incidents.append([
                "begin": "2026-05-\(String(format: "%02d", (index % 27) + 1))T00:00:00+00:00",
                "end": index % 7 == 0 ? NSNull() : "2026-05-\(String(format: "%02d", (index % 27) + 2))T00:00:00+00:00",
                "modified": "2026-05-\(String(format: "%02d", (index % 27) + 1))T12:00:00.123+00:00",
                "external_desc": String(repeating: "incident description ", count: 10),
                "severity": "medium",
                "status_impact": "SERVICE_INFORMATION",
                "affected_products": [["title": "Gemini", "id": "npdyhgECDJ6tB66MxXyo"]],
                "most_recent_update": updates[0],
                "updates": updates,
            ])
        }
        let data = try JSONSerialization.data(withJSONObject: incidents)
        struct Incident: Decodable { let begin: Date?; let end: Date?; let modified: Date?
            let updates: [Update]?
            struct Update: Decodable { let when: Date? } }
        func measure(_ strategy: JSONDecoder.DateDecodingStrategy) -> [String] {
            let clock = ContinuousClock()
            var ms: [Double] = []
            for _ in 0..<10 {
                let d = clock.measure {
                    let decoder = JSONDecoder()
                    decoder.keyDecodingStrategy = .convertFromSnakeCase
                    decoder.dateDecodingStrategy = strategy
                    _ = try? decoder.decode([Incident].self, from: data)
                }
                ms.append(Double(d.components.attoseconds) / 1e15 + Double(d.components.seconds) * 1000)
            }
            return ms.map { String(format: "%.1f", $0) }
        }
        let previousStrategy = JSONDecoder.DateDecodingStrategy.custom { decoder in
            let container = try decoder.singleValueContainer()
            let raw = try container.decode(String.self)
            let formatter = ISO8601DateFormatter()
            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
            if let date = formatter.date(from: raw) { return date }
            formatter.formatOptions = [.withInternetDateTime]
            if let date = formatter.date(from: raw) { return date }
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "bad date")
        }
        print("TIMING_RESULT payload_bytes=\(data.count)")
        print("BEFORE per-field formatters: \(measure(previousStrategy))")
        let clock = ContinuousClock()
        var ms2: [Double] = []
        for _ in 0..<10 {
            let d = clock.measure {
                _ = try? UsageStore.parseGoogleWorkspaceStatus(data: data, productID: "npdyhgECDJ6tB66MxXyo")
            }
            ms2.append(Double(d.components.attoseconds) / 1e15 + Double(d.components.seconds) * 1000)
        }
        print("AFTER shared formatters (full parse incl. filtering): \(ms2.map { String(format: "%.1f", $0) })")
    }
}

@ProspectOre

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 10, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper

clawsweeper Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed June 10, 2026, 10:13 PM ET / 02:13 UTC.

Summary
Moves provider-status feed fetching and decoding onto the concurrent executor, replaces per-field ISO8601 formatter allocation with synchronized shared formatters, and adds focused executor regression coverage.

Reproducibility: yes. at source level: current main lacks an explicit concurrent-executor boundary for these UsageStore status helpers, and the linked Instruments report identifies this exact decode stack as a measured main-thread microhang. This read-only Linux review did not run the macOS app locally.

Review metrics: 3 noteworthy metrics.

  • Patch surface: 3 files; 82 added, 27 removed. The implementation is narrowly contained to status decoding, one focused regression test, and an accompanying release-note entry.
  • Executor boundaries: 2 async helpers changed. Both statuspage and Google Workspace feed paths receive the same explicit off-main execution guarantee.
  • Live-feed improvement: about 3.1× faster. The submitted live output reports approximately 68–71 ms before and 22–26 ms after on a 420,521-byte real feed.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🐚 platinum hermit
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Next step before merge

  • No automated repair is needed; maintainers can complete ordinary code review, required checks, and merge handling for the exact head.

Security
Cleared: The diff introduces no dependency, downloaded artifact, build-script, permission, credential, secret-handling, or package-resolution changes.

Review details

Best possible solution:

Land the focused concurrent-executor and shared-formatter implementation after ordinary maintainer review and required checks, while continuing to track the separate merged-menu freeze mechanism in #1399.

Do we have a high-confidence way to reproduce the issue?

Yes at source level: current main lacks an explicit concurrent-executor boundary for these UsageStore status helpers, and the linked Instruments report identifies this exact decode stack as a measured main-thread microhang. This read-only Linux review did not run the macOS app locally.

Is this the best way to solve the issue?

Yes. Explicitly moving the stateless asynchronous helpers onto the concurrent executor and reusing lock-guarded formatters is a narrow correction that preserves parsing and publication behavior without introducing a competing configuration or workflow.

AGENTS.md: found and applied where relevant.

Codex review notes: reasoning high; reviewed against 0e0102c30fe6.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The updated PR body contains after-fix copied live output from a real 420,521-byte Google status feed, including repeated timings and a successfully parsed active incident.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The updated PR body contains after-fix copied live output from a real 420,521-byte Google status feed, including repeated timings and a successfully parsed active incident.
  • remove status: 📣 needs proof: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦪 silver shellfish: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.

Label justifications:

  • P2: This addresses a measured provider-status refresh stall with meaningful UI impact but limited scope and no evidence of an emergency availability failure.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🐚 platinum hermit and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The updated PR body contains after-fix copied live output from a real 420,521-byte Google status feed, including repeated timings and a successfully parsed active incident.
  • proof: sufficient: Contributor real behavior proof is sufficient. The updated PR body contains after-fix copied live output from a real 420,521-byte Google status feed, including repeated timings and a successfully parsed active incident.
Evidence reviewed

What I checked:

  • Current-main behavior: Current main defines the status-feed helpers on the MainActor-isolated UsageStore path without an explicit concurrent-executor boundary, so the central performance problem remains present on main. (Sources/CodexBar/UsageStore+Status.swift:5, 0e0102c30fe6)
  • Explicit executor boundary: The final PR head marks both stateless asynchronous feed helpers @Concurrent and nonisolated, while the existing caller remains responsible for publishing the returned status. (Sources/CodexBar/UsageStore+Status.swift:47, 7a96eb4963e5)
  • Formatter reuse: The proposed implementation serializes access to two reusable ISO8601 formatters, preserving fractional and plain timestamp support without allocating a formatter for every decoded field. (Sources/CodexBar/UsageStore+Status.swift:7, 7a96eb4963e5)
  • Focused regression coverage: The test invokes the workspace-status helper from the main actor and records Thread.isMainThread immediately before JSON parsing, rather than only observing the transport stub. (Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift:45, 7a96eb4963e5)
  • Head refinement provenance: The final commit, authored by Peter Steinberger, added the explicit @Concurrent guarantee and moved the regression observation to the pre-decode boundary; this supports treating those details as intentional final-head behavior. (Sources/CodexBar/UsageStore+Status.swift:47, 7a96eb4963e5)
  • Real behavior proof: The updated PR body reports a 420,521-byte live Google incidents feed, five before/after timing iterations improving from roughly 68–71 ms to 22–26 ms, and successful parsing of an active Gemini incident. Copied live output is an accepted proof form for this non-visual behavior. (7a96eb4963e5)

Likely related people:

  • Peter Steinberger: The available main-branch blame attributes the status-feed implementation to commit 920997c, and Peter authored the final PR-head concurrency hardening in 7a96eb4. (role: introduced behavior and recent area contributor; confidence: high; commits: 920997c6a365, 7a96eb4963e5; files: Sources/CodexBar/UsageStore+Status.swift, Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift)
  • serezha93: Commit f62bb8c introduced the shared provider HTTP transport used by the affected status-fetch helpers, making this contributor relevant to transport-boundary review. (role: introduced adjacent transport behavior; confidence: medium; commits: f62bb8c8d564; files: Sources/CodexBar/UsageStore+Status.swift)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal priority bug or improvement with limited blast radius. labels Jun 10, 2026
@ProspectOre ProspectOre force-pushed the perf/google-status-decode-off-main branch from 3ade9f4 to 4b8cf0a Compare June 10, 2026 22:18
@ProspectOre

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 10, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

ProspectOre and others added 2 commits June 11, 2026 03:04
UsageStore's status fetch/parse helpers are statics on a MainActor type,
so the Google Workspace incidents feed (hundreds of kilobytes live)
decoded on the main thread, stalling the UI 150-340ms per Google-status
provider per refresh - refreshes that also fire during menu interaction
(steipete#1399). The status helpers touch no store state, so they are now
nonisolated and run on the concurrent executor, and the per-date-field
ISO8601DateFormatter allocations are replaced with shared lock-guarded
formatters (same pattern as CostUsageISO8601FormatterBox).
@steipete steipete force-pushed the perf/google-status-decode-off-main branch from 4b8cf0a to 7a96eb4 Compare June 11, 2026 02:08
@clawsweeper clawsweeper Bot added proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 11, 2026
@steipete steipete merged commit 02c9403 into steipete:main Jun 11, 2026
4 checks passed

Copy link
Copy Markdown
Owner

Landed as 02c94032c79a23b95658b37c2f85ea59d8de713a.

Proof:

  • swift test --filter GoogleWorkspaceStatus — 4 tests pass
  • swift test — 3,532 tests in 402 suites pass
  • make check and git diff --check — clean
  • autoreview — no accepted/actionable findings
  • exact-head CI — all required checks green
  • live 420,521-byte feed decode improved from ~68–71ms to ~22–26ms and now runs off the main actor

Thanks @ProspectOre.

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

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Merge Icons mode causes system-wide input freezes / beachballs on macOS 26 — WindowServer event-buffer overflow evidence (not an in-process hang)

2 participants