Files
buzz/mobile/ios
Matt Toohey 4278e99441 fix(mobile): answer push snapshot bridge calls at user-initiated QoS
`BuzzPushSnapshotEnrichmentTests` failed on two unrelated branches on
2026-11-01 in "Mobile Swift (Native)", step "Test native controls and
Runner unit tests", and passed on re-run both times:

- Run 41327410782 (PR #8896, job 124910378184):
  `testRestrictionCleanupFailurePreservesSnapshotAndRestoreReleasesAuthority`
  timed out its 10 s "allowed without restoring snapshot" wait (19.3 s
  total). The preceding `testNewBridgeRetriesRecordedCleanupWithoutRestrictingAccess`
  passed but took 4.3 s.
- Run 41325606910 (PR #8897, job 124905175903):
  `testNewBridgeRetriesRecordedCleanupWithoutRestrictingAccess` timed out
  its "retry failure stays allowed" wait (nil instead of
  `interaction_cleanup_retry_failed`), then the restriction test timed out
  both waits (nil result, still restricted, 0 deletion calls).

Cause: scheduling, not test logic. Every reply from `BuzzPushSnapshotBridge`
hops through a private serial queue created at `.utility` QoS and then
`DispatchQueue.main`. In both failing runs the test host (the full Flutter
app, loaded as `TEST_HOST`) saturated the 3-vCPU `macos-latest` runner for
roughly 40 s after launch: `BrowserAuthBridgeTests.testAllowsOnlyBuilderlabLoginPages`,
a synchronous main-thread URL-parsing test, took 2.65 s instead of 0.013 s,
and the since-deleted `testStrictAgeGateWriteFailsWhenAppGroupStoreIsUnavailable`
took 4.9 s. In that window the utility queue was the lowest-priority
runnable work in the process, so it got no CPU for 10-20 s while
main-thread work kept moving slowly; in both passing re-runs every test in
this class took under 0.3 s. The tests themselves have nothing to fix: the
deletion mocks are synchronous, the deadline scheduler is a no-op, each
test uses its own temp directory and bridge, and no state is shared across
tests, so neither a state leak nor an expectation ordering race explains a
callback that never arrives. Utility is also the wrong class in production:
Dart awaits every one of these calls, and the age-gate restore runs during
app launch, exactly when the process is busiest.

Change: the bridge queue and the default interaction-deletion deadline
timer run at `.userInitiated`. The restriction test now records
`qos_class_self()` inside the injected deletion closure, which the bridge
invokes synchronously on its queue, and asserts both calls ran at
`QOS_CLASS_USER_INITIATED`. The 10 s waits are unchanged.

Verification (Xcode 27.2, iOS 27.2 simulator, 16-core host; the CI
starvation did not reproduce locally):

- Class in isolation: 10/10 runs green, 0.1-0.4 s each.
- Full RunnerTests bundle with CI's `-parallel-testing-enabled NO`: 2/2
  runs green (101 tests, about 22 s).
- Falsification: with the queue temporarily back at `.utility` the new
  assertion fails, observing `[17, 17]` (utility) instead of `[25, 25]`.
- A throwaway probe confirmed that async blocks on a queue with an assigned
  QoS run at that QoS (17 for utility, 25 for user-initiated), so the change
  alters real scheduling. Saturating the host process's default-QoS pool
  with spinning blocks did not delay a utility hop (5 ms), so the CI stall
  is OS-level CPU priority starvation on a saturated 3-vCPU VM, which this
  machine cannot reproduce; the diagnosis rests on the CI timings above.
- The repo has no swiftformat or swiftlint configuration; `git diff --check`
  is clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-11-02 15:16:54 +11:00
..