Skip to content

Throttle iOS download progress events by time - #113

Merged
ofalvai merged 1 commit into
masterfrom
ios-time-based-progress-throttle
Nov 11, 2026
Merged

ofalvai merged 1 commit into
masterfrom
ios-time-based-progress-throttle

Conversation

@ofalvai

@ofalvai ofalvai commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

On iOS, CodePushDownloadProgress fires once per network chunk. In a local test, a 6 MB download sent 384 events in about 2 seconds, roughly 180 per second. This PR limits them to about 10 per second, and still sends the final one.

This came up while reviewing #112. There, useDownloadProgress() always subscribes to the event, so each extra event costs a re-render. The fix belongs in native code, so it lives in its own PR.

Why the old throttle stopped working

The module throttled per frame with RCTFrameUpdateObserver. The New Architecture never calls frame observers: RCTInstance registers only RCTTiming with its display link. f8171a1 worked around it by sending an event on every chunk. That removed the throttle on the New Architecture. On the old one, the frame observer still ran too, and sent progress that had already gone out.

What changes

  • A time-based throttle in CodePushCore: It sends at most one event per 100 ms.
  • The final event works without a known size: With gzip, expectedContentLength is -1, so expected == received never matches. The throttle reports unsent progress when the download finishes. When the size is known, it sends the last chunk right away and doesn't wait for unzip and signature checks.
  • The frame observer is gone: That removes the RCTFrameUpdateObserver conformance, paused/pauseCallback, didUpdateFrame:, and the module-level progress ivars. The byte counts now live on the throttle, one per download.

Why time, and why 100 ms

Other native modules do the same thing:

  • React Native's own Android networking (NetworkingModule.kt) throttles upload and download progress to 100 ms (CHUNK_TIMEOUT_NS), and always sends the done event.
  • react-native-fs has progressInterval. react-native-blob-util (ReactNativeBlobUtilProgress.swift) uses an interval and always reports completion.
  • expo-updates filters by whole-percent change instead. That needs a known total size, which a gzip response doesn't have.

Alternative considered: A CADisplayLink would bring back the per-frame behavior. Nothing here needs to sync with frames, though, and 60 or 120 events per second is still more than a progress bar needs.

Out of scope: Android

Android still throttles per frame with ReactChoreographer, so about 60 events per second, and it detects completion with totalBytes == receivedBytes.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings November 10, 2026 13:30

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.

🟢 Approval recommended

No active review issues are reported for the download-progress throttling change.

No open issues 🎉

What changed in this PR

Replaces iOS frame-based download progress reporting with a time-based throttle, reducing event traffic and re-renders for consumers such as useDownloadProgress().

Changes:

  • Adds a per-download Swift throttle with a 100 ms interval and immediate reporting of known-size completion.
  • Flushes unsent final progress on successful completion, including downloads with unknown sizes.
  • Removes the legacy frame observer and adds unit coverage for throttling and final-event behavior.
File Description
ios/​CodePushCore/​Tests/​CodePushCoreTests/​ProgressThrottleTests.swift Tests event timing, rate limits, and final-progress delivery.
ios/​CodePushCore/​Sources/​CodePushCore/​Download/​ProgressThrottle.swift Implements the throttle with an injectable clock and Objective-C interface.
ios/​CodePush/​CodePush.m Integrates per-download throttling and removes frame-observer state and callbacks.

Review effort: Balanced

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.

🟡 Changes recommended

The new ProcessInfo.systemUptime usage lacks the required system-boot-time declaration and elapsed-time reason 35F9.1 in the distributed privacy manifest, risking App Store validation errors for consuming apps.

1 open issue

Review effort: Balanced

The RCTFrameUpdateObserver throttle never runs on the New Architecture,
and f8171a1 worked around that by sending an event per network chunk,
even when JS didn't ask for progress. Replace both with a 100 ms
throttle in CodePushCore that never holds back the completed event.

When the last chunk doesn't bring receivedBytes up to totalBytes (no
Content-Length, e.g. gzip, or a mismatched one), the download handler
now sends a final event with totalBytes set to the received size.
Before, a response without Content-Length ended on totalBytes -1, so JS
never saw receivedBytes == totalBytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ofalvai
ofalvai force-pushed the ios-time-based-progress-throttle branch from c599a2d to 2292131 Compare November 10, 2026 19:51
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:NO];
if ([NSOperationQueue instancesRespondToSelector:@selector(setUnderlyingQueue:)]) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Available since iOS 8, no need to use a runtime availability check: https://developer.apple.com/documentation/foundation/operationqueue/underlyingqueue

@ofalvai
ofalvai merged commit fa035ee into master Nov 11, 2026
6 checks passed
@ofalvai
ofalvai deleted the ios-time-based-progress-throttle branch November 11, 2026 09:56
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.

3 participants