Skip to content

fix(environment): cache App Store environment checks - #254

Open
paulb777 wants to merge 1 commit into
mainfrom
pb-cache-app-store-checks
Open

paulb777 wants to merge 1 commit into
mainfrom
pb-cache-app-store-checks

Conversation

@paulb777

@paulb777 paulb777 commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Cache +[GULAppEnvironmentUtil isAppStoreReceiptSandbox] and
+[GULAppEnvironmentUtil isFromAppStore] for the lifetime of the process.

Part of firebase/firebase-ios-sdk#16726.

Why

firebase/firebase-ios-sdk#16726 reports main-thread hangs in production, some
ending in watchdog terminations, on two Firebase paths that both end in
-[NSBundle appStoreReceiptURL]. That property is not a file lookup. It calls
+[LSBundleProxy bundleProxyForCurrentProcess], which ends in
xpc_connection_send_message_with_reply_sync, so the caller waits for the
LaunchServices daemon. That can take seconds when the daemon is busy.

Before this change, isAppStoreReceiptSandbox had no cache. isFromAppStore
cached only IsAppEncrypted(). Every call from every caller (GULLogger,
FirebaseCore's user agent, Messaging, Auth, Performance) repeated the XPC round
trip. All inputs (receipt type, bundle contents, Mach-O encryption info, and the
FirebaseAppStoreReceiptURLCheckEnabled Info.plist key) are fixed for the life
of the process.

Changes

  • The existing method bodies move, unchanged and with their comments, into
    file-local IsFromAppStore() and IsAppStoreReceiptSandbox() helpers. The
    public class methods wrap them in dispatch_once.
  • IsFromAppStore() still calls the now-cached +isAppStoreReceiptSandbox, so
    the receipt URL is read at most once per process across both APIs.
  • Removed the now-redundant inner dispatch_once around IsAppEncrypted().

Notes for reviewers

  • Caching makes the cost once per process. It does not move that first call
    off the calling thread. The Firebase PRs move the callers off the main
    thread (links below).
  • dispatch_once callers that arrive while the first computation is running
    wait for it to finish. Previously they would each have made their own XPC
    call.

Testing

  • New testAppStoreReceiptURLIsReadOnlyOnce: after a warm-up call, it swizzles
    -[NSBundle appStoreReceiptURL] with a counter and asserts that neither API
    reads it again. A sanity check confirms the swizzle intercepts reads. The test
    fails on main (receiptURLReadCount is 2) and passes with this change.
  • xcodebuild -scheme GoogleUtilities-Package -destination 'platform=macOS' test: all suites pass (160 UtilitiesUnit tests plus the Swift and ObjC
    import tests).

Related PRs

`+[GULAppEnvironmentUtil isAppStoreReceiptSandbox]` read
`-[NSBundle appStoreReceiptURL]` on every call. That property makes a
synchronous XPC call to the LaunchServices daemon, which can block the
calling thread for seconds when the daemon is busy. `+isFromAppStore`
repeated the call because it only cached `IsAppEncrypted()`. Firebase
calls these from the main thread (heartbeat user agent, Messaging APNs
token handling), which caused hangs and watchdog terminations.

Compute both values once per process with `dispatch_once`. Their inputs
(receipt type, bundle contents, Mach-O encryption info, and the
Info.plist opt-out key) can't change while the app is running. The
original bodies move unchanged into file-local helpers.

Part of firebase/firebase-ios-sdk#16726
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants