Skip to content

Instruments profiling

Hub › iOS › Advanced › Instruments profiling

Goal

Use Xcode's Instruments to profile the app — capture a Time Profiler trace, find slow code, and detect memory leaks with the Leaks instrument.

Prerequisites

Launch Instruments

Three ways to start:

  1. Xcode: Product → Profile (Cmd+I) → choose a template
  2. Terminal: xcrun instruments (CLI mode)
  3. Standalone: Open Instruments.app from Xcode → Open Developer Tool

Time Profiler

The Time Profiler samples the call stack every 1ms and shows which functions consume the most CPU time.

Common findings in a SwiftUI app

SymptomLikely cause
High body evaluation timeUnnecessary view recomputation — add EquatableView or @ViewBuilder
JSONDecoder.decode at topFetching and decoding on the main actor — move to background Task
modelContext.save() spikesSaving after every toggle — batch saves or debounce

Profile the list

  1. Select Time Profiler template
  2. Tap the record button (red circle)
  3. Scroll through the list several times
  4. Stop recording

The call tree shows that ItemViewModel.load() and JSONDecoder.decode(_:from:) are the top consumers. This confirms you should fetch on a background actor.

Fix: move network fetch to a ModelActor

Create DataFetcher.swift:

swift
import SwiftData

@ModelActor
actor DataFetcher {
    func fetchAndCache() async throws {
        let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
        let (data, _) = try await URLSession.shared.data(from: url)
        let remote = try JSONDecoder().decode([RemoteItem].self, from: data)

        for item in remote {
            let descriptor = FetchDescriptor<CachedItem>(
                predicate: #Predicate { $0.id == item.id }
            )
            if try modelContext.fetch(descriptor).first == nil {
                modelContext.insert(
                    CachedItem(id: item.id, title: item.title, detail: item.body)
                )
            }
        }
        try modelContext.save()
    }
}

Call it from the ViewModel:

swift
let fetcher = DataFetcher(modelContainer: modelContainer)
try await fetcher.fetchAndCache()

Profile again — the main thread CPU drops significantly.

Leaks instrument

  1. Select the Leaks template
  2. Record, then navigate through the app: sign in, favorite items, sign out, sign in again
  3. Stop

The Leaks instrument shows a timeline. Any red bars indicate leaked memory. Click a leak to see the retain cycle:

  • Typical SwiftUI leak: a closure capturing self strongly in a view modifier.
  • Fix: use [weak self] in escaping closures, or prefer @Observable over ObservableObject.

Memory Graph Debugger

For a quick ad-hoc check without Instruments:

  1. Run the app in Xcode
  2. Press the Memory Graph Debugger button (the three diamonds in the debug bar)
  3. Search for objects that should have been deallocated after a navigation pop

Checkpoint

  • Time Profiler: network fetch no longer appears on the main thread.
  • Leaks: zero red bars after 2 minutes of navigation.
  • Memory Graph: no orphaned view models.

Next: TestFlight distribution — archive, export, and submit to App Store Connect.