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 its 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, (slow network requests or laggy scrolling), you should target those first. But if things seem to be going ok, I’d suggest, at least, you look at your CPU and memory usage during a few runs of the application to ensure everything is behaving as it should.
When to Profile
Before going into this information, I want to note that I don’t expect every developer to actively profile 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 do 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 can define some of these check-in’s as part of your dev plan, so that 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 number 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 show 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’s 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 it 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 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. Without going into too much detail about how you can optimize your application, here are some things to consider
- Offloading non-UI processes to different threads.
- Caching images, data, or anything being used multiple times that do not need to always be reloaded.
- Reducing the number of UI updates (sometimes you 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. 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.
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 you 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 – 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, thought it might 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 happen to have prefixed your files you can search on that prefix and see all your objects that way, or you can search on some naming scheme you might have used. For example, if 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 know exactly what the issue is here in my code, 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 onto 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 can sometimes take 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 will 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.