CPU Profiling Mobile Apps (Instruments/Android Profiler)
Developer says "app stutters between screens". This is not data for work. Data is "transition from HomeViewController to DetailViewController takes 380 ms, 240 ms of it in viewDidLoad, and in it 200 ms is synchronous NSJSONSerialization.jsonObject on main thread". This precision is what CPU profiler is for.
Xcode Instruments: Getting Real Data
Instruments launches via Xcode → Product → Profile or ⌘I. For CPU — use Time Profiler (sampling profiler, 1ms interval by default) or CPU Profiler (instrumentation-based, more precise but with overhead).
Time Profiler — first choice for most tasks. Shows call tree with execution time for each method. Critical settings:
- Call Tree Options → Hide System Libraries — remove system framework noise, see only your code
- Separate by Thread — understand which thread stutters
- Invert Call Tree — shows "leaves" of call tree, methods where time really spent
Typical scenario: record 10 seconds of app work, open call tree. [MyImageProcessor processImage:] uses 67% CPU. Expand — vImageScale_ARGB8888 called on main thread from didSelectRowAt. Move to DispatchQueue.global(qos: .userInitiated), apply result via DispatchQueue.main.async — problem solved.
Signposts and os_log for Precise Measurement
System profiler has overhead and noise. For precise single-operation measurement — os_signpost:
import os.signpost
let log = OSLog(subsystem: "com.app", category: "Performance")
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: "Image Processing", signpostID: id)
processImage(data)
os_signpost(.end, log: log, name: "Image Processing", signpostID: id)
In Instruments → Logging track see precise time stamps. Lets measure not "where general stutter" but "how long exactly this operation with different inputs".
Flame Graph and Where to Read It
Xcode 14+ Time Profiler shows flame graph. Wide horizontal rectangles — time-consuming methods. Nesting shows call stack. Key rule: look at "plateaus" — wide blocks without child methods. This is stack "bottom" where time really spent.
Android Studio Profiler: CPU
Android Studio CPU Profiler supports three modes:
| Mode | When to Use | Overhead |
|---|---|---|
| Sample Java/Kotlin Methods | Initial diagnosis | Low |
| Trace Java/Kotlin Methods | Precise analysis, full stack needed | High |
| Sample C/C++ Functions | Native code, NDK | Low |
| System Trace | System events, janky frames | Minimal |
System Trace — most informative for jank analysis. Shows Choreographer#doFrame, RenderThread, hwuiTask, binder calls. See specific frame that lagged and why.
Record via UI or programmatically:
Debug.startMethodTracing("myapp_trace")
// operation
Debug.stopMethodTracing()
.trace file opens in Android Studio Profiler for analysis.
Typical Android Finding
Profiling showed: opening chat screen 180 ms spent on SharedPreferences.getAll() — developer loaded all settings per opening to check flag. SharedPreferences on main thread with large file (2 MB from cached data) — real UI blocker. Switch to DataStore with background read via Flow removed this lag completely.
What We Do in Service
- Record Instruments / Android Profiler sessions for key user scenarios
- Analyze call tree and flame graph, identify top-3 CPU bottlenecks by time
- Add
os_signpost/Tracemarkers for precise critical operation measurement - Eliminate found issues: move to background threads, optimize algorithms, add cache
- Re-profile to confirm improvement
Timelines
Profiling and analysis — 1–2 days. Eliminating found problems — depends on count and complexity: 2 days to 2 weeks. Estimate after analysis.







