home / skills / charleswiltgen / axiom / axiom-concurrency-profiling

This skill helps diagnose and optimize Swift async/await performance by profiling concurrency in Instruments, identifying actor contention and thread pool

npx playbooks add skill charleswiltgen/axiom --skill axiom-concurrency-profiling

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.2 KB
---
name: axiom-concurrency-profiling
description: Use when profiling async/await performance, diagnosing actor contention, or investigating thread pool exhaustion. Covers Swift Concurrency Instruments template, task visualization, actor contention analysis, thread pool debugging.
license: MIT
metadata:
  version: "1.0.0"
---

# 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

1. Profile with Swift Concurrency template
2. Look at main thread → "Swift Tasks" lane
3. Find long blue bars (task executing on main)
4. Check if work could be offloaded

**Solution patterns**:

```swift
// ❌ 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

1. Enable "Swift Actors" instrument
2. Look for serialized access patterns
3. Red = waiting, Blue = executing
4. High red:blue ratio = contention problem

**Solution patterns**:

```swift
// ❌ 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

1. Look for gaps in task execution across all threads
2. Check for blocking primitives
3. Replace with async equivalents

**Common culprits**:

```swift
// ❌ 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

1. Inspect task priorities in Instruments
2. Follow wait chains
3. Ensure critical paths use appropriate priority

```swift
// ✅ 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:

1. **Is work actually async?**
   - Look for suspension points (`await`)
   - Sync code in async function still blocks

2. **Holding locks across await?**
   ```swift
   // ❌ Deadlock risk
   mutex.withLock {
       await something()  // Never!
   }
   ```

3. **Tasks in tight loops?**
   ```swift
   // ❌ 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) }
       }
   }
   ```

4. **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

Overview

This skill helps profile and optimize Swift async/await code using the Swift Concurrency Instruments template. It guides you to diagnose main-thread blocking, actor contention, thread-pool exhaustion, and priority inversion so you can make targeted fixes that restore responsiveness.

How this skill works

It inspects Instruments traces using the Swift Tasks, Swift Actors, and Thread States lanes to visualize task lifetimes, actor access patterns, and thread behavior. The skill highlights running vs waiting vs suspended tasks, shows red:blue contention ratios, and surfaces gaps that indicate pool exhaustion or blocking primitives. It also points to diagnostics like the SWIFT_CONCURRENCY_COOPERATIVE_THREAD_BOUNDS flag for detecting unsafe blocking.

When to use it

  • UI stutters or freezes during async operations
  • Suspect actor serialization or unexpected sequential execution
  • Tasks are queued but not making progress (gaps across threads)
  • Investigating priority inversion or unexpected task wait chains
  • Verifying whether async code is actually suspending vs blocking

Best practices

  • Use the Swift Concurrency Instruments template to view Swift Tasks, Actors, and Thread States together
  • Offload heavy CPU work from @MainActor using Task.detached or nonisolated functions
  • Replace blocking primitives (semaphores, sync I/O) with async equivalents or continuations
  • Split hot actors or reduce isolated state; use nonisolated for pure CPU work
  • Prefer structured concurrency (withTaskGroup) over spawning many loose Tasks
  • Check task priorities and assign explicit priority for critical paths

Example use cases

  • Diagnose a view controller that freezes while loading data and move heavy work off the main actor
  • Detect and resolve actor contention where many callers serialize on a single actor
  • Find thread-pool exhaustion caused by synchronous file or blocking calls inside Tasks
  • Analyze priority inversion where a user-initiated task is waiting behind lower-priority work
  • Audit code for unsafe primitives (DispatchSemaphore.wait, Thread.sleep) in async contexts

FAQ

What does red vs blue mean in the Swift Actors lane?

Blue bars are executing tasks; red bars are tasks waiting due to contention; gray indicates suspended awaiting points.

How do I fix thread pool exhaustion?

Remove blocking calls in Tasks, use async APIs, avoid semaphores in async context, and enable SWIFT_CONCURRENCY_COOPERATIVE_THREAD_BOUNDS to detect issues.