network warmup hijacks main thread

hello,

i found something peculiar: the very first http/https network load in the app even when done on a background queue affects the main thread significantly. have a look at the screen recording of the app:

http shorturl.at/nxAE4

and how non linear the scrolling performance is:

http shorturl.at/uADY2

there i scroll a list of simple items and when it hit item 30 it loads it on a background queue. result (in this case error) is delivered on another background queue and ignored. the second load of this or a different URL doesn't cause such delay.

what exactly is system doing on the main thread here and can it not do it elsewhere?

what is the best way to figure what extra code is executed on main thread? i tried instruments with various tools in it but can't make heads or tails of it to figure out what exactly is going on here.

the sample code that used for url loading:
Code Block
let someQueue = DispatchQueue(label: "someQueue")
let otherQueue = DispatchQueue(label: "otherQueue")
var someOpQueue: OperationQueue = {
let q = OperationQueue()
q.underlyingQueue = someQueue
q.name = "someOpQueue"
return q
}()
var session = URLSession(configuration: .default, delegate: nil, delegateQueue: someOpQueue)
func loadUrl(_ url: URL) {
otherQueue.async {
let task = session.dataTask(with: url) { d, r, e in
/* ignore */
}
task.resume()
}
}

Answered by Systems Engineer in 625533022
One of the first things you could do here is look at the handoff between your background queue and the main to see if this is causing a bottleneck. If this is not the cause, then look at how your app actually reads the data to reload the tableview. I suspect something in either one of these scenarios is causing the issue.

As for how this can be done, you can try and investigate this with Time Profiler in Intruments:

Using Time Profiler in Instruments:
<https://developer.apple.com/videos/play/wwdc2016/418/>

When you do this, look for points on where you have significant jumps in CPU and dig into your call trees from there. These jumps in CPU will be denoted by spikes of on the bar graph.

When you use the Time Profiler, at the top left under your target device you may see a filter for "ATTRIBUTE: target" and "ANY: *". Try changing the ANY filter to THREAD. This should give you a look at the threads running during your profile capture and allow you to see the activity on the main thread. These threads will have a complete stack trace available for your to view. This can be helpful when debugging situations like this.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Accepted Answer
One of the first things you could do here is look at the handoff between your background queue and the main to see if this is causing a bottleneck. If this is not the cause, then look at how your app actually reads the data to reload the tableview. I suspect something in either one of these scenarios is causing the issue.

As for how this can be done, you can try and investigate this with Time Profiler in Intruments:

Using Time Profiler in Instruments:
<https://developer.apple.com/videos/play/wwdc2016/418/>

When you do this, look for points on where you have significant jumps in CPU and dig into your call trees from there. These jumps in CPU will be denoted by spikes of on the bar graph.

When you use the Time Profiler, at the top left under your target device you may see a filter for "ATTRIBUTE: target" and "ANY: *". Try changing the ANY filter to THREAD. This should give you a look at the threads running during your profile capture and allow you to see the activity on the main thread. These threads will have a complete stack trace available for your to view. This can be helpful when debugging situations like this.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
thank you for the tips, will try those.

there is no cell reload in this sample app, and the data received from network (in this case error) is just ignored.
the full source below, i deliberately put everything in one source file for this test purposes and there is no need storyboards, etc:

Code Block
import UIKit
let someQueue = DispatchQueue(label: "someQueue")
let otherQueue = DispatchQueue(label: "otherQueue")
var someOpQueue: OperationQueue = {
let q = OperationQueue()
q.underlyingQueue = someQueue
q.name = "someOpQueue"
return q
}()
var session = URLSession(configuration: .default, delegate: nil, delegateQueue: someOpQueue)
func loadUrl(_ url: URL) {
otherQueue.async {
let task = session.dataTask(with: url) { data, response, error in
// ignore
}
task.resume()
}
}
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
private var tableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 10000 }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let row = indexPath.row
var text = String(row)
if (row % 30) == 0 && row != 0 {
let n = Int.random(in: 0 ..< 10000)
text = "https://www.some-\(n)-thing.com/file-\(n).png" /* some bogus URL */
}
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = text
if text.contains("https"), let url = URL(string: text) {
loadUrl(url)
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let y = scrollView.contentOffset.y + 88 // quick & dirty math here. good enough for this test
title = String(format: "%4.1f", y / 44)
}
override func viewDidLoad() {
super.viewDidLoad()
tableView = UITableView(frame: view.bounds)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = self
tableView.delegate = self
tableView.allowsSelection = false
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(tableView)
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
window = UIWindow(frame: UIScreen.main.bounds)
window!.rootViewController = UINavigationController(rootViewController: ViewController())
window!.makeKeyAndVisible()
return true
}
}


worth opening a DST incident?

Interesting. I tried this on a device (iPhone 7+) and a simulator, with an actual URL that hits a cache server, and the scrolling performance looked fine. If there is more to this equation, it might be worth looking at what happens after the data is downloaded and used in your app.

One thing you could do as well is take a look at this with from the perspective of Animation Hitches as well. For more on this, check out Eliminate animation hitches with XCTest. This video describes this exact scenario.

<https://developer.apple.com/videos/play/wwdc2020/10077/>

worth opening a DST incident?

Sure, you can also open a DTS incident for extended help or deeper analysis on a topic.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Matt, thank you.

i filed the DTS incident (742866859)

Note that in this sample code this is only the very first load that is affected. (although i am not so sure as i saw this effect in the real app that had loads of network activity prior to a similar hiccup.) the effect is easier to see under real device (i used iPhone XS Max).

it might be worth looking at what happens after the data is downloaded and used in your app.

in this case the data is unused, the test is just to check if loading done on a background queue affects the main queue (and somehow it does). i tested it with a non bogus URL, like apple.com - same hiccup occurs to me. and no difference between http & https.

Mike
btw, to plot this graph i stepped through the recorded video frame by frame and put content.offset.y (conveniently presented in the navigation bar title) into a table.

http shorturl.at/uADY2

network warmup hijacks main thread
 
 
Q