A few weeks ago I talked about Allocations, a tool in Instruments to track down memory issues. This week I'd like to talk about Time profiler (XCode 7.3). Although from time to time I tend use other tools to debug issues, Allocations & Time Profiler are the ones I most often consult. Every iOS developer should know how to use those.
Time profiler consists basically of three panes. The track pane at the top showing the CPU usage over time, the detail pane at the bottom providing information about methods a CPU spends its time in and the configuration pane on the right to configure various parameters of the profiling session.
How does it work?
When you are working with Time Profiler, it's important to have a basic understanding of how it works. While you are profiling your app, Time Profiler takes a screenshot of your call stack. Every time it does so, it counts a method's appearance in a sample. Let's say the sample interval, which is configurable by the way, is 10 ms and you record over a timeframe of 30 ms. So you get 3 samples: [A, B], [B], [C]. So sample counter for A and C is 1 and for B it is 2. So the higher the sample counter the more time the CPU spent in a method. Hence the CPU time spent for A, B, C is 10 ms, 20 ms, 10 ms respectively, which would be shown in the detail pane's first column.
I'd like to point out that Time Profiler leaves out methods by default that are blocked. If you want to see the full list of methods just enable "Record Waiting Threads".
Where to profile?
When it comes to profiling, always profile on a device. Usually you want the best app performance for all your customers. Hence when you are profiling on a device, choose the slowest device the app is able to run on. If you are like me then you probably develop your apps on your own device which is never more than two years old. The problem with that approach is that you don't see potential bottlenecks on older devices. Hence if your app support iOS9 then you should definitely test your app on an iPhone 4s.
Apart from that, when you are profiling always use the release build. Debug builds don't contain any optimized code and a lot can be gained with optimizations. Profiling debug builds might make you track down issues that aren't there. You want to profile the version of your app that is as close as possible to the version the customer is later holding in his hands.
When to profile?
Don't profile for the sake of profiling. Profile if there is an issue with your app. This usually means that most of the time you end up profiling before you merge your feature branches back to your development branch since this is the time your testers usually get hold of the latest version of your app.
Don't get me wrong. There is nothing wrong with optimizations. Just don't get lost in the details. If iOS development is a hobby then just optimize away. But if you make your living with developing apps then time is a scarce resource which needs to be spent wisely. Choose your battles. Often it's not worth the effort.
If feature branches are not 'your thing', let me give an advice and do a full regression of your app before you submit to the Appstore. You might find issues worth profiling.
How to profile?
You know the greek aphorism "Know thyself" ? When it comes to profiling, know your app. There is no use in starting up Time Profiler if you can't understand what it tells you. If you are the sole developer of your app then that is definitely not the case, but if you just joined a team of iOS developers, your time might be better spent in studying the app first. Which brings me to my next point.
Before you start profiling to track down an issue, form a hypothesis. I mean, it's your app, so you know how it works. Ask yourself what could go wrong? Is it just bad scrolling performance? Are the network requests made on the main thread? Or maybe your view hierarchy is a tad too complex and it takes too long to calculate the opacity of each pixel. Time Profiler is definitely able to help you with the former, although it might be faster to just jump to the code directly. With the latter however tools like Instrument's Core Animation might be a better fit.
Once you have hypothesis, try to confirm it. That's when Time Profiler becomes invaluable and it has all the bells and whistles to help you there:
- deferred mode (File -> Record Options)
In deferred mode Time Profiler only collects samples. It doesn't do any visualization of its current dataset nor does it allow you to interact with its UI. Hence since it's got less to do, it consumes less CPU time and the results you get are more accurate. This should be the default option. Unfortunately it's not, so please before you profile enable this under File -> Record Options
- Filter relevant data (Track Pane)
Although this is a no brainer, the information Time Profiler collects is just too much. Drill down to the essential bits by taking advantage of the track pane's filter functionality.
- Record waiting threads (Configuration Pane, 1st tab)
You might want to see all the method in your samples. If you do, enable this option on the right configuration pane. Its usefulness strongly depends on the problem your are looking at. Just remember that it is there and off by default and hence by default you are missing out on some data.
- Separate by Thread (Configuration Pane, 2nd tab)
Often you want to know what's happening on the main thread. Enable this option to get you a thread-based perspective of the data. Most of the performance problems originate from code being redundantly executed on the main thread. So most of the time this should be enabled.
- Hide system libraries (Configuration Pane, 2nd tab)
This should be on by default. It's another switch to exclude data and in that particular case to exclude profile data from system libraries. When you are running Time Profiler, it's because your app has a problem. It's true it might originate in one of the system libraries but so far, when I was tracking down issues, the actual problem was always caused by my own code.
- Invert call tree (Configuration Pane, 2nd tab)
The option invert call tree provides you with a different perspective. To illustrate what it does imagine a map. What you normally see is a specific route from one A to B. When you invert the call tree you see all the As that led to B in your current sample data set.
- Extended Detail Page (Configuration Pane, 3rd tab)
This is kind of quick jump help. Instead of looking at the detail pane and drilling down, go to extended detail tab instead. You've got everything there at a glance.
- Strategies (Top right corner, Menu Bar)
It often helps to get a different perspective. That's where strategies help. Apart from the default strategy, there is a CPU strategy that shows you what's happening on each CPU core and a threading strategy that visualizes how occupied your threads are.
So as I said, confirm your hypothesis. When you have your confirmation, fix the issue and then reconfirm. Mind, the work is not done when you found the issue. After you fixed the issue, run Time Profiler again to make sure you really fixed it.
What if your hypothesis was wrong? Well, form a new one based on the current data set. I know this is easier said than done. Believe me, I know. But this has little to do now with Time Profiler as we are entering the realm of debugging techniques. But let me give you an advice here: If you can't come up with an explanation try out rubber ducking. You might be in for a surprise.
If you have got your new hypothesis, collect some new data to confirm it. Repeat this until, well, until you fixed the issue.
As I pointed out earlier Time Profiler is one of the tools every iOS developer needs to know. As with every tool, it is only as good as the person using it. Although it can be kind of overwhelming looking at the many tools available in Instruments, it's important to understand that each tool by itself does a very simple task. Here Time Profiler is no exception. It's collecting sample stack traces and visualizes those to the developer. What a developer needs to learn is which knob to turn to get the information he needs. It's like being Michelangelo sculpting David. Like Michelangelo chipped away every stone that didn't look like David, a developer just needs to chip away the information that isn't relevant to the current task at hand.