Repository navigation
Throttle iOS download progress events by time - #113
Conversation
There was a problem hiding this comment.
🟢 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
2113f50 to
c599a2d
Compare
There was a problem hiding this comment.
🟡 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>
c599a2d to
2292131
Compare
| NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request | ||
| delegate:self | ||
| startImmediately:NO]; | ||
| if ([NSOperationQueue instancesRespondToSelector:@selector(setUnderlyingQueue:)]) { |
There was a problem hiding this comment.
Available since iOS 8, no need to use a runtime availability check: https://developer.apple.com/documentation/foundation/operationqueue/underlyingqueue

On iOS,
CodePushDownloadProgressfires 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:RCTInstanceregisters onlyRCTTimingwith 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
CodePushCore: It sends at most one event per 100 ms.expectedContentLengthis -1, soexpected == receivednever 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.RCTFrameUpdateObserverconformance,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:
NetworkingModule.kt) throttles upload and download progress to 100 ms (CHUNK_TIMEOUT_NS), and always sends the done event.progressInterval. react-native-blob-util (ReactNativeBlobUtilProgress.swift) uses an interval and always reports completion.Alternative considered: A
CADisplayLinkwould 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 withtotalBytes == receivedBytes.🤖 Generated with Claude Code