What every iOS Developer should be doing with Instruments
You’ve just wrapped up development on a shiny new iOS project and have done your best to ensure that the app doesn’t crash and it seems to run ok on your test devices, but is it ready to submit? If you haven’t done any profiling in Instruments, the answer is probably no. Just because it doesn’t crash doesn’t mean that it’s going to behave and run well on your user’s devices.
Xcode includes a performance tuning application named Instruments that you can use to profile your application using all sorts of different metrics. They have tools to inspect CPU usage, memory usage, leaks, file/network activity, and energy usage just to name a few. It’s really easy to start profiling your app from Xcode, but it’s sometimes not as easy to understand what you see when it’s profiling, which deters some developers from being able to use this tool to it’s fullest potential.
With so many things you can profile, how do you choose what to look at? Obviously if there are any immediate performance issues that you know about, like slow network requests or laggy scrolling, you should target those first. But if things seem to be going ok, I’d suggest that you at least look at your CPU and memory usage during a few runs of the application to ensure that everything is behaving as you might think.
When to Profile
Before going into this information I did want to note that I don’t expect every developer to be actively profiling their app all the time. Most of us have deadlines and expectations to meet, so profiling your application sometimes get shoved to the side, but let’s talk about some points where you should be profiling.
At a bare minimum, you should be doing this activity before you submit it to the app store. You don’t want your app to get past review and get into the hands of the users and have bad things happen while they are using your app. You’ll end up with lots of poor reviews, which will seriously hurt your downloads.
I’d suggest that after you finish up some major new features you should do some quick profiles to make sure everything is in order. The longer you wait, the more potential issues you might find and they might pile up into a larger work item to resolve that might delay your launch. As a team you could define some of these check in’s as part of your dev plan so there is time allocated.
The other time I’d suggest that you fire up Instruments is when you are working with some different pieces of the frameworks you are not as familiar with. With the amount of iOS frameworks and libraries constantly expanding, it’s likely you are going to work with some frameworks you aren’t as familiar with. Most developers will know when this is, and if you have that feeling, you should run some quick profiles to ensure your work is playing nice with the rest of the application. Bringing in 3rd party libraries can also be a good time to profile to ensure that the libraries you are adding are not causing any memory issues that might be out of your control.
Profiling in Xcode
Xcode has been expanding out the ‘Debug Navigator’ to include lots of information that used to be previously buried inside of Instruments. If you hit CMD-6, you can bring up the view that shows performance information about your application. Here you can see some quick summaries of CPU/Memory/Energy/Disk/Network activity and spot check to see if there are any immediate issues. From here you can even start up Instruments by clicking the ‘Profile in Instruments’ button at the top which will ask you transfer that debug session or start a new one in Instruments.
The first thing we’ll take a look at is profiling your CPU usage. To start CPU profiling, we’ll choose to select ‘Profile’ product command and select the device as the target. You want to be sure to use an actual device when doing CPU profiling to get accurate information about the CPU usage. If you target a simulator, you’ll be getting CPU information off your machine and not how it’s running on an actual device which will be very different. Ideally you’ll want to use the slowest device you have to ensure it’ll work on that and anything faster.
CPU Profiling works by taking samples of processes running at set intervals. By default it samples every 1ms, but you can change this to customize the behavior if needed. By seeing what processes are still running between snapshots, it can determine how long things are running.
After the profile build is finished, Instruments will launch and ask you which profiling template to use. For CPU usage, we’ll use the ‘Time Profiler’.
This will give us the initial Instruments view with the Time Profiler view setup. Now to actually run the profiler, you’ll need to click the record button to start the profiling. If for some reason the record button is disabled, it should tell you why on the right hand side of the profiler track near the top. I’ve seen it get stuck in a state where it says the ‘device is offline’, and a device reboot usually fixes it. Once you’ve clicked the record button, it’ll start showing information about what’s happening in your application.
You’ll see the top shows a chart of your CPU usage over time during the recording and the bottom will show the Call Tree of processes that have run. The initial dump of processes doesn’t really look very useful as it’s showing the top of the Call Tree and forces you to expand out the items to drill into the stuff that’s really happening. It is also showing you all kinds of system library activity that you may or may not be interested in. The majority of the time you are probably only interested in the code that you wrote. Luckily there are some quick settings you can change to get to the important information faster.
If you click the gear on the right side for ‘Display Settings’ (⌘2), there are a bunch of options for the ‘Call Tree’. By default most of these options are off and we’ll want to turn them on. Let’s look at what these options do:
- Separate by Thread — Shows the processes by thread to help diagnose overworked threads
- Invert Call Tree — Reverses the stack to show the bottom portion first which is usually much more useful
- Hide System Libraries — Removes system library processes allowing to focus only on your code
- Flatten Recursion — Combines recursive calls into one single entry shown
- Top Functions — Combines the time the function called used plus the time spent by functions called from that function. This helps find your more expensive methods to diagnose
I typically find that it’s most useful to check all these boxes when profiling to quickly get to the information you want to see about your application. Once you check these, you’ll see the Call Tree become much more useful showing your application methods that are using up your CPU.
Now with your filtered list of expensive CPU methods you can begin to try to optimize certain spots in your application. There will likely be some things that you can’t do much about, but you might see some things that you can optimize. I’m not going to go into much detail about how you can optimize your application, but some things to consider would be:
- Offloading non UI processes to different threads
- Caching images, data, or anything being used multiple times that don’t need to always be reloaded
- Reducing the number of UI updates, you sometimes might be updating the UI unnecessarily
One quick tip to mention is that you can use the ‘Extended Detail’ section ( the icon next to the Display Settings or ⌘3) on the right hand side of the ‘Call Tree’ section to see the specific stack trace for what you selected and then you can double click a line in that stack to have Instruments take you to that exact line in your code. From there you can click on the tiny little Xcode icon that will put you back into Xcode to work on that specific method.
After you make some updates to address any of your performance issues, run the same set of steps again with the profiler and check to see if your performance is better and continue this process until you are happy with the performance.
The next type of profiling we’ll look at is Memory profiling. This is often one of the most overlooked issues in developing iOS apps as it doesn’t cause any immediate issues. If you have memory leaks and users continue to use your app, memory will grow and grow until you reach an out of memory situation and the app will crash. Not only is this bad for your app, it’s also not being a good app citizen on that user’s device which might cause low memory situations for other apps. Let’s take a look at how we can use Instruments to make sure we’re not leaking memory and how we might be able to fix some of these situations.
When looking at your memory usage over time, you’ll want to make sure that it doesn’t keep increasing over time.
You don’t want to see growth like this
You should see something like this
The easiest way to take a look at memory usage is again in the ‘Debug Navigator’ when you run your app from Xcode. Here you can select the ‘Memory’ panel and watch memory usage over time. Scanning the chart here can help see any immediate memory issues like usage constantly growing and never going down.
To take a closer look at your memory usage, click the ‘Profile in Instruments’ button and it’ll ask you if you’d like to transfer this session or restart as a new one. Choose to restart the session with Instruments. I haven’t had good luck doing the transfer as some information seems to get lost. This will open Instruments and include the Allocations and Leaks templates that will help you see all the memory allocations and potential leaks that are occurring.
I’m not going to go into too much detail on the Leaks panel, but it will take snapshots for you at specified intervals to see if you have any memory that has been allocated but unable to deallocate. This often happens if you are working in Objective-C and using some of the C libraries where you are responsible for freeing any allocated memory. You can usually find most of these things with the ‘Analyze’ build option, but sometimes you might have a situation where the Analyze tool didn’t find anything, but Instruments caught the issue. If you are working in Swift, these leaks are less common as it takes care of some of the things you used to have to do in Objective-C.
With the memory profiler running, what I find useful to do is to perform a sequence of events multiple times and marking the generation of memory after each sequence. You can then analyze the memory growth between each snapshot. To take a snapshot of the memory you just click the ‘Mark Generation’ button on the ‘Display Settings’ section on the right hand side. If you forget to mark a generation during your sequence, you can always add a new one after the fact and then move it to the spot you want, you just click and hold on the flag near the top and then move it. Once you have a handful of generations, I’d sort them by growth, change the ‘Allocation Type’ to ‘All Heap Allocations’ which will sort them by raw size and remove some system things you may not have control over. Then you might see some things that stick out in terms of memory usage, but it may be hard to see the objects you have created versus some of the primitives that are just using up space.
Now that you have a list of memory allocations, it’s time to start looking to see that everything is in order, but the initial display of this data isn’t immediately useful. If you happen to be working in Swift, all of your Swift objects will get prefixed with the application name so you can just search for your application name in the filter to see all of the things in memory that are your objects. If you are working in Objective-C, it’s a little more tricky to get this information. You basically have to know the names of the things you are looking for. If you happened to have prefixed your files you could search on that prefix and see all your objects that way, or you could also search on some naming scheme you might have used like assuming you named all your view controllers *ViewController, you could search for ‘ViewController’ and see all those objects.
Here in my example screenshot, we can see that I made 4 snapshots and I’m filtering on objects with the name ViewController and I can see that I’m leaking a ServiceViewController between each generation snapshot.
I happened to know exactly what the issue is here in my code here as I put something in the ViewController to cause a retain cycle for this article’s purpose, but for your code you’ll have to dig around and see what’s keeping some of your leaked objects from getting released. A retain cycle is when you have two objects that have strong references to each other and prevents them from being deallocated. I won’t go into all the details about what causes retain cycles, but I’d suggest looking at your delegates and blocks/closures first. They seem to cause the most drama. You’ll want to make sure your delegates are weak and that you are using weak ( or unowned in Swift ) references in your blocks/closures.
If you have trouble immediately seeing what’s holding on to some of your objects, you can use Instruments to give you more information on the allocation summary of the objects in question. If you click the little arrow next to the object, it’ll show you all the allocations of that object, and who is responsible for creating it. Then if you click the little arrow next to one of those allocations, you can see the detailed information about its retain and release count. If that number doesn’t reach 0, it won’t get deallocated. It sometimes takes some detective skills to see what’s causing some of the retains, but a quick tip is to look at anything where the ‘Responsible Library’ is your application or ‘libsystem_blocks’ and skip over anything ‘UIKit’. You can type those items in the search box to filter the list. Then be sure to have the ‘Extended Detail’ shown and you can see the stack trace of what’s going on with each of those.
This really just scratches the surface of all the things that Instruments does, but hopefully this information helps gets you past starting up Instruments and not immediately knowing what to do with it. Once you become more comfortable with the tooling, you’ll end up using it more, and the better your code will become. You’ll become more proactive about checking on things that you know might cause an issue and it’ll become part of your standard workflow.
If you want to learn more about using Instruments, I’d highly recommend watching this WWDC video from 2014 — Improving your app with Instruments. It’ll walk through some of the things discussed in this article with some additional tips and tricks. If you just want to read more about Instruments, check out the user guide from Apple.