Concurrency Profiling β Instruments Workflows
Profile and optimize Swift async/await code using Instruments.
When to Use
β
Use when:
- UI stutters during async operations
- Suspecting actor contention
- Tasks queued but not executing
- Main thread blocked during async work
- Need to visualize task execution flow
β Don't use when:
- Issue is pure CPU performance (use Time Profiler)
- Memory issues unrelated to concurrency (use Allocations)
- Haven't confirmed concurrency is the bottleneck
Swift Concurrency Template
What It Shows
| Track |
Information |
| Swift Tasks |
Task lifetimes, parent-child relationships |
| Swift Actors |
Actor access, contention visualization |
| Thread States |
Blocked vs running vs suspended |
Statistics
- Running Tasks: Tasks currently executing
- Alive Tasks: Tasks present at a point in time
- Total Tasks: Cumulative count created
Color Coding
- Blue: Task executing
- Red: Task waiting (contention)
- Gray: Task suspended (awaiting)
Workflow 1: Diagnose Main Thread Blocking
Symptom: UI freezes, main thread timeline full
- Profile with Swift Concurrency template
- Look at main thread β "Swift Tasks" lane
- Find long blue bars (task executing on main)
- Check if work could be offloaded
Solution patterns:
@MainActor
class ViewModel: ObservableObject {
func process() {
let result = heavyComputation()
self.data = result
}
}
@MainActor
class ViewModel: ObservableObject {
func process() async {
let result = await Task.detached {
heavyComputation()
}.value
self.data = result
}
}
Workflow 2: Find Actor Contention
Symptom: Tasks serializing unexpectedly, parallel work running sequentially
- Enable "Swift Actors" instrument
- Look for serialized access patterns
- Red = waiting, Blue = executing
- High red:blue ratio = contention problem
Solution patterns:
actor DataProcessor {
func process(_ data: Data) -> Result {
heavyProcessing(data)
}
}
actor DataProcessor {
nonisolated func process(_ data: Data) -> Result {
heavyProcessing(data)
}
func storeResult(_ result: Result) {
}
}
More fixes:
- Split actor into multiple (domain separation)
- Use Mutex for hot paths (faster than actor hop)
- Reduce actor scope (fewer isolated properties)
Workflow 3: Thread Pool Exhaustion
Symptom: Tasks queued but not executing, gaps in task execution
Cause: Blocking calls exhaust cooperative pool
- Look for gaps in task execution across all threads
- Check for blocking primitives
- Replace with async equivalents
Common culprits:
Task {
semaphore.wait()
semaphore.signal()
}
Task {
let data = Data(contentsOf: fileURL)
}
Task {
let (data, _) = try await URLSession.shared.data(from: fileURL)
}
Debug flag:
SWIFT_CONCURRENCY_COOPERATIVE_THREAD_BOUNDS=1
Detects unsafe blocking in async context.
Workflow 4: Priority Inversion
Symptom: High-priority task waits for low-priority
- Inspect task priorities in Instruments
- Follow wait chains
- Ensure critical paths use appropriate priority
Task(priority: .userInitiated) {
await criticalUIUpdate()
}
Thread Pool Model
Swift uses a cooperative thread pool matching CPU core count:
| Aspect |
GCD |
Swift Concurrency |
| Threads |
Grows unbounded |
Fixed to core count |
| Blocking |
Creates new threads |
Suspends, frees thread |
| Dependencies |
Hidden |
Runtime-tracked |
| Context switch |
Full kernel switch |
Lightweight continuation |
Why blocking is catastrophic:
- Each blocked thread holds memory + kernel structures
- Limited threads means blocked = no progress
- Pool exhaustion deadlocks the app
Quick Checks (Before Profiling)
Run these checks first:
-
Is work actually async?
- Look for suspension points (
await)
- Sync code in async function still blocks
-
Holding locks across await?
mutex.withLock {
await something()
}
-
Tasks in tight loops?
for item in items {
Task { process(item) }
}
await withTaskGroup(of: Void.self) { group in
for item in items {
group.addTask { process(item) }
}
}
-
DispatchSemaphore in async context?
- Always unsafe β use
withCheckedContinuation instead
Common Issues Summary
| Issue |
Symptom in Instruments |
Fix |
| MainActor overload |
Long blue bars on main |
Task.detached, nonisolated |
| Actor contention |
High red:blue ratio |
Split actors, use nonisolated |
| Thread exhaustion |
Gaps in all threads |
Remove blocking calls |
| Priority inversion |
High-pri waits for low-pri |
Check task priorities |
| Too many tasks |
Task creation overhead |
Use task groups |
Safe vs Unsafe Primitives
Safe with cooperative pool:
await, actors, task groups
os_unfair_lock, NSLock (short critical sections)
Mutex (iOS 18+)
Unsafe (violate forward progress):
DispatchSemaphore.wait()
pthread_cond_wait
- Sync file/network I/O
Thread.sleep() in Task
Resources
WWDC: 2022-110350, 2021-10254
Docs: /xcode/improving-app-responsiveness
Skills: axiom-swift-concurrency, axiom-performance-profiling, axiom-synchronization, axiom-lldb (interactive thread state inspection)