|
| 1 | +# Ktor Darwin Client (Internal Fork) |
| 2 | + |
| 3 | +This is an internal fork of the [Ktor Darwin HTTP engine](https://github.com/ktorio/ktor) with modifications to address memory issues when processing large HTTP response bodies on Apple platforms. |
| 4 | + |
| 5 | +## Why This Fork Exists |
| 6 | + |
| 7 | +### The Problem: Out of Memory (OOM) on Large Sync Payloads |
| 8 | + |
| 9 | +The upstream Ktor Darwin engine uses an unbounded channel to buffer incoming response data chunks from `NSURLSession`. When processing large sync payloads (hundreds of MBs), this causes: |
| 10 | + |
| 11 | +1. **NSURLSession delivers data faster than it can be processed** - chunks accumulate in the unbounded channel |
| 12 | +2. **Memory usage spikes dramatically** - we observed multi-GB allocations during sync operations |
| 13 | +3. **OOM crashes on iOS/macOS** - devices run out of memory before the response is fully processed |
| 14 | + |
| 15 | +This issue is specific to the Darwin engine because: |
| 16 | + |
| 17 | +- `NSURLSession` delivers data via delegate callbacks that cannot be paused |
| 18 | +- The upstream implementation uses `Channel.UNLIMITED` - buffering all chunks without backpressure |
| 19 | +- Other Ktor engines have natural backpressure mechanisms: |
| 20 | + - **OkHttp**: Uses `BufferedSource.read()` which blocks until data is consumed |
| 21 | + - **Apache/Apache5**: Uses `CapacityChannel` for explicit backpressure signaling |
| 22 | + |
| 23 | +### The Solution: Bounded Channel with Backpressure |
| 24 | + |
| 25 | +Our fork modifies `DarwinTaskHandler` to apply backpressure: |
| 26 | + |
| 27 | +```kotlin |
| 28 | +// Bounded channel instead of unbounded |
| 29 | +private val bodyChunks = Channel<NSData>(capacity = 64) |
| 30 | + |
| 31 | +fun receiveData(dataTask: NSURLSessionDataTask, data: NSData) { |
| 32 | + val result = bodyChunks.trySend(data) |
| 33 | + when { |
| 34 | + result.isClosed -> dataTask.cancel() |
| 35 | + result.isFailure -> { |
| 36 | + // Buffer full - block to apply backpressure |
| 37 | + runBlocking { bodyChunks.send(data) } |
| 38 | + } |
| 39 | + } |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +Key changes: |
| 44 | + |
| 45 | +- **Limited channel capacity (64)** - prevents unbounded memory growth |
| 46 | +- **`runBlocking` on buffer full** - blocks the NSURLSession delegate thread, naturally slowing data delivery |
| 47 | +- **Backpressure propagates to NSURLSession** - the network layer throttles based on processing speed |
| 48 | + |
| 49 | +### Alternative Approaches Considered |
| 50 | + |
| 51 | +**Task Pause/Resume**: We considered using `NSURLSessionTask.suspend()` and `resume()` to pause data delivery when the buffer was full. However, this approach was rejected due to: |
| 52 | + |
| 53 | +- **Complexity** - managing pause/resume state across async boundaries added significant complexity |
| 54 | +- **Concurrency issues** - race conditions between pause signals and data delivery callbacks |
| 55 | +- **Data delivery timing** - the asynchronous nature of NSURLSession means data can still be delivered after calling `suspend()` on the task, which would require periodic draining and complex state management. |
| 56 | +- **Error-prone implementation** - the combination of these factors made the approach fragile and difficult to test |
| 57 | + |
| 58 | +The simpler bounded channel with `runBlocking` approach was chosen as it provides effective backpressure with minimal complexity and maintenance burden. |
| 59 | + |
| 60 | +## When to Update This Fork |
| 61 | + |
| 62 | +This fork should be updated if: |
| 63 | + |
| 64 | +- Ktor releases a fix for this issue upstream (track [ktor issues](https://github.com/ktorio/ktor/issues)) |
| 65 | +- Security vulnerabilities are found in the Ktor Darwin engine |
| 66 | +- New Darwin-specific features are needed |
| 67 | + |
| 68 | +## Files Modified |
| 69 | + |
| 70 | +- `darwin/src/io/ktor/client/engine/darwin/internal/DarwinTaskHandler.kt` - Bounded channel + backpressure logic |
| 71 | + |
| 72 | +## References |
| 73 | + |
| 74 | +- [Original Ktor Darwin Engine](https://github.com/ktorio/ktor/tree/main/ktor-client/ktor-client-darwin) |
| 75 | +- PowerSync Kotlin SDK issue: OOM during large sync operations on iOS/macOS |
0 commit comments