← All topics/Performance & optimization

Question 2

Using Instruments effectively

An app feels slow, but the cause isn't obvious. How do you use Instruments to narrow it down, and which templates do you usually start with?

Follow-ups

  • Time Profiler vs Allocations vs Leaks vs Memory Graph?
  • What patterns do you look for first?

Answer outline

Match the symptom you can see to the right template first, then drill into the patterns inside it:

  1. 1.CPU or UI hitches: start with Time Profiler. Filter to the main thread, find the hottest symbols and call trees, and look for synchronous I/O, JSON decoding, and regex in hot paths.
  2. 2.Memory climbing: run Allocations, repeat an action, and watch net growth. Unbounded growth usually means a cache without eviction or a retained closure, and pressing Mark Generation between passes shows what survived each one.
  3. 3.Suspected leaks: run Leaks. It flags allocations that nothing can reach anymore, which is the classic leak signature.
  4. 4.Retain cycles (or orphaned view controllers): open Memory Graph in Xcode and follow the reference chains back to whatever keeps the object alive. Strong delegate references and closure captures are the usual links.

Once you're inside a template, work top down. Sort by weight, then turn on Invert Call Tree and Hide System Libraries so the hot spots in your own code surface first. Confirm the finding on a second trace before you change anything.

Principles

  • Profile on a physical device in a Release build with realistic data, because Debug builds and empty databases lie.
  • Take a baseline trace before any fix, so you can prove a change helped instead of assuming it did.
  • Don't chase tiny wins like objc_msgSend until the big bottlenecks are gone.
  • Name your own phases with os_signpost intervals, and vague slowness turns into regions you can compare.

An os_signpost interval in the Points of Interest category gets its own track, so your phases line up against the timeline:

Mark a region in Instruments
import os.signpost

let log = OSLog(subsystem: "com.app", category: .pointsOfInterest)

func loadPage() async throws {
    os_signpost(.begin, log: log, name: "LoadPage")
    defer { os_signpost(.end, log: log, name: "LoadPage") }
    // Fetch and decode the page here
}

Follow-up angles

  • When battery drain or heat is the complaint, use the Energy Log template on a device. It lines up CPU, location, and network activity so you can see which one is burning power.
  • System Trace shows thread scheduling and priority inversion, which is where to look when background work is blocking the UI.