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:
// ❌ Heavy work on MainActor @MainActor class ViewModel: ObservableObject { func process() { let result = heavyComputation() // Blocks UI self.data = result } }
// ✅ Offload heavy work @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:
// ❌ All work serialized through actor actor DataProcessor { func process(_ data: Data) -> Result { heavyProcessing(data) // All callers wait } }
// ✅ Mark heavy work as nonisolated actor DataProcessor { nonisolated func process(_ data: Data) -> Result { heavyProcessing(data) // Runs in parallel }
func storeResult(_ result: Result) {
// Only actor state access serialized
}
}
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:
// ❌ Blocks cooperative thread Task { semaphore.wait() // NEVER do this // ... semaphore.signal() }
// ❌ Synchronous file I/O in async context Task { let data = Data(contentsOf: fileURL) // Blocks }
// ✅ Use async APIs 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
// ✅ Explicit priority for critical work 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?
// ❌ Deadlock risk mutex.withLock { await something() // Never! }
Tasks in tight loops?
// ❌ Overhead may exceed benefit for item in items { Task { process(item) } }
// ✅ Structured concurrency 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