Tuesday, October 12, 2021

Download Progress With Awaited Network Tasks

Soroush Khanlou:

One would think the URLSessionTaskDelegate would have some affordance that calls you back when new bytes come in, but if that exists, I couldn’t find it.

However, iOS 15 brings a new API that can be used for this purpose — a byte-by-byte asynchronous for loop that can do something every time a new byte comes in from the network — called AsyncBytes.

[…]

One question that this API raises: why does it call your block with every byte? The (very) old NSURLConnectionDelegate would give you updates with chunks of data, so why the change?

[…]

I think this example highlights something important about this new API. The file I was trying to download was about 20MB. That means my for loop is going to spin 20 million times. Because of that, it’s extremely sensitive to any slow tasks that take place in the loop.

[…]

The last thing I did, which did work, is to keep a local variable for the progress, and then only update the progress in SwiftUI when the fastRunningProgress had advanced by a percent.

Joseph Lord:

For the record AsyncSequence blazing fast in beta 5. 🏎❤️ Quicker to XOR the bytes in the file with AsyncSequence of bytes than to Data(contentsOf:) and iterate over the unsafe bytes

David Smith:

I think one of the biggest things people are going to be startled by in my async Swift code is how often I write async functions that aren’t (usually) asynchronous. It took me a few months and some advice from a coworker to internalize how useful this is.

[…]

The most obvious case this is useful is if you have some sort of cache: check the cache, if you hit do the thing immediately, if you miss, suspend and do the thing once you’ve loaded what you needed into the cache.

David Smith:

For bytes, next() looks approximately like this…

@inline(__always) @inlinable mutating func next() async -> UInt8 {
    if bufferRange.isEmpty { await refillBuffer() /* does suspend */ }
    return buffer[bufferRange.popFirst()] /* doesn't suspend */
}

Previously:

3 Comments RSS · Twitter

The URLSession:dataTask:didReceiveData: delegate method provides exactly that, get notified when a bunch of bytes have been received (in chunks). Sounds like a much better strategy than to iterate over each byte.

@Leo Yes, but that’s the older non-async API. I think it’s a valid question why there isn’t an async API that gives you chunks, because, as this example shows, even if iterating bytes is fast, you may have to reimplement chunks on top of that, anyway. Or maybe the receiving chunks and progress chunks are separate concerns that should be handled separately?

@Michael I think it already is established by the async API, that lifecycle and other interruptions should be handled in a different context than the one waiting for a result.

https://developer.apple.com/documentation/foundation/urlsession/3767353-data

“delegate
A delegate that receives life cycle and authentication challenge callbacks as the transfer progresses.”

There is, however, a bug with Apple’s impl, where the lifecycle delegate methods, such as the one I mentioned above, are not called, despite what the docs say.

Leave a Comment