commit 89e85099e3d7809eb1953b191e5b45d2dcb74860
parent c4d448911d854f6a20ee148810a1bfd982a460ff
Author: Michael Camilleri <[email protected]>
Date: Wed, 13 May 2026 04:24:33 +0900
Parallelise the silent-push handler and skip wasteful zone discovery
handleRemoteNotification ran three independent CloudKit round-trips back to
back on every silent-push wake: discoverNewZonesDirect, fetchPushPingsDirect,
then either fetchPushChangesDirect for the active game or a fallback engine
fetch. The active-game direct fetch — the round-trip that actually drives the
'collaborator typed a letter' path — only started after the other two had
returned, even though it touches a different zone and a different record set.
End-to-end push-to-pixel latency was the sum of all three rather than the
slowest one.
Zone discovery is also pure overhead on the collaborative hot path. By the time
a push arrives for a game the user has open, that game's zone is by definition
already known locally; allRecordZones() finds nothing new but costs a
round-trip on every keystroke a partner makes.
When there is an active game in the pushed database scope, the handler now
skips zone discovery entirely and runs fetchPushChangesDirect and
fetchPushPingsDirect concurrently via async let. The two read different record
types in different zones and don't share state, so the wall time drops from t₁
+ t₂ to max(t₁, t₂). Brand-new shared games arriving during a session aren't
surfaced until the next push or a foreground sync — an accepted trade-off,
since the latency-sensitive case is the puzzle that's already open. When no
game is open, the handler keeps the original discovery → pings → engine-fetch
order so a background wake still catches new shares.
Database subscriptions don't carry a zone ID in their push payload, so a
finer-grained "skip discovery if the pushed zone is known" check isn't
available without switching to per-zone subscriptions; the active-game
heuristic captures the same intent for the case that matters.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
Diffstat:
1 file changed, 34 insertions(+), 35 deletions(-)
diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift
@@ -313,28 +313,6 @@ final class AppServices {
guard await ensureICloudSyncStarted() else { return }
syncMonitor.note("remote notification: \(summary)")
- // Fast path: discover any zones this device hasn't seen yet so a
- // game started on another device of the same iCloud user (or a
- // freshly-accepted share) lands locally without waiting on
- // CKSyncEngine's database-scope change delivery, which can lag on a
- // silent-push wake. Runs before the ping fast-path so any pings
- // tied to a newly-discovered game can resolve their game locally.
- if let scope, scope != .public {
- await syncMonitor.run("remote-notification zone discovery") {
- _ = try await syncEngine.discoverNewZonesDirect(scope: scope)
- }
- }
-
- // Fast path: surface Ping-driven notifications immediately by
- // querying Ping records directly, bypassing CKSyncEngine. Works
- // whether or not a game is open, and works during the background-
- // wake window where CKSyncEngine.fetchChanges() can silently no-op.
- if let scope, scope != .public {
- await syncMonitor.run("remote-notification ping fast-path") {
- _ = try await syncEngine.fetchPushPingsDirect(scope: scope)
- }
- }
-
guard let scope, scope != .public else {
await syncMonitor.run("remote-notification fetch") {
try await syncEngine.fetchChanges(source: "push")
@@ -342,22 +320,43 @@ final class AppServices {
await refreshSnapshot()
return
}
- guard let activeGameID = activeGameID(in: scope) else {
- await syncMonitor.run("remote-notification fetch") {
- try await syncEngine.fetchChanges(source: "push")
+
+ if let activeGameID = activeGameID(in: scope) {
+ // Hot path: collaborator activity on the open puzzle. The active
+ // game's zone is already known, so we skip the zone-discovery
+ // round-trip; new shared games arriving during this window are
+ // picked up by the next push or by foregrounding. The direct
+ // fetch and the ping fast-path read different record types and
+ // are independent, so run them concurrently.
+ async let activeFetch: Void = syncMonitor.run("remote-notification direct fetch") {
+ let handled = try await self.syncEngine.fetchPushChangesDirect(
+ scope: scope,
+ gameID: activeGameID
+ )
+ if !handled {
+ try await self.syncEngine.fetchChanges(source: "push")
+ }
}
- await refreshSnapshot()
- return
- }
- await syncMonitor.run("remote-notification direct fetch") {
- let handled = try await syncEngine.fetchPushChangesDirect(
- scope: scope,
- gameID: activeGameID
- )
- if !handled {
- try await syncEngine.fetchChanges(source: "push")
+ async let pingFastPath: Void = syncMonitor.run("remote-notification ping fast-path") {
+ _ = try await self.syncEngine.fetchPushPingsDirect(scope: scope)
+ }
+ _ = await (activeFetch, pingFastPath)
+ } else {
+ // Cold path: no puzzle open. Discover any zones this device
+ // hasn't seen yet (e.g. a freshly-accepted share or a game
+ // started on another device of the same iCloud user) so the
+ // ping fast-path can resolve pings whose zones are brand new.
+ await syncMonitor.run("remote-notification zone discovery") {
+ _ = try await self.syncEngine.discoverNewZonesDirect(scope: scope)
+ }
+ await syncMonitor.run("remote-notification ping fast-path") {
+ _ = try await self.syncEngine.fetchPushPingsDirect(scope: scope)
+ }
+ await syncMonitor.run("remote-notification fetch") {
+ try await self.syncEngine.fetchChanges(source: "push")
}
}
+
await refreshSnapshot()
}