Making TimesSquare fast on iOS
Making it flexible was easy enough: use the system calendar APIs to do all of your calculations, put each week into a table row, call it a day. Everybody is pretty good at coming up with custom table rows.
So we did this, integrated the view into the Square Wallet app, and gave it a spin. And indeed, we could select dates and deliver those great Square Wallet gift cards in the future!
Only problem was the scrolling, which was really chunky. We were dropping a ton of frames, and this was on pretty recent hardware. Obviously, we wouldn't want to ship something like that to our customers, so it had to be faster.
I heard that Objective-C methods are slow, so I used C++
The worst thing you can do when faced with making code fast is relying on hearsay or FUD about languages. Never guess at what's going wrong.
Implement something that's correct first and fast second; getting from something that's correct to something that's correct and fast is the important step. And in Objective-C, it's not even that hard.
Instruments to the rescue
If you press and hold on the "Run" button in Xcode, the third option is "Profile", which will build your app and then open Instruments, Apple's fabulous profiling tool.
Here, we use a slightly modified version of TimesSquare's standard test app that continuously scrolls up and down. This will help us get to the bottom of the issue; sure enough, we can run the app for a few seconds and then stop it to see what happened.
Here, we've checked the "Hide System Libraries" option to get a clearer picture of what's so slow. We've also set the inspection range to where the app is scrolling at a steady state to avoid having the view initialization mess up our data.
Note that the CPU is using almost a constant 82% of its cycles calling in to two of TimesSquare's methods. While this doesn't directly correspond to what's happening on the device—this is running in the simulator on a much more powerful Mac—it does tell us that our code is really slow.
What's taking so much time?
We can drill down into those methods to get a much better picture. Highlighting any of the rows and clicking on the arrow that appears at the end of the library name will focus the listing on that method and everything it calls. Now we can uncheck "Hide System Libraries" and we see this:
Wow! A quarter of the samples in this method get spent in calls to
+[NSCalendar currentCalendar]. It's super easy to cache that as a property on the cell. One problem down.
In fact, a couple of rows up we see that we spend 30% of this method's time creating and configuring the UIButtons that respond to input touches.
A moment's thought points out a few problems with this approach: creating views is slow enough that
UITableView has an entire mechanism for reusing them in its
dequeueReusableCellWithIdentifier: method. If the cell itself is getting reused, its subviews should get reused, too, so we can just reconfigure those buttons instead of recreating them every time the cell gets reused. Two problems down.
Also, every week has the same number of days. There's no reason we can't just reuse the buttons we've already made. Three problems down.
A big improvement
After several more iterations of using Instruments to find calls that are taking up a stupidly unreasonable amount of time, we see that the method in question is now taking about 11% of the time it used to take.
Even more importantly, virtually all of the computation time is spent doing things that are important, like setting the correct information on the cell and doing calendar arithmetic. Score one for fast iOS apps!
Looking in the wrong places
Imagine our surprise, then, when one of the first issues filed against TimesSquare on GitHub was this:
Remember above when I said we wanted to set the inspection range to where we were steady-state scrolling? It turns out that prevented us from seeing a pretty big problem in startup time.
It's easy enough to fix, though, by shifting our focus a bit:
If we can get the layout metrics for our table view without creating any cells at all, then we can save another ton of overhead during startup. One more problem down.
Finally, we have a view that's as fast as we want it to be.
In this whole experience, one truth has been constant: don't guess what's making your app slow, measure it. Instruments is a fantastic tool, and it will find all of the low-hanging performance fruit for you. Overall, there's no good reason to ship a slow app when it's so easy to make a fast one.