iOS App Performance: Instruments & beyond
Users don’t like to wait. They don’t (and shouldn’t) care about what does an app need to initialise, they just want to accomplish their task as quickly as possible. Your application should start almost instantaneously, its interface should be buttery smooth. The app performance is one of the key advantages in the competitive software market.
As developers, we also want to be proud of the apps we work on.
However, performance optimization is a tricky problem. Most of the bottlenecks are counterintuitive. With no proper measurements, it is extremely hard to figure out what is slowing down your app.
To be able to optimize your app’s performance, you should make decisions based on data. In this piece I will show how to get this data by measuring performance of the different areas of your app.
The areas I will touch are:
- CPU, GPU, Memory & Power usage of your app;
- startup time;
- performance metrics to collect from your users.
Let’s get started!
CPU, GPU, Memory & Power usage
The first task is to profile your app to find inefficient code that overuses CPU, GPU or memory. Apple has a great tool to accomplish that: Instruments.
There are 4 main areas to focus on:
- CPU (“Time Profiler” tool);
- GPU (“Core Animation” tool);
- Memory Usage (“Allocations” tool);
- Power Usage (“Energy diagnostics” tool).
WWDC videos are the best source of information about using Instruments to profile your app.
These are some picks to start with:
- Learning Instruments;
- iOS Performance 1, 2, 3;
- Improving You App With Instruments;
- Advanced Graphics & Animations for iOS Apps;
- Profiling In-Depth;
- Cocoa Touch Best Practices;
- iOS Performance and Power Optimization with Instruments;
- Polishing Your App.
Next important thing to measure is responsiveness of the UI. Touch handling happens in the main thread. When you have time-consuming operations there, your app become sluggish.
Some operations might take time, even when they don’t use CPU. If you have synchronous calls in the main thread, measure the time spent on these calls.
To measure this, you can use logs.
One more approach was described by developers of Viber. They have a special thread that watches the main one and checks that it doesn’t get blocked for more than 400 ms.
More information can be found in the presentation itself (PDF, 7MB).
Use this data to detect calls that takes too much time (400 ms is a good threshold, you can read this book for more information) and either optimize them or move it out of the main thread.
Next important thing to measure is how fast your app starts. Typical user spends just a few minutes in your app. Long startup time leads to frustration.
There are 2 cases of how your app can be started:
- Cold startup: your app’s process wasn’t running, it is now being launched by the OS.
- Warm startup: your app was minimised but wasn’t killed. It is being restored from background.
This section is devoted to the cold startup since it is more resource-heavy operation.
There is the startup sequence of an iOS app.
1. Measure the total time spent on startup
We should measure the time spent between the beginning of main() to the end of applicationDidBecomeActive:.
Watch that this time doesn’t get worse as you introduce new features. Try to keep the cold startup time under 1 sec.
2. Measure the time for the phases of launch sequence
It is usually not enough to know just the total time spent on launching. It is also important to know what phase of the startup sequence slows it down.
The most important phases to look at are:
- -[AppDelegate application:didFinishLaunchingWithOptions:] — this callback is called while the launch image (or storyboard) is shown. As soon as you app returns from this method, actual UI starts loading.
- -[UIViewController loadView] — if your app loads a custom view, this is the place where the view is initialised.
- -[UIViewController viewDidLoad] — the view was loaded; time for the final initialisations.
- -[AppDelegate applicationDidBecomeActive:]— the UI have been already initialised, but it is still blocked until this callback is finished. This method is also called when your app is being restored from background.
If some of the methods is taking too much time, optimise it.
3. Measure the startup time “under pressure”
There is one important difference between the real world and the typical test environment.
Your app doesn’t live in an isolation in the real world.
User usually switches to your app from another app.
The “another app” can be really heavy.
It is really important to measure the startup time in the conditions when your app is starting while another, heavy app is at the same time going to the background and trying to save its data.
That testing can uncover some unexpected results. The code, that was completely harmless before, might slow down your app significantly in that conditions.
4. App has already started but is still useless
If your app is not useful as soon as it has loaded the UI, it didn’t really finish launching. Even if the UI is loaded and responsive, but there is some data you need to load to be ready, count it as a startup phase as well.
Performance metrics to collect from your users
All the previous measurements are possible in the test environment. They are necessary, but not sufficient. If your app is popular, if your userbase is distributed across the globe, some of your users might have an environment that is vastly different from what you expect.
They might have different:
- network conditions;
- software (OS version, Jailbreak…);
- amount of free space on the device;
They might use the app differently either.
You might get one-star reviews with complains (“Your app is slow!”), even if all the metrics you measure in-lab are in the safe zone.
What to do about it?
Define a set of performance metrics (or KPI) and gather them from your real users. You can use almost any analytics package to do this.
There are examples of the KPIs you can get from your users:
- Total cold startup time.
- Total warm startup time.
- Startup time for the startup phases.
- Time spent to download the necessary data from server.
- Number of times, when main thread is blocked for > 400 ms.
- Number of memory warnings.
- Number of FOOMS.
- Lengths of the operations when the UI is blocked or unusable.
The analytics packages will let you to distribute put these values into segments, together with device type, country or network operator. That might bring insights on what performance issues your users experience and how to fix it.
As you can see, performance measurements go beyond just launching Instruments.app. There are also other valuable places to look at.
Some of the described methods are fast and easy to implement, the others require more time and effort. Nevertheless, they will help you to monitor your app performance, to find and resolve issues and to make your app more enjoyable to use.
Good luck and 5-star reviews!