Skip to content

fix: parallelize mobile-sync fan-out and skip known-offline peers#58

Merged
sirily11 merged 1 commit into
mainfrom
fix/mobilesync-fanout-perf
May 26, 2026
Merged

fix: parallelize mobile-sync fan-out and skip known-offline peers#58
sirily11 merged 1 commit into
mainfrom
fix/mobilesync-fanout-perf

Conversation

@sirily11

Copy link
Copy Markdown
Contributor

Summary

  • Parallelize broadcastToAllClients so one slow relay RTT does not block the others.
  • Parallelize fanoutPush and fanoutAPNs for the same reason.
  • Skip peers already known offline in broadcastMobileSnapshots so we don't burn a round-trip just to receive delivery_failed.

Why

A log from a session with 5 paired peers (2 on Local Relay, 3 on RxLab Hosted Relay) showed:

  • 790 sync events, 51% (403) returned relay delivery failed.
  • Every change event ran 5 peer sends serially. With ~150ms remote RTT, a 5-peer broadcast wall-clock was close to a full second.
  • Offline peers were still iterated because broadcastMobileSnapshots did not filter by onlineState, so each broadcast paid 5× RTT just to be told "already offline" 5 times.

Changes

  • RxCode/Services/MobileSyncService.swift
    • broadcastToAllClients: snapshot targets on the main actor, fan out with withTaskGroup so each peer's websocket send awaits concurrently.
    • fanoutPush: capture provider + push URL on the main actor, then run the per-device HTTPS POST in a TaskGroup.
    • fanoutAPNs: same pattern.
  • RxCode/App/AppState+MobileSnapshots.swift
    • broadcastMobileSnapshots: pre-filter pairedDevices by onlineState != .offline. Presence flips back to .online on any inbound, so a peer that returns gets snapshots again immediately.

Test plan

  • xcodebuild -project RxCode.xcodeproj -scheme RxCode -configuration Debug build succeeds.
  • Manual: with multiple paired devices on a remote relay, observe snapshot/notification broadcast latency drops from ~N × RTT to ~1 × RTT.
  • Manual: take a device offline, confirm subsequent broadcasts no longer log delivery failed for that device until it sends an inbound again.
  • Manual: bring the offline device back online — first inbound should flip onlineState and resume snapshots.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 25, 2026 18:09
@vercel

vercel Bot commented May 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rxcode Ready Ready Preview, Comment May 26, 2026 3:25am

Request Review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves mobile sync broadcast latency by parallelizing per-peer fan-out and avoiding known-offline peers, and it also adds cross-platform push/analytics plumbing (Firebase + Android FCM) to support push delivery and unified event logging.

Changes:

  • Parallelize relay socket broadcast + push fan-out using task groups to avoid N×RTT behavior.
  • Introduce provider-aware push tokens (apns/fcm) and add Android FCM push delivery end-to-end (Android app + relay + desktop).
  • Add a shared analytics facade (AnalyticsService) and wire Firebase Analytics/Crashlytics bootstrapping; emit new navigation/surface analytics events.

Reviewed changes

