Background NSURLSession imposes penalties while app is is foreground

The documentation at https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background, in the section titled "Use Background Session efficiently", talks about delays being introduced when an app uses background NSURLSessions in a certain way.


It also says that all delays are reset when the app is brought to the foreground.


In the app in question, I have

- a file provider that uses a background NSURLSession exclusively

- a HTTP scheduler that schedules small requests on a local NSURLSession and up- and downloads on a seperate background NSURLSession


Now, what I noticed under iOS 13.1.3 is that - even though the app is in the foreground at all times - delays are added to requests going to the app's background NSURLSession. Here are the timings of one affected request, taken directly from NSURLSessionTaskTransactionMetrics:


{ total: [2019-10-16 18:56:24 +0000 - 2019-10-16 18:56:54 +0000, 29.95 sec], startedAfter: 0.00, redirects: 0, transactions: [1: fetchStart: 0.02, request: 29.02..29.02 (0.00), cloud: 29.02..29.73 (0.71), response: 29.73..29.95 (0.22)] }


- fetchStart is at 0.02 seconds

- requestStart is at 29.02 seconds


That's 29 seconds from the task being to the app's background NSURLSession from actually being performed, on a device that's not under any network load whatsoever.


Now, the app already takes care to schedule as few requests as possible on the background NSURLSession. And according to documentation, the delays are reset and not carried out if the app is in the foreground. Which the app is while the above delay is imposed.


Is there anything else that could be causing these delays? Or anything else that I could do to avoid them? Could the File Provider's exclusive use of a background NSURLSession - in the background - lead to penalties being imposed on the app? Or are these two accounted for on a separate basis?

Replies

Is there any development here? Has this bug been fixed by the latest iOS versions?

You could be hitting a rate limiting situation here, or the OS is not ready to run your download. In any event, an approach to take here is to be picky about how you setup your NSURLSessionConfiguration. If you need foreground tasks to be run, setup a managing class that uses a default NSURLSessionConfiguration to run your tasks from this configuration immediately. If you need to run a background downloads using BGTaskScheduler, then create a managing class that uses a background NSURLSessionConfiguration. In thinking about a situation like this you try a class like the following to do so.


protocol NetworkDisplay: class {
    func updateUI(response: String)
}


class NetworkSessionManager: NSObject {
    
    private var urlSession: URLSession?
    private var downloadRequest: URLSessionDownloadTask?
    private weak var delegate: NetworkDisplay?
    private var resumeData: Data?
    
    init(withBackgroundSession: String, delegate: NetworkDisplay) {
        super.init()
        let config = URLSessionConfiguration.background(withIdentifier: withBackgroundSession)
        config.isDiscretionary = true
        config.sessionSendsLaunchEvents = true
        config.allowsCellularAccess = true
        self.delegate = delegate
        urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    }
    
    init(delegate: NetworkDisplay) {
        super.init()
        let config = URLSessionConfiguration.default
        config.allowsCellularAccess = true
        self.delegate = delegate
        urlSession = URLSession(configuration: config, delegate: self, delegateQueue: .main)
    }
    
    func downloadWithFile(with url: String) {
        guard let url = URL(string: url),
            let unwrappedURLSession = urlSession else { return }
        downloadRequest = unwrappedURLSession.downloadTask(with: url)
        downloadRequest?.resume()
    }
    
    func attemptToResume() {
        // Attempting to resume task if the server supports it.
        if let unwrappedResumeData = resumeData {
            downloadRequest = urlSession?.downloadTask(withResumeData: unwrappedResumeData)
            print("Resuming: \(String(describing: downloadRequest))")
            downloadRequest?.resume()
            resumeData = nil
        }
    }
    
}




extension NetworkSessionManager: URLSessionDownloadDelegate, URLSessionDelegate {
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        print("Response: \(downloadTask.response.debugDescription)")
        delegate?.updateUI(response: downloadTask.response.debugDescription)
        print("Response location: \(location)")
    }
    


    func urlSession(_ session: URLSession,
                downloadTask: URLSessionDownloadTask,
                didWriteData bytesWritten: Int64,
           totalBytesWritten: Int64,
           totalBytesExpectedToWrite: Int64) {
        print("Bytes written: \(totalBytesWritten) out of: \(totalBytesExpectedToWrite)")
    }


    func urlSession(_ session: URLSession,
                    task: URLSessionTask,
                    didCompleteWithError error: Error?) {
        print("didCompleteWithError: \(String(describing: error?.localizedDescription))")
        if let unwrappedError = error {
  
            let userInfo = (unwrappedError as NSError).userInfo
            if let resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
                self.resumeData = resumeData
                delegate?.updateUI(response: "Error downloading, resuming download")
                attemptToResume()
            } else {
                delegate?.updateUI(response: unwrappedError.localizedDescription)
            }
        }
    }


}


Matt Eaton

DTS Engineering, CoreOS

meaton3 at apple.com