Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/performance.md
1# Performance23Use this when:45- Async code is slower than expected or causing UI hangs.6- You need to choose between synchronous, asynchronous, and parallel execution.7- You are profiling concurrency overhead with Instruments.89Skip this file if:1011- The issue is a compiler diagnostic about isolation or Sendable. Use `actors.md` or `sendable.md`.12- You mainly need to fix a memory leak. Use `memory-management.md`.1314Jump to:1516- Core Principles17- Common Performance Issues18- Using Xcode Instruments19- Suspension Points / Reducing Suspensions20- Choosing Execution Style21- Parallelism Costs22- Optimization Checklist2324## Core Principles2526### Measurement is essential2728Can't improve what you don't measure. Establish baseline before optimizing.2930### Start simple, optimize later3132```33Synchronous → Asynchronous → Parallel34```3536Move right only when proven necessary.3738### Three phases of concurrency39401. **No concurrency** - Synchronous method412. **Suspend without parallelism** - Asynchronous method423. **Advanced concurrency** - Parallel execution4344## Common Performance Issues4546### UI hangs4748Too much work on main thread causes interface freezes.4950### Poor parallelization5152Heavy work funneled into single task instead of parallel execution.5354### Actor contention5556Tasks waiting on busy actor, causing unnecessary suspensions.5758## Using Xcode Instruments5960### Swift Concurrency template6162Profile with CMD + I → Select "Swift Concurrency" template.6364**Instruments included**:65- **Swift Tasks**: Track running, alive, total tasks66- **Swift Actors**: Show actor execution and queue size6768### Key metrics6970```71Tasks:72- Total count73- Running vs suspended74- Task states (Creating, Running, Suspended, Ending)7576Actors:77- Queue size78- Execution time79- Contention points8081Main Thread:82- Hangs83- Blocked time84```8586### Task states8788- **Creating**: Task being initialized89- **Running**: Actively executing90- **Suspended**: Waiting (at await)91- **Ending**: Completing9293> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.1: Using Xcode Instruments to find performance bottlenecks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)9495## Identifying Issues9697### Main thread blocked9899```swift100// ❌ All work on main thread101@MainActor102func generateWallpapers() {103Task {104for _ in 0..<100 {105let image = generator.generate() // Blocks main thread106wallpapers.append(image)107}108}109}110```111112**Instruments shows**: Long main thread hang, no parallelism.113114### Solution: Move to background115116```swift117@MainActor118func generateWallpapers() {119Task {120for _ in 0..<100 {121let image = await backgroundGenerator.generate()122wallpapers.append(image)123}124}125}126127actor BackgroundGenerator {128func generate() -> Image {129// Heavy work in background130}131}132```133134### Actor contention135136```swift137actor Generator {138func generate() -> Image {139// Heavy work140}141}142143// ❌ Sequential through actor144for _ in 0..<100 {145let image = await generator.generate() // Queue size = 1146}147```148149**Instruments shows**: Actor queue never exceeds 1, no parallelism.150151### Solution: Remove unnecessary actor152153```swift154struct Generator {155@concurrent156static func generate() async -> Image {157// Heavy work, no shared state158}159}160161// ✅ Parallel execution162for i in 0..<100 {163Task(name: "Image \(i)") {164let image = await Generator.generate()165await addToCollection(image)166}167}168```169170## Suspension Points171172### What creates suspension173174Every `await` is potential suspension point:175176```swift177let data = await fetchData() // May suspend178```179180**Not guaranteed** - if isolation matches, may not suspend.181182### Suspension surface area183184Code between suspension points. Larger = harder to reason about:185- Actor invariants186- Performance187- Thread hops188- Reentrancy189- State consistency190191### Goal192193- Do work before crossing isolation194- Cross once195- Finish job196- Only cross again when necessary197198## Reducing Suspensions199200### 1. Use synchronous methods201202```swift203// ❌ Unnecessary async204private func scale(_ image: CGImage) async { }205206func process(_ image: CGImage) async {207let scaled = await scale(image) // Suspension point208}209210// ✅ Synchronous helper211private func scale(_ image: CGImage) { }212213func process(_ image: CGImage) async {214let scaled = scale(image) // No suspension215}216```217218**Rule**: If method doesn't need to suspend, don't mark async.219220### 2. Prevent actor reentrancy221222```swift223// ❌ Reenters actor224actor BankAccount {225func deposit(_ amount: Int) async {226balance += amount227await logTransaction() // Leaves actor228balance += bonus // Reenters - state may have changed229}230}231232// ✅ Complete work before leaving233actor BankAccount {234func deposit(_ amount: Int) async {235balance += amount236balance += bonus237await logTransaction() // Leave after state changes238}239}240```241242### 3. Inherit isolation243244```swift245// ❌ Switches isolation246@MainActor247func update() async {248await process() // Switches away from main actor249}250251// ✅ Inherits isolation (still requires await -- but no executor hop)252@MainActor253func update() async {254await process() // Stays on main actor when nonisolated(nonsending)255}256257nonisolated(nonsending) func process() async { }258```259260> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.2: Reducing suspension points by managing isolation effectively](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)261262### 4. Use non-suspending APIs263264```swift265// ❌ May suspend266try await Task.checkCancellation()267268// ✅ No suspension269if Task.isCancelled {270return271}272```273274### 5. Match Task entry isolation to its synchronous prefix275276For unstructured `Task { ... }`, decide startup isolation from the synchronous prefix (everything before the first `await`). If that prefix needs main-actor access, keep inherited `@MainActor` entry. If the prefix does not need main actor, use `Task { @concurrent in ... }` and hop back with `MainActor.run` only when UI-owned mutation is required. A trivial non-main line (such as `print`) does **not** justify `@concurrent` when main-actor work already exists in the same prefix.277278```swift279// ❌ Synchronous prefix is empty; first work hops away280Task {281await hopToOtherIsolationDomain()282}283284// ❌ Synchronous prefix is only `print` (trivial, non-main); first await hops away285Task {286print("Also not main-thread-bound")287await hopToOtherIsolationDomain()288}289290// ✅ Start off the main actor, hop back only for UI work291Task { @concurrent in292await hopToOtherIsolationDomain()293await MainActor.run { updateUI() }294}295296// ✅ Synchronous prefix DOES contain main-actor work — keep inheritance297Task {298print("debug") // trivial, non-main — rides along299self.isLoading = true // needs @MainActor, before any await300await fetchData()301}302```303304Delayed retry is one specialization of this rule:305306```swift307// ❌ Can wait for MainActor, then suspend immediately308registrationRetryTask = Task { @MainActor [weak self] in309try? await Task.sleep(for: .milliseconds(100))310guard let self else { return }311self.registrationRetryTask = nil312self.updateConnectedTargetWindow()313}314```315316The delay itself is not UI work. Starting on `@MainActor` can add an avoidable executor wait before reaching `Task.sleep`, especially when scheduled from another executor or while the main actor is busy.317318```swift319// ✅ Sleep off-main, hop back only for the UI-owned work320registrationRetryTask = Task { @concurrent [weak self] in321do {322try await Task.sleep(for: .milliseconds(100))323} catch is CancellationError {324return325}326guard let self else { return }327328await MainActor.run {329self.registrationRetryTask = nil330self.updateConnectedTargetWindow()331}332}333```334335Use this rule for any unstructured task: delayed retries, backoff, timer-like work, off-main computation, and actor hops. The key check is always “what runs before the first `await`?”, not “what does the task eventually do?”.336337### 6. Embrace parallelism338339```swift340// ❌ Sequential341for url in urls {342let image = await download(url)343images.append(image)344}345346// ✅ Parallel347await withTaskGroup(of: Image.self) { group in348for url in urls {349group.addTask { await download(url) }350}351for await image in group {352images.append(image)353}354}355```356357## Analyzing Suspensions in Instruments358359### View task states3603611. Select Swift Tasks instrument3622. Switch to "Task States" view3633. Look for Suspended states3644. Check suspension duration365366### Navigate to code3673681. Click task state (Running/Suspended)3692. Open Extended Detail3703. Click related method3714. Use "Open in Source Viewer"372373### Predict suspensions374375```swift376Task {377// State 1: Running378// State 2: Suspended (switch to background)379let data = await backgroundWork()380// State 3: Running (in background)381// State 4: Suspended (switch to main actor)382// State 5: Running (on main actor)383await MainActor.run {384updateUI(data)385}386}387```388389### Optimization example390391```swift392// Before: Two suspensions393Task {394let data = await generate() // Suspension 1395self.items.append(data) // Suspension 2 (back to main)396}397398> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.3: Using Xcode Instruments to detect and remove suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)399400// After: One suspension401Task { @concurrent in402let data = generate() // No suspension (synchronous)403await MainActor.run {404self.items.append(data) // Suspension 1 (to main)405}406}407```408409## Choosing Execution Style410411### Decision checklist412413**Use async/parallel if**:414- [ ] Blocks main actor visibly (>16ms)415- [ ] Scales with data (N items → N cost)416- [ ] Involves I/O (network, disk)417- [ ] Benefits from combining operations418- [ ] Called frequently419420**2+ checks** → async/parallel justified.421422### Start synchronous423424```swift425// Start here426func processData(_ data: Data) -> Result {427// Fast, in-memory work428}429```430431**Only move to async if**:432- Instruments show main thread hang433- User reports sluggishness434- Work scales with input size435436### When to use async437438```swift439func processData(_ data: Data) async -> Result {440// Use when:441// - Touches persistent storage442// - Parses large datasets443// - Network communication444// - Proven slow by profiling445}446```447448### When to use parallel449450```swift451await withTaskGroup(of: Result.self) { group in452for item in items {453group.addTask { await process(item) }454}455}456457// Use when:458// - Multiple independent operations459// - Time-to-first-result matters460461> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.4: How to choose between serialized, asynchronous, and parallel execution](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)462// - Work scales with collection size463// - Proven beneficial by profiling464```465466## Parallelism Costs467468### Tradeoffs469470**Benefits**:471- Faster completion (if CPU-bound)472- Better resource utilization473- Improved responsiveness474475**Costs**:476- Increased memory pressure477- CPU scheduling overhead478- System resource saturation479- Battery drain480- Thermal impact481482### When parallelism hurts483484```swift485// ❌ Over-parallelization486for i in 0..<1000 {487Task { await lightWork(i) }488}489// Creates 1000 tasks for trivial work490```491492**Better**: Batch work or use fewer tasks.493494## UX-Driven Decisions495496### Smooth animations > raw speed497498```swift499// 80ms on main thread, but animation stutters500@MainActor501func process() {502heavyWork() // Freezes UI for 1 frame503}504505// 100ms total, but smooth UI506@MainActor507func process() async {508await backgroundWork() // UI stays responsive509}510```511512**Perception**: Smooth feels faster than raw speed.513514### Progress indication515516```swift517@MainActor518func loadItems() async {519isLoading = true520521for i in 0..<100 {522let item = await fetchItem(i)523items.append(item)524progress = Double(i) / 100 // Incremental updates525}526527isLoading = false528}529```530531Background work + progress = feels faster.532533## Optimization Checklist534535Before optimizing, ask:536537- [ ] Have I profiled with Instruments?538- [ ] Is main thread actually blocked?539- [ ] Can this be synchronous?540- [ ] Am I over-parallelizing?541- [ ] Is actor contention the issue?542- [ ] Are suspensions necessary?543- [ ] Does UX require background work?544- [ ] Will this scale with data?545546Anti-patterns to avoid with unstructured tasks:547- Starting on inherited `@MainActor` when nothing in the synchronous prefix (before first `await`) needs main actor.548- Moving trivial non-main lines off `@MainActor` when the same synchronous prefix already includes required main-actor mutation.549550## Common Patterns551552### Move heavy work to background553554```swift555// Before556@MainActor557func generate() {558for _ in 0..<100 {559let item = heavyGeneration()560items.append(item)561}562}563564// After565@MainActor566func generate() async {567for _ in 0..<100 {568let item = await backgroundGenerate()569items.append(item)570}571}572573@concurrent574func backgroundGenerate() async -> Item {575// Heavy work off main thread576}577```578579### Parallelize independent work580581```swift582// Before: Sequential583for url in urls {584let image = await download(url)585images.append(image)586}587588// After: Parallel589await withTaskGroup(of: Image.self) { group in590for url in urls {591group.addTask { await download(url) }592}593for await image in group {594images.append(image)595}596}597```598599### Reduce actor hops600601```swift602// Before: Multiple hops603actor Store {604func process() async {605let a = await fetch1() // Hop 1606let b = await fetch2() // Hop 2607let c = await fetch3() // Hop 3608combine(a, b, c)609}610}611612// After: Batch fetches613actor Store {614func process() async {615async let a = fetch1()616async let b = fetch2()617async let c = fetch3()618combine(await a, await b, await c) // One hop619}620}621```622623## Best Practices6246251. **Profile before optimizing** - measure baseline6262. **Start synchronous** - add async only when needed6273. **Use Instruments regularly** - catch issues early6284. **Name tasks** - easier debugging in Instruments6295. **Check suspension count** - reduce unnecessary awaits6306. **Avoid premature parallelism** - has costs6317. **Consider UX** - smooth > fast6328. **Batch actor work** - reduce contention6339. **Test on real devices** - simulators lie63410. **Monitor in production** - real usage patterns differ635636## Debugging Performance637638### Instruments workflow6396401. Profile with Swift Concurrency template6412. Identify main thread hangs6423. Check task parallelism6434. Analyze actor queue sizes6445. Review suspension points6456. Navigate to problematic code6467. Apply optimizations6478. Re-profile to verify648649### Red flags in Instruments650651- Main thread blocked >16ms652- Actor queue size always 1653- High suspension count654- Tasks created but not running655- Excessive task creation (1000+)656657## Further Learning658659For real-world optimization examples, profiling techniques, and advanced performance patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).660661