From b079e98d5430df8e747699f7fb9e12f514711bca Mon Sep 17 00:00:00 2001 From: pickaxe <54486432+ProspectOre@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:01:46 -0700 Subject: [PATCH 1/2] Decode provider status feeds off the main actor 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 (#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). --- Sources/CodexBar/UsageStore+Status.swift | 76 ++++++++++++------- .../GoogleWorkspaceStatusNetworkTests.swift | 24 ++++++ 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/Sources/CodexBar/UsageStore+Status.swift b/Sources/CodexBar/UsageStore+Status.swift index 4d1b85e4d3..7939fa31af 100644 --- a/Sources/CodexBar/UsageStore+Status.swift +++ b/Sources/CodexBar/UsageStore+Status.swift @@ -1,8 +1,50 @@ import CodexBarCore import Foundation +/// Shared, lock-guarded ISO8601 formatters for status feeds. Allocating a fresh +/// `ISO8601DateFormatter` per decoded date field is a measurable share of decoding the +/// Google Workspace incidents feed, which can run to hundreds of kilobytes (#1399). +private final class StatusISO8601FormatterBox: @unchecked Sendable { + let lock = NSLock() + let withFractional: ISO8601DateFormatter = { + let fmt = ISO8601DateFormatter() + fmt.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return fmt + }() + + let plain: ISO8601DateFormatter = { + let fmt = ISO8601DateFormatter() + fmt.formatOptions = [.withInternetDateTime] + return fmt + }() +} + +private enum StatusFeedDateParser { + static let box = StatusISO8601FormatterBox() + + static func parse(_ text: String) -> Date? { + self.box.lock.lock() + defer { self.box.lock.unlock() } + return self.box.withFractional.date(from: text) ?? self.box.plain.date(from: text) + } + + static func decodingStrategy() -> JSONDecoder.DateDecodingStrategy { + .custom { decoder in + let container = try decoder.singleValueContainer() + let raw = try container.decode(String.self) + guard let date = StatusFeedDateParser.parse(raw) else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid ISO8601 date") + } + return date + } + } +} + extension UsageStore { - static func fetchStatus( + /// Status feeds decode off the main actor: the Google Workspace incidents payload alone + /// can be hundreds of kilobytes and cost 150-340ms to decode (#1399), and these helpers + /// touch no store state. + nonisolated static func fetchStatus( from baseURL: URL, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> ProviderStatus @@ -32,16 +74,7 @@ extension UsageStore { } let decoder = JSONDecoder() - decoder.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: "Invalid ISO8601 date") - } + decoder.dateDecodingStrategy = StatusFeedDateParser.decodingStrategy() let response = try decoder.decode(Response.self, from: data) let indicator = ProviderStatusIndicator(rawValue: response.status.indicator) ?? .unknown @@ -51,7 +84,7 @@ extension UsageStore { updatedAt: response.page?.updatedAt) } - static func fetchWorkspaceStatus( + nonisolated static func fetchWorkspaceStatus( productID: String, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> ProviderStatus @@ -65,19 +98,10 @@ extension UsageStore { return try Self.parseGoogleWorkspaceStatus(data: data, productID: productID) } - static func parseGoogleWorkspaceStatus(data: Data, productID: String) throws -> ProviderStatus { + nonisolated static func parseGoogleWorkspaceStatus(data: Data, productID: String) throws -> ProviderStatus { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.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: "Invalid ISO8601 date") - } + decoder.dateDecodingStrategy = StatusFeedDateParser.decodingStrategy() let incidents = try decoder.decode([GoogleWorkspaceIncident].self, from: data) let active = incidents.filter { $0.isRelevant(productID: productID) && $0.isActive } @@ -105,7 +129,7 @@ extension UsageStore { return ProviderStatus(indicator: best.indicator, description: description, updatedAt: updatedAt) } - private static func indicatorRank(_ indicator: ProviderStatusIndicator) -> Int { + private nonisolated static func indicatorRank(_ indicator: ProviderStatusIndicator) -> Int { switch indicator { case .none: 0 case .maintenance: 1 @@ -116,7 +140,7 @@ extension UsageStore { } } - private static func workspaceIndicator(status: String?, severity: String?) -> ProviderStatusIndicator { + private nonisolated static func workspaceIndicator(status: String?, severity: String?) -> ProviderStatusIndicator { switch status?.uppercased() { case "AVAILABLE": return .none case "SERVICE_INFORMATION": return .minor @@ -134,7 +158,7 @@ extension UsageStore { } } - private static func workspaceSummary(from text: String?) -> String? { + private nonisolated static func workspaceSummary(from text: String?) -> String? { guard let text else { return nil } let normalized = text .replacingOccurrences(of: "\r\n", with: "\n") diff --git a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift index 9266127293..cbf9667378 100644 --- a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift +++ b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift @@ -1,4 +1,5 @@ import Foundation +import os import Testing @testable import CodexBar @@ -41,4 +42,27 @@ struct GoogleWorkspaceStatusNetworkTests { #expect(requests.count == 1) #expect(requests.first?.url?.host == "www.google.com") } + + @Test + func `fetchWorkspaceStatus stays off the main thread when called from the main actor`() async throws { + // The incidents feed can run to hundreds of kilobytes; decoding it on the main + // actor stalls the UI for 150-340ms per Google-status provider per refresh (#1399). + let fetchedOffMainThread = OSAllocatedUnfairLock(initialState: false) + let transport = ProviderHTTPTransportStub { request in + fetchedOffMainThread.withLock { $0 = !Thread.isMainThread } + let response = try HTTPURLResponse( + url: #require(request.url), + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"])! + return (Data("[]".utf8), response) + } + + let status = try await UsageStore.fetchWorkspaceStatus( + productID: "npdyhgECDJ6tB66MxXyo", + transport: transport) + + #expect(status.indicator == .none) + #expect(fetchedOffMainThread.withLock { $0 }) + } } From 7a96eb4963e53638008f133d6eb256c769828540 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2026 03:08:06 +0100 Subject: [PATCH 2/2] perf: guarantee status decoding stays off main --- CHANGELOG.md | 1 + Sources/CodexBar/UsageStore+Status.swift | 6 +++++- .../GoogleWorkspaceStatusNetworkTests.swift | 12 +++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f62670b7c7..2ac0eae4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Doubao: confirm zero-remaining HTTP 200 request limits before falling back, preserving genuine exhaustion and avoiding false 100% usage (#1383). Thanks @LeoLin990405 and @foobra! - Menu bar: defer pasteboard writes and copy feedback outside the `NSMenu` tracking callback so in-menu copy buttons no longer beachball on macOS 26 (#1388). Thanks @LeoLin990405! - Menu bar: defer merged status-icon redraws until the tracked menu closes while preserving animation lifecycle and quota-warning timing, reducing WindowServer churn during long menu sessions (#1409, fixes #1399). Thanks @kiranmagic7! +- Provider status: decode status feeds on the concurrent executor and reuse ISO8601 formatters, removing a measured main-thread stall during refreshes (#1406). Thanks @ProspectOre! - Menu bar: keep one stable width across merged provider tabs and resize every hosted card row to AppKit's final menu width so provider switching no longer leaves a widened menu with inset submenu arrows (#1410). - Menu bar: defer Overview-row provider transitions out of AppKit's click callback so opening provider detail no longer performs a full synchronous menu rebuild (#1325). - Menu bar: open cached menus immediately after data-only invalidations, then refresh missing or stale provider data asynchronously without queuing redundant work on close (#1398). Thanks @joshuavial! diff --git a/Sources/CodexBar/UsageStore+Status.swift b/Sources/CodexBar/UsageStore+Status.swift index 7939fa31af..c1bd9b3fbd 100644 --- a/Sources/CodexBar/UsageStore+Status.swift +++ b/Sources/CodexBar/UsageStore+Status.swift @@ -44,6 +44,7 @@ extension UsageStore { /// Status feeds decode off the main actor: the Google Workspace incidents payload alone /// can be hundreds of kilobytes and cost 150-340ms to decode (#1399), and these helpers /// touch no store state. + @concurrent nonisolated static func fetchStatus( from baseURL: URL, transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) @@ -84,9 +85,11 @@ extension UsageStore { updatedAt: response.page?.updatedAt) } + @concurrent nonisolated static func fetchWorkspaceStatus( productID: String, - transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared, + beforeDecoding: (@Sendable () -> Void)? = nil) async throws -> ProviderStatus { guard let url = URL(string: "https://www.google.com/appsstatus/dashboard/incidents.json") else { @@ -95,6 +98,7 @@ extension UsageStore { var request = URLRequest(url: url) request.timeoutInterval = 10 let (data, _) = try await transport.data(for: request) + beforeDecoding?() return try Self.parseGoogleWorkspaceStatus(data: data, productID: productID) } diff --git a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift index cbf9667378..1d51d3dfa2 100644 --- a/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift +++ b/Tests/CodexBarTests/GoogleWorkspaceStatusNetworkTests.swift @@ -44,12 +44,11 @@ struct GoogleWorkspaceStatusNetworkTests { } @Test - func `fetchWorkspaceStatus stays off the main thread when called from the main actor`() async throws { + func `fetchWorkspaceStatus decodes off the main thread when called from the main actor`() async throws { // The incidents feed can run to hundreds of kilobytes; decoding it on the main // actor stalls the UI for 150-340ms per Google-status provider per refresh (#1399). - let fetchedOffMainThread = OSAllocatedUnfairLock(initialState: false) + let decodedOffMainThread = OSAllocatedUnfairLock(initialState: false) let transport = ProviderHTTPTransportStub { request in - fetchedOffMainThread.withLock { $0 = !Thread.isMainThread } let response = try HTTPURLResponse( url: #require(request.url), statusCode: 200, @@ -60,9 +59,12 @@ struct GoogleWorkspaceStatusNetworkTests { let status = try await UsageStore.fetchWorkspaceStatus( productID: "npdyhgECDJ6tB66MxXyo", - transport: transport) + transport: transport, + beforeDecoding: { + decodedOffMainThread.withLock { $0 = !Thread.isMainThread } + }) #expect(status.indicator == .none) - #expect(fetchedOffMainThread.withLock { $0 }) + #expect(decodedOffMainThread.withLock { $0 }) } }