Copilot reviewed 57 out of 58 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
scripts/ci/write-firebase-config.sh Decodes Firebase config files from CI secrets into expected paths.
ci_scripts/ci_post_clone.sh Calls Firebase config writer in Xcode Cloud post-clone step.
.github/workflows/test.yaml Adds Firebase config materialization steps for CI jobs.
.github/workflows/build.yaml Adds Firebase config materialization to build workflow.
.gitignore Ignores decoded Firebase config files.
Packages/Sources/RxCodeCore/Utilities/AnalyticsService.swift Adds transport-agnostic analytics service + shared event names.
RxCode/App/FirebaseBootstrap.swift Configures Firebase + installs analytics handler (macOS app).
RxCodeMobile/FirebaseBootstrap.swift Configures Firebase + installs analytics handler (iOS app).
RxCode/App/RxCodeApp.swift Boots Firebase during macOS app init.
RxCodeMobile/AppDelegate.swift Boots Firebase during iOS app launch.
RxCode/Views/Sidebar/BriefingView.swift Logs briefing list open analytics event.
RxCodeMobile/Views/MobileBriefingView.swift Logs briefing list open analytics event.
RxCodeMobile/Views/MobileBriefingDetailView.swift UI tweaks + logs briefing detail open event.
RxCode/Views/RunProfile/RunProfileDetailForm.swift Logs run profile editor open event.
RxCodeMobile/Views/MobileRunProfileEditorView.swift Logs run profile editor open event.
RxCode/Views/Inspector/ChangesView.swift Logs diff view open event from inspector changes surface.
RxCode/Views/Inspector/ThisThreadDiffView.swift Logs diff view open event from “this thread” surface.
RxCodeMobile/Views/ThreadChangesSheet.swift Logs diff view open event from mobile thread changes sheet.
RxCodeMobile/Views/MobileChatView.swift Logs thread opened event on session load task.
RxCodeMobile/Views/NewThreadSheet.swift Logs thread created event on newly created session.
RxCodeMobile/Views/ProjectsSidebar.swift Logs project opened + new project started analytics events.
RxCodeMobile/Views/MobileInAppBrowserView.swift Logs in-app browser open analytics event.
RxCode/App/AppState+Project.swift Logs project opened when selecting a project (desktop).
Packages/Sources/RxCodeSync/Protocol/Payload.swift Adds push_token payload type to shared sync protocol.
Packages/Tests/RxCodeSyncTests/PayloadTests.swift Adds round-trip test for push_token payload.
RxCode/Services/MobileSyncService.swift Parallelizes websocket broadcast; adds provider-aware push fan-out + FCM push send path.
RxCode/Services/MobileSyncService+EventDispatch.swift Persists provider-aware push token state from inbound payloads.
RxCode/Services/MobileSyncService+LiveActivity.swift Makes push rejection logging resilient to missing reason.
RxCode/App/AppState+MobileSnapshots.swift Skips known-offline peers when broadcasting snapshots.
RxCode/Views/Settings/MobileSettingsTab.swift Shows push badge/provider label and supports FCM devices.
relay-server/push.go Adds provider routing for /push (APNs vs FCM).
relay-server/fcm.go Implements FCM HTTP v1 sender + service-account loading.
relay-server/main.go Wires FCM sender initialization + exposes via /push and /healthz.
relay-server/health.go Extends health response with fcm availability.
relay-server/README.md Documents /push provider and FCM config/env.
relay-server/go.mod Promotes github.com/golang-jwt/jwt/v4 to direct dependency.
relay-server/k8s/README.md Documents Kubernetes FCM secret/config setup.
relay-server/k8s/configmap.yaml Adds FCM_PROJECT_ID config entry.
RxCodeAndroid/gradle/libs.versions.toml Adds Firebase BOM, libs, and Gradle plugins to version catalog.
RxCodeAndroid/build.gradle.kts Adds google-services + crashlytics plugins to root plugin management.
RxCodeAndroid/app/build.gradle.kts Conditionally applies google-services/crashlytics; adds Firebase deps.
RxCodeAndroid/app/src/main/AndroidManifest.xml Registers FCM service + requests POST_NOTIFICATIONS permission.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/RxCodeApplication.kt Initializes Firebase when configured; enables Crashlytics collection.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/push/RxCodeFirebaseMessagingService.kt Handles FCM token refresh + decrypts incoming encrypted alerts into notifications.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/push/FcmTokenReporter.kt Reports FCM token to paired desktops via sync channel.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/push/EncryptedAlert.kt Adds encrypted alert envelope + plaintext models for FCM path.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/proto/Payload.kt Adds push_token payload to Android serializer and model.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/MainActivity.kt Requests notification permission; routes notification-tap deep links into state.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileState.kt Adds buffered pending notification session id for deep link routing.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/state/MobileAppState.kt Reports FCM token on pairing/cold start; buffers notification-tap routing; adds draft→real session redirects.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/RxCodeApp.kt Consumes buffered notification deep link and routes UI to the session.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sheets/NewThreadSheet.kt Redesigns new-thread sheet; starts session via view model and returns draft id.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/sessions/SessionsScreen.kt Updates new-thread flow to navigate using returned session id and requires non-null viewModel.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/projects/ProjectsPane.kt Avoids draft-session cross-tab routing; navigates using explicit session id.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingPane.kt Updates new-thread callback to use session id.
RxCodeAndroid/app/src/main/java/app/rxlab/rxcode/ui/briefing/BriefingDetailScreen.kt Launches new-thread sheet from briefing detail; returns created session id.
RxCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved Adds Firebase iOS SDK pins to Xcode workspace dependencies.
RxCode.xcodeproj/project.pbxproj Adds Firebase SPM dependency products to macOS + iOS app targets.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to 27
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 24) {
headerCard
summaryCard
threadsSection
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 20)
}
.scrollBounceBehavior(.basedOnSize, axes: .horizontal)
.scrollContentBackground(.hidden)
Comment on lines +27 to +42
for (desktop in desktops) {
desktop.relayUrl?.let { client.setRelayUrl(it) }
client.addPeer(desktop.pubkeyHex)
client.start()
withTimeoutOrNull(CONNECT_TIMEOUT_MS) {
client.connectionState.first { it == RelayClient.ConnectionState.CONNECTED }
}
val sent = client.send(
Payload.PushToken(PushTokenPayload(provider = "fcm", token = token)),
desktop.pubkeyHex,
)
Log.w(
TAG,
"FCM token report sent=$sent desktop=${desktop.pubkeyHex.take(12)} token=${token.take(12)}",
)
}
Comment on lines 41 to 43
init() {
FirebaseBootstrap.configure()
try? Tips.configure([
Sync broadcasts were O(peers × relay-RTT) because every paired device
was serviced serially. With 5 paired peers on a remote relay (~150ms
RTT) a single change event took close to a second wall-clock, and
offline peers still burned a full round-trip per broadcast before the
relay returned `delivery_failed`.

- broadcastToAllClients: fan out concurrently via TaskGroup.
- fanoutPush / fanoutAPNs: same — each push is an independent HTTPS
  POST so per-device RTT no longer compounds.
- broadcastMobileSnapshots: skip peers already known offline; presence
  flips back to online on the next inbound, so a peer that returns
  still gets snapshots immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sirily11 sirily11 force-pushed the fix/mobilesync-fanout-perf branch from 21a1726 to d1c33ae Compare May 26, 2026 03:25
@sirily11 sirily11 enabled auto-merge (squash) May 26, 2026 03:26
@sirily11 sirily11 merged commit 6c58930 into main May 26, 2026
11 checks passed
@sirily11 sirily11 deleted the fix/mobilesync-fanout-perf branch May 26, 2026 03:41
@sirily11

Copy link
Copy Markdown
Contributor Author

🎉 This PR is included in version 1.12.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

bug Something isn't working enhancement New feature or request released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants