A step-by-step guide to using the Time Profiler template in Instruments to find the slow code behind janky scrolling and high CPU usage in your iOS app.
The Time Profiler is a good tool whenever your app feels slow or your CPU usage looks high. Classic symptoms include stuttering scroll, a laggy animation, or a tab that takes too long to appear.
It works by sampling the call stacks of your app's threads at regular intervals. Functions that show up in many samples are functions where your app is spending its time.
This sampling approach is lightweight enough to run on a real device without distorting results too much. That makes it a safe first stop for many speed problems.
Before you start, make sure you have a concrete, reproducible scenario in mind. Profiling is far more useful when you know exactly which action you are about to measure.
Open your project in Xcode and select a real device as the run destination if you can. Real hardware gives you more honest numbers.
From the Xcode menu choose Product, then Profile, or use the standard Profile shortcut. This builds your app in a release-style configuration and then launches Instruments.
Profiling a release-style build matters because compiler optimizations change where time is spent. A debug build can mislead you about the real hot paths.
Wait for the build to finish. Xcode will hand the built app over to Instruments automatically.
When Instruments opens, it presents a gallery of templates. Select Time Profiler and confirm to create a new tracing document.
Instruments will show a target at the top of the window, which should already be set to your app on your chosen device. Confirm that the target is correct.
You will see an empty timeline ready to record. This is your blank canvas for the measurement you are about to take.
If you do not see your device, check that it is connected, unlocked, and trusted, then reopen the template.
Press the record button to start the trace. Instruments begins sampling immediately.
Now perform exactly the interaction you want to investigate, such as scrolling the laggy list or opening the slow screen. Keep the session short and focused on that one action.
When you have captured the behavior, press stop. A long trace is harder to read, so aim to capture just enough to include the problem.
You now have a recorded timeline. The track will show CPU activity over the duration of your session.
Open the detail area and switch to the Call Tree view. This shows the aggregated stack traces collected during the run.
Enable the common Call Tree options to make the data readable. Inverting the call tree surfaces the leaf functions where time is actually spent, and hiding system libraries focuses attention on your own code.
Look for the functions with the highest weight. These are where the most CPU is being consumed during your interaction.
Double-click a heavy symbol to jump to the corresponding source in your project. This is where Instruments connects measurement back to the code you can change.
Janky UI is usually a main-thread problem. The main thread is responsible for layout, drawing, and event handling, so heavy work there freezes the interface.
Use the thread filtering options to focus on the main thread specifically. If expensive functions appear there, you have likely found the cause of your jank.
The common fix is to move heavy work off the main thread, for example onto a background queue, and only return to the main thread to update the UI. Caching expensive results is another frequent remedy.
Identify which heavy operations truly need to be on the main thread and which do not. That distinction usually points directly at the fix.
Make one focused change based on what the trace told you. Resist the urge to change five things at once, because then you cannot tell what helped.
Rebuild with Product then Profile and record the same interaction again. Use the identical scenario so the comparison is fair.
Compare the new call tree against the old one. The function you optimized should now consume noticeably less time, or disappear from the top of the list.
Repeat the loop until the interaction feels smooth and the numbers agree. Profiling is iterative, and small confirmed wins add up quickly.
It helps to know what the Time Profiler is and is not doing. It is a sampling profiler, which means it periodically pauses to record the call stacks of your threads rather than logging every single function call.
This sampling model is why the tool is relatively cheap to run. It also means the results are statistical: a function that appears in many samples was on the stack for a large share of the time, but you are seeing an estimate rather than an exact count of calls.
The practical consequence is that very short, infrequent functions may not show up, while anything that genuinely dominates your CPU time will. That tradeoff is usually exactly what you want when chasing a slow interaction.
If you need precise call counts or timing of specific operations, that is a different kind of measurement, often better served by targeted logging or signposts. For finding the hot path behind jank, sampling is the right and efficient choice.
Keep this in mind when you interpret the call tree. A function with a high self time was genuinely busy; a function with high total time but low self time was mostly waiting on the work it called. Reading those two columns together tells you whether to optimize a function directly or look deeper into what it invokes.
Profile on a real device when possible, and prefer a device similar to what your users carry. The simulator can hide or exaggerate problems.
Close other heavy apps and avoid running on a thermally throttled, low-battery device. Background noise pollutes your measurements.
Keep traces short and named clearly so you can revisit them later. A tidy set of before-and-after traces is excellent evidence when you report a fix.
Finally, remember that the Time Profiler measures where time goes, not why your algorithm is wrong. Pair it with your own understanding of the code to turn a hot symbol into a real solution.
The simulator runs on your Mac's processor, which is usually far more powerful than a phone. Real-device profiling reflects the performance your users actually experience.
Inverting the call tree groups results by the leaf functions where time is actually spent, rather than by top-level entry points. This makes it easier to spot the specific hot function.
Enable the option to hide system libraries and separate by thread. That focuses the call tree on your own code, which is what you can actually change.
Sampling adds some overhead but is light enough for practical use on real devices. Keep sessions short and focused for the cleanest data.