
Debugging Instruments
- 2.8k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
debugging-instruments is an iOS skill for LLDB debugging, Memory Graph leak analysis, hang diagnostics, and Instruments performance profiling.
About
Debugging and Instruments guides iOS crash diagnosis, memory debugging, hang detection, build failure triage, and performance profiling with LLDB, Memory Graph Debugger, and Instruments. LLDB sections cover po versus v for locals, breakpoint management with conditions and logpoints, expression evaluation, watchpoints, and symbolic breakpoints for Auto Layout and malloc errors. Memory debugging documents Memory Graph workflow, common retain cycle patterns in closures, delegates, and timers, plus Allocations and Leaks Instruments templates with Mark Generation isolation between user actions. Hang diagnostics explain main thread blocking thresholds, Thread Checker, os_signpost intervals, and MetricKit hang references. Build failure triage and Instruments overview sections address CPU, memory, energy, and network profiling templates. A common mistakes list and review checklist guard against debugging pitfalls before shipping iOS builds to production testers and stakeholders reviewing stability reports.
- LLDB commands for breakpoints, watchpoints, and expression eval.
- Memory Graph Debugger leak and retain cycle pattern fixes.
- Instruments Allocations, Leaks, and Mark Generation workflows.
- Main thread hang detection with Thread Checker and signposts.
- Common mistakes list and review checklist before ship.
Debugging Instruments by the numbers
- 2,806 all-time installs (skills.sh)
- +140 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #28 of 598 Debugging skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
debugging-instruments capabilities & compatibility
- Capabilities
- lldb breakpoint, watchpoint, and expression work · memory graph leak detection and retain cycle fix · instruments allocations and leaks template usage · main thread hang identification and signpost mar · build failure triage guidance · cpu, memory, energy, and network profiling overv
- Use cases
- debugging · testing
- Platforms
- macOS
- Pricing
- Free
What debugging-instruments says it does
A hang occurs when the main thread is blocked for > 250ms
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill debugging-instrumentsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.8k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I diagnose iOS crashes, retain cycles, main thread hangs, and profile CPU or memory bottlenecks?
Debug iOS crashes, memory leaks, retain cycles, main thread hangs, and profile CPU, memory, energy, and network with LLDB and Instruments.
Who is it for?
iOS developers debugging crashes, memory leaks, or performance issues in Swift and UIKit or SwiftUI apps.
Skip if: Skip for server-side debugging, Android profiling, or App Store submission metadata only.
When should I use this skill?
User reports iOS crashes, memory leaks, retain cycles, hangs, slow rendering, or needs Instruments profiling.
What you get
Structured LLDB and Instruments workflow with retain cycle fixes, hang markers, and profiling template selection.
- Root-cause diagnosis
- Instruments trace interpretation
- Retain-cycle fix recommendations
By the numbers
- Instruments profiling covers 4 dimensions: CPU, memory, energy, and network
Files
Debugging and Instruments
Diagnose crashes, memory leaks, retain cycles, main thread hangs, and performance bottlenecks in iOS apps using LLDB, Memory Graph Debugger, and Instruments. Covers breakpoint workflows, memory graph analysis, hang detection, build failure triage, and Instruments profiling for CPU, memory, energy, and network.
Contents
- LLDB Debugging
- Memory Debugging
- Hang Diagnostics
- Build Failure Triage
- Instruments Overview
- Common Mistakes
- Review Checklist
- References
LLDB Debugging
Essential Commands
(lldb) po myObject # Print object description (calls debugDescription)
(lldb) p myInt # Print with type info (uses LLDB formatter)
(lldb) v myLocal # Frame variable — fast, no code execution
(lldb) bt # Backtrace current thread
(lldb) bt all # Backtrace all threads
(lldb) frame select 3 # Jump to frame #3 in the backtrace
(lldb) thread list # List all threads and their states
(lldb) thread select 4 # Switch to thread #4Use v over po when you only need a local variable value — it does not execute code and cannot trigger side effects.
Breakpoint Management
(lldb) br set -f ViewModel.swift -l 42 # Break at file:line
(lldb) br set -n viewDidLoad # Break on function name
(lldb) br set -S setValue:forKey: # Break on ObjC selector
(lldb) br modify 1 -c "count > 10" # Add condition to breakpoint 1
(lldb) br modify 1 --auto-continue true # Log and continue (logpoint)
(lldb) br command add 1 # Attach commands to breakpoint
> po self.title
> continue
> DONE
(lldb) br disable 1 # Disable without deleting
(lldb) br delete 1 # Remove breakpointExpression Evaluation
(lldb) expr myArray.count # Evaluate Swift expression
(lldb) e -l swift -- import UIKit # Import framework in LLDB
(lldb) e -l swift -- self.view.backgroundColor = .red # Modify state at runtime
(lldb) e -l objc -- (void)[CATransaction flush] # Force UI update after changesAfter modifying a view property in the debugger, call CATransaction.flush() to see the change immediately without resuming execution.
Watchpoints
(lldb) w set v self.score # Break when score changes
(lldb) w set v self.score -w read # Break when score is read
(lldb) w modify 1 -c "self.score > 100" # Conditional watchpoint
(lldb) w list # Show active watchpoints
(lldb) w delete 1 # Remove watchpointWatchpoints are hardware-backed (limited to ~4 on ARM). Use them to find unexpected mutations — the debugger stops at the exact line that changes the value.
Symbolic Breakpoints
Set breakpoints on methods without knowing the file. Useful for framework or system code:
(lldb) br set -n "UIViewController.viewDidLoad"
(lldb) br set -r ".*networkError.*" # Regex on symbol name
(lldb) br set -n malloc_error_break # Catch malloc corruption
(lldb) br set -n UIViewAlertForUnsatisfiableConstraints # Auto Layout issuesIn Xcode, use the Breakpoint Navigator (+) to add symbolic breakpoints for common diagnostics like -[UIApplication main] or swift_willThrow.
Memory Debugging
Memory Graph Debugger Workflow
1. Run the app in Debug configuration. 2. Reproduce the suspected leak (navigate to a screen, then back). 3. Tap the Memory Graph button in Xcode's debug bar. 4. Look for purple warning icons — these indicate leaked objects. 5. Select a leaked object to see its reference graph and backtrace.
Enable Malloc Stack Logging (Scheme > Diagnostics) before running so the Memory Graph shows allocation backtraces.
Common Retain Cycle Patterns
Closure capturing self strongly:
// LEAK — closure holds strong reference to self
class ProfileViewModel {
var onUpdate: (() -> Void)?
func startObserving() {
onUpdate = {
self.refresh() // strong capture of self
}
}
}
// FIXED — use [weak self]
func startObserving() {
onUpdate = { [weak self] in
self?.refresh()
}
}Strong delegate reference:
// LEAK — strong delegate creates a cycle
protocol DataDelegate: AnyObject {
func didUpdate()
}
class DataManager {
var delegate: DataDelegate? // should be weak
}
// FIXED — weak delegate
class DataManager {
weak var delegate: DataDelegate?
}Timer retaining target:
// LEAK — Timer.scheduledTimer retains its target
timer = Timer.scheduledTimer(
timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true
)
// FIXED — use closure-based API with [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}Instruments: Allocations and Leaks
- Allocations template: Track memory growth over time. Use the
"Mark Generation" feature to isolate allocations created between user actions (e.g., open/close a screen).
- Leaks template: Automatically detects reference cycles at runtime.
Run alongside Allocations for a complete picture.
- Filter by your app's module name to exclude system allocations.
Malloc Stack Logging
Enable in Scheme > Run > Diagnostics > Malloc Stack Logging (All Allocations). This records the call stack for every allocation, letting the Memory Graph Debugger and leaks CLI show where objects were created.
# CLI leak detection
leaks --atExit -- ./MyApp.app/MyApp
# Symbolicate with dSYMs for readable stacksHang Diagnostics
Identifying Main Thread Hangs
A hang occurs when the main thread is blocked for > 250ms (noticeable) or
1s (severe). Common detection tools:
- Thread Checker (Xcode Diagnostics): warns about non-main-thread UI calls
- os_signpost and
OSSignposter: mark intervals for Instruments - MetricKit hang diagnostics: production hang detection (see
metrickit skill for MXHangDiagnostic)
import os
let signposter = OSSignposter(subsystem: "com.example.app", category: "DataLoad")
func loadData() async {
let state = signposter.beginInterval("loadData")
let result = await fetchFromNetwork()
signposter.endInterval("loadData", state)
process(result)
}Using the Time Profiler
1. Product > Profile (Cmd+I) to launch Instruments. 2. Select the Time Profiler template. 3. Record while reproducing the slow interaction. 4. Focus on the main thread — sort by "Weight" to find hot paths. 5. Check "Hide System Libraries" to see only your code. 6. Double-click a heavy frame to jump to source.
Common Hang Causes
| Cause | Symptom | Fix |
|---|---|---|
| Synchronous I/O on main thread | Network/file reads block UI | Move to Task { } or background actor |
| Lock contention | Main thread waiting on a lock held by background work | Use actors or reduce lock scope |
| Layout thrashing | Repeated layoutSubviews calls | Batch layout changes, avoid forced layout |
| JSON parsing large payloads | UI freezes during data load | Parse on a background thread |
| Synchronous image decoding | Scroll jank on image-heavy lists | Use AsyncImage or decode off main thread |
Build Failure Triage
Reading Compiler Diagnostics
- Start from the first error — subsequent errors are often cascading.
- Search for the error code (e.g.,
error: cannot convert) in the build log. - Use Report Navigator (Cmd+9) for the full build log with timestamps.
SPM Dependency Resolution
# Common: version conflict
error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
# Fix: check Package.resolved and update version ranges
# Reset package caches if needed:
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build
swift package resolveModule Not Found / Linker Errors
| Error | Check |
|---|---|
No such module 'Foo' | Target membership, import paths, framework search paths |
Undefined symbol | Linking phase missing framework, wrong architecture |
duplicate symbol | Two targets define same symbol; check for ObjC naming collisions |
Build settings to inspect first:
FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGSSWIFT_INCLUDE_PATHSBUILD_LIBRARY_FOR_DISTRIBUTION(for XCFrameworks)
Instruments Overview
Template Selection Guide
| Template | Use When |
|---|---|
| Time Profiler | CPU is high, UI feels slow, need to find hot code paths |
| Allocations | Memory grows over time, need to track object lifetimes |
| Leaks | Suspect retain cycles or abandoned objects |
| Network | Inspecting HTTP request/response timing and payloads |
| SwiftUI | Profiling view body evaluations and update frequency |
| Core Animation | Frame drops, off-screen rendering, blending issues |
| Energy Log | Battery drain, background energy impact |
| File Activity | Excessive disk I/O, slow file operations |
| System Trace | Thread scheduling, syscalls, virtual memory faults |
xctrace CLI for CI Profiling
# Record a trace from the command line
xcrun xctrace record --device "My iPhone" \
--template "Time Profiler" \
--output profile.trace \
--launch MyApp.app
# Export trace data as XML for automated analysis
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
# List available templates
xcrun xctrace list templates
# List connected devices
xcrun xctrace list devicesUse xctrace in CI pipelines to catch performance regressions automatically. Compare exported metrics between builds.
Common Mistakes
DON'T: Use print() for debugging instead of os.Logger
print() output is not filterable, has no log levels, and is not automatically stripped from release builds. It pollutes the console and makes it impossible to isolate relevant output.
// WRONG — unstructured, not filterable, stays in release builds
print("user tapped button, state: \(viewModel.state)")
print("network response: \(data)")
// CORRECT — structured logging with Logger
import os
let logger = Logger(subsystem: "com.example.app", category: "UI")
logger.debug("Button tapped, state: \(viewModel.state, privacy: .public)")
logger.info("Network response received, bytes: \(data.count)")Logger messages appear in Console.app with filtering by subsystem and category, and .debug messages are written to the in-memory log store only (not persisted to disk in release builds).
DON'T: Forget to enable Malloc Stack Logging before memory debugging
Without Malloc Stack Logging, the Memory Graph Debugger shows leaked objects but cannot display allocation backtraces, making it difficult to find the code that created them.
// WRONG — open Memory Graph without enabling Malloc Stack Logging
// Result: leaked objects visible but no allocation backtrace
// CORRECT — enable BEFORE running:
// Scheme > Run > Diagnostics > check "Malloc Stack Logging: All Allocations"
// Then run, reproduce the leak, and open Memory GraphDON'T: Debug optimized code expecting full variable visibility
In Release (optimized) builds, the compiler may inline functions, eliminate variables, and reorder code. LLDB cannot display optimized-away values.
// WRONG — profiling with Debug build, debugging with Release build
// Debug builds: extra runtime checks distort perf measurements
// Release builds: variables show as "<optimized out>" in debugger
// CORRECT approach:
// Debugging: use Debug configuration (full symbols, no optimization)
// Profiling: use Release configuration (realistic performance)DON'T: Stop on every loop iteration without conditional breakpoints
Breaking on every iteration wastes time and makes it hard to find the specific case you care about.
// WRONG — breakpoint on line inside loop, stops 10,000 times
for item in items {
process(item) // breakpoint here stops on EVERY item
}
// CORRECT — use a conditional breakpoint:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// Or in Xcode: right-click breakpoint > Edit > add ConditionDON'T: Ignore Thread Sanitizer warnings
Thread Sanitizer (TSan) warnings indicate data races that may only crash intermittently. They are real bugs, not false positives.
// WRONG — ignoring TSan warning about concurrent access
var cache: [String: Data] = [:] // accessed from multiple threads
// CORRECT — protect shared mutable state
actor CacheActor {
var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { cache[key] }
func set(_ key: String, _ value: Data) { cache[key] = value }
}Enable TSan: Scheme > Run > Diagnostics > Thread Sanitizer. Note: TSan cannot run simultaneously with Address Sanitizer.
Review Checklist
- [ ] Using
os.Loggerinstead ofprint()for diagnostic output - [ ] Malloc Stack Logging enabled before memory debugging sessions
- [ ] Memory Graph Debugger checked after dismiss/dealloc flows
- [ ] Delegates declared as
weak varto prevent retain cycles - [ ] Closures stored as properties use
[weak self]capture lists - [ ] Timers use closure-based API with
[weak self] - [ ] Thread Sanitizer enabled in test schemes
- [ ] No synchronous I/O or heavy computation on the main thread
- [ ] Time Profiler run on Release build for performance baselines
- [ ] Build failures triaged from the first error in the build log
- [ ]
OSSignposterused for custom performance intervals - [ ] Conditional breakpoints used for loop/collection debugging
References
- Logging (unified logging system)
- Logger
- OSSignposter
- Generating log messages from your code
- Recording performance data (signposts)
- Diagnosing memory, thread, and crash issues early
- Data races
- Reducing your app's memory use
- Profiling apps using Instruments
- Analyzing the performance of your shipping app
- LLDB command reference: references/lldb-patterns.md
- Instruments template guide: references/instruments-guide.md
Instruments Guide Reference
Detailed template-by-template guide for profiling iOS apps with Instruments. Companion to the main debugging-instruments skill.
Contents
- General Workflow
- Time Profiler
- Allocations
- Leaks
- Network
- SwiftUI Instruments
- Core Animation
- Energy Log
- File Activity
- System Trace
- xctrace CLI
- Custom Instruments with os_signpost
- Automation and CI Integration
General Workflow
1. Build for profiling: Product > Profile (Cmd+I). This builds with Release optimization by default. 2. Select a template from the Instruments chooser. 3. Configure recording: Set the target device and process. 4. Record: Press the red record button and reproduce the scenario. 5. Analyze: Use the timeline, detail views, and call tree to find issues. 6. Filter: Check "Hide System Libraries" and use the search bar to focus on your code.
Always profile on a physical device for accurate measurements. Simulator performance does not reflect real-world behavior.
Time Profiler
When to use: CPU is high, UI is slow, animations stutter, or you need to find which functions consume the most time.
Key Workflow
1. Record while reproducing the slow interaction. 2. Select the time range of interest in the timeline. 3. Switch to the Call Tree view in the detail pane. 4. Enable these checkboxes:
- Separate by Thread — isolate main thread vs background
- Invert Call Tree — show leaf functions (actual work) first
- Hide System Libraries — focus on your code
5. Sort by Weight (self time) to find the hottest functions. 6. Double-click a function to view source with per-line timing.
Reading the Call Tree
- Weight: total time in this function and all its callees
- Self Weight: time spent in this function alone (not callees)
- Symbol Name: the function — look for your module prefix
Focus on functions with high Self Weight — these are doing the actual work.
Tips
- Profile the same user interaction 3 times to get stable measurements.
- Use the Comparison view to diff traces before/after a fix.
- Check "Flatten Recursion" if recursive calls make the tree hard to read.
Allocations
When to use: Memory grows over time, you suspect objects are not being freed, or you need to track object lifetimes.
Key Workflow
1. Record while reproducing the scenario. 2. Use Mark Generation (button in the detail pane) to snapshot allocations at specific points. For example:
- Mark before navigating to a screen
- Navigate to the screen
- Navigate back
- Mark again
- The difference shows objects that were not freed
3. Expand a generation to see allocations grouped by category. 4. Filter by your module name to exclude system allocations.
Allocation Lifespan
- Created & Still Living: objects allocated and not yet freed
- Created & Destroyed: objects with normal lifetimes
- Persistent: long-lived allocations (singletons, caches)
- Transient: short-lived allocations (autoreleased, temporary)
Focus on "Created & Still Living" objects in the generation diff — these are your potential leaks.
Heap Growth Analysis
Enable the Allocations List and sort by Persistent Bytes. Look for:
- Classes with unexpectedly high instance counts
- Image data or NSData objects that should have been released
- View controllers that persist after dismissal
Leaks
When to use: Suspected retain cycles or abandoned memory.
Key Workflow
1. Run with the Leaks template. 2. The tool automatically checks for leaks every 10 seconds. 3. Red "Leak" markers appear in the timeline when leaks are detected. 4. Click a leak marker to see the leaked object and its retain/release history. 5. The Cycles & Roots view shows the reference graph — follow the arrows to find the cycle.
Interpreting Results
- Leak: an object with no references pointing to it (true orphan)
- Root Leak: the object at the head of a leaked object graph
- Cycles & Roots graph: arrows show retain relationships — look for
bidirectional arrows indicating a cycle
Common Cycle Patterns
Leaks instrument commonly catches:
- Closure -> self -> closure cycles
- Delegate strong reference cycles
- NotificationCenter observer blocks retaining self
- CADisplayLink/Timer retaining target
Network
When to use: Inspecting HTTP request/response timing, payload sizes, or connection reuse.
Key Workflow
1. Record with the Network template. 2. Each HTTP request appears as a bar in the timeline. 3. Select a request to see:
- URL, method, status code
- Request/response headers
- Timing breakdown (DNS, connect, TLS, request, response)
- Payload size
What to Look For
- Waterfall gaps: sequential requests that could be parallelized
- Large payloads: responses that could use pagination or compression
- Redundant requests: duplicate calls to the same endpoint
- DNS/TLS latency: consider connection prewarming
SwiftUI Instruments
When to use: SwiftUI view body is evaluated too frequently, unnecessary redraws, or identity churn in lists.
Key Workflow
1. Select the SwiftUI template in Instruments. 2. Record while interacting with the UI. 3. Check these lanes:
- Update Groups: shows batches of view updates triggered together
- Long View Body Updates: highlights body evaluations exceeding a time threshold
- Cause and Effect Graph: traces why a view was re-evaluated
4. Look for views with excessive body evaluations during a single interaction.
Tips
- Filter by view name to focus on a specific component.
- Cross-reference with Time Profiler to see if body evaluations are expensive.
- See the
swiftui-performanceskill for remediation patterns.
Core Animation
When to use: Frame drops, off-screen rendering, excessive blending.
Key Workflow
1. Record with the Core Animation template on a real device. 2. Check the FPS lane for drops below 60 (or 120 on ProMotion devices). 3. Enable these debug options in the Recording Options:
- Color Blended Layers — red areas have multiple overlapping layers
- Color Off-screen Rendered — yellow areas use off-screen passes
- Color Hits Green and Misses Red — rasterization cache hits/misses
Common Issues
| Issue | Indicator | Fix |
|---|---|---|
| Transparent overlapping views | Red blended layers | Use opaque backgrounds |
| Corner radius + clip | Off-screen rendering | Use cornerCurve with pre-masked images |
| Shadow without path | Off-screen rendering | Set shadowPath explicitly |
| Large images not downsampled | High memory + slow rendering | Downsample before display |
Energy Log
When to use: Battery drain complaints, background energy impact, or App Store rejection for excessive energy use.
Key Workflow
1. Record with the Energy Log template on a physical device. 2. Check the Energy Impact lane for high/very high readings. 3. Examine component breakdown:
- CPU
- Network
- Location
- GPU
- Background tasks
Tips
- Profile typical user sessions (5-10 minutes of real usage).
- Compare foreground vs background energy impact.
- Check that background tasks complete and do not run indefinitely.
- Cross-reference with MetricKit energy metrics from production.
File Activity
When to use: Slow file operations, excessive disk I/O, or disk write exceptions from MetricKit.
Key Workflow
1. Record with the File Activity template. 2. Look for:
- Frequent writes to the same file (journaling, logging)
- Large reads on the main thread
- Unintended file access patterns
System Trace
When to use: Deep investigation of thread scheduling, virtual memory faults, system calls, and inter-process communication.
Key Workflow
1. Record with the System Trace template. 2. Focus on the main thread lane. 3. Look for:
- Thread blocks: main thread waiting on locks, semaphores, or dispatch queues
- VM faults: page faults from memory-mapped files or large allocations
- Context switches: excessive switching between threads
This is the most advanced template — use it after Time Profiler when you need OS-level detail.
xctrace CLI
Recording
# Record Time Profiler trace
xcrun xctrace record \
--template "Time Profiler" \
--device "iPhone" \
--output ~/traces/profile.trace \
--time-limit 30s \
--launch com.example.MyApp
# Record Allocations trace for a running app
xcrun xctrace record \
--template "Allocations" \
--device "iPhone" \
--output ~/traces/alloc.trace \
--attach com.example.MyApp
# Record with multiple instruments
xcrun xctrace record \
--template "Time Profiler" \
--template "Allocations" \
--output ~/traces/combined.trace \
--launch com.example.MyAppExporting Data
# List available tables in a trace
xcrun xctrace export --input profile.trace --toc
# Export specific table as XML
xcrun xctrace export --input profile.trace \
--xpath '/trace-toc/run/data/table[@schema="time-profile"]'
# Export to a file
xcrun xctrace export --input profile.trace \
--xpath '/trace-toc/run/data/table[@schema="time-profile"]' \
--output profile_data.xmlListing Resources
# Available templates
xcrun xctrace list templates
# Connected devices
xcrun xctrace list devices
# Running processes on a device
xcrun xctrace list processes --device "iPhone"Custom Instruments with os_signpost
Emitting Signposts for Instruments
import os
let signposter = OSSignposter(subsystem: "com.example.app", category: "Networking")
func fetchUser(id: String) async throws -> User {
let state = signposter.beginInterval("fetchUser", id: signposter.makeSignpostID())
defer { signposter.endInterval("fetchUser", state) }
let (data, _) = try await URLSession.shared.data(from: userURL(id))
signposter.emitEvent("dataReceived", "\(data.count) bytes")
return try JSONDecoder().decode(User.self, from: data)
}Viewing in Instruments
1. Open Instruments with the os_signpost or Points of Interest template. 2. Your custom intervals appear as labeled bars in the timeline. 3. Events appear as point markers. 4. Filter by subsystem or category to isolate your signposts.
Integration with MetricKit
Signposts emitted through MXMetricManager.makeLogHandle(category:) are also reported in MetricKit payloads. See the metrickit skill for details on custom signpost metrics.
Automation and CI Integration
Performance Baselines with xctrace
Create a shell script for CI:
#!/bin/bash
set -euo pipefail
APP_BUNDLE="build/MyApp.app"
TRACE_OUTPUT="traces/ci_profile_$(date +%s).trace"
TEMPLATE="Time Profiler"
# Record trace
xcrun xctrace record \
--template "$TEMPLATE" \
--output "$TRACE_OUTPUT" \
--time-limit 60s \
--launch "$APP_BUNDLE"
# Export data for analysis
xcrun xctrace export \
--input "$TRACE_OUTPUT" \
--toc > "traces/toc.xml"
echo "Trace saved to $TRACE_OUTPUT"XCTest Performance Metrics
Complement Instruments with in-test measurements:
func testScrollPerformance() {
let app = XCUIApplication()
app.launch()
let measureOptions = XCTMeasureOptions()
measureOptions.iterationCount = 5
measure(metrics: [
XCTClockMetric(),
XCTCPUMetric(),
XCTMemoryMetric(),
XCTStorageMetric()
], options: measureOptions) {
app.swipeUp()
app.swipeDown()
}
}Set performance baselines in Xcode to automatically flag regressions in CI.
LLDB Patterns Reference
Complete LLDB command reference for iOS debugging. Companion to the main debugging-instruments skill.
Contents
- Inspection Commands
- Breakpoint Patterns
- Expression Evaluation
- Watchpoints
- Thread and Stack Navigation
- Memory Inspection
- Custom Type Summaries
- Python Scripting
- Useful Symbolic Breakpoints
- LLDB Init File
Inspection Commands
po vs p vs v
| Command | Mechanism | Side Effects | Speed |
|---|---|---|---|
po expr | Calls debugDescription via expression eval | Yes — runs code | Slow |
p expr | LLDB formatter on expression result | Yes — runs code | Medium |
v varname | Reads frame memory directly | No | Fast |
(lldb) po myArray # Calls CustomDebugStringConvertible
(lldb) p myArray # Shows type + formatted value
(lldb) v myArray # Fastest, no code execution
(lldb) v myArray[0] # Access elements directly
(lldb) v self.viewModel.state # Dot-path into propertiesUse v as the default. Fall back to po when you need debugDescription or custom string output. Use p when you need type information.
Register and Memory
(lldb) register read # All registers
(lldb) register read x0 x1 # Specific registers (ARM64)
(lldb) memory read 0x600003a04000 # Read raw memory
(lldb) memory read -s1 -fx -c32 addr # 32 bytes as hexBreakpoint Patterns
File and Line
(lldb) br set -f ViewModel.swift -l 42
(lldb) br set -f ViewModel.swift -l 42 -c "count > 10"
(lldb) br set -f ViewModel.swift -l 42 --one-shot true # Delete after first hitFunction and Method Names
(lldb) br set -n viewDidLoad # Any function named viewDidLoad
(lldb) br set -n "MyApp.ViewModel.loadData()" # Fully qualified Swift name
(lldb) br set -S "setValue:forKey:" # ObjC selector
(lldb) br set -r ".*Error.*" # Regex match on symbol name
(lldb) br set -r "MyModule\..*\.deinit" # All deinits in a moduleBreakpoint Actions
(lldb) br set -n loadData
(lldb) br command add 1
> po "loadData called at \(Date())"
> bt
> continue
> DONELogpoints (Auto-Continue Breakpoints)
(lldb) br set -f File.swift -l 42
(lldb) br modify 1 --auto-continue true
(lldb) br command add 1
> po "state = \(self.state)"
> DONEThis prints the value every time line 42 is hit without stopping execution. Equivalent to Xcode's "Log Message" breakpoint action with auto-continue.
Listing and Managing
(lldb) br list # Show all breakpoints
(lldb) br disable 1 # Disable breakpoint 1
(lldb) br enable 1 # Re-enable
(lldb) br delete 1 # Remove
(lldb) br delete # Remove ALL breakpoints
(lldb) br modify 1 -i 5 # Skip first 5 hits (ignore count)Expression Evaluation
Swift Expressions
(lldb) expr myArray.count
(lldb) expr myArray.filter { $0.isActive }.count
(lldb) expr let result = myFunc(); print(result)
(lldb) e -l swift -- import Foundation
(lldb) e -l swift -- self.title = "Debug Title"Objective-C Expressions (for UIKit internals)
(lldb) e -l objc -- (void)[CATransaction flush]
(lldb) e -l objc -- (void)[[UIApplication sharedApplication] _performMemoryWarning]
(lldb) e -l objc -- (BOOL)[(id)0x7fc... isKindOfClass:[UIView class]]
(lldb) e -l objc -- (void)[0x7fc... recursiveDescription] # View hierarchy dumpModifying State at Runtime
(lldb) e self.debugLabel.text = "Modified in debugger"
(lldb) e self.view.backgroundColor = UIColor.red
(lldb) e -l objc -- (void)[CATransaction flush] # Force redrawCalling Functions
(lldb) e self.viewModel.reset()
(lldb) e UserDefaults.standard.set(true, forKey: "debug_mode")
(lldb) e NotificationCenter.default.post(name: .init("DebugReload"), object: nil)Watchpoints
(lldb) w set v self.score # Watch for writes
(lldb) w set v self.score -w read # Watch for reads
(lldb) w set v self.score -w read_write # Watch both
(lldb) w set e -- 0x600003a04000 # Watch memory address
(lldb) w modify 1 -c "self.score > 100" # Conditional
(lldb) w list # Show active watchpoints
(lldb) w delete 1 # RemoveHardware watchpoint limit on Apple Silicon: 4 watchpoints. Use them sparingly for tracking unexpected mutations.
Thread and Stack Navigation
(lldb) thread list # All threads with status
(lldb) thread select 3 # Switch to thread 3
(lldb) bt # Backtrace current thread
(lldb) bt all # Backtrace every thread
(lldb) bt 5 # Show only top 5 frames
(lldb) frame select 2 # Jump to frame #2
(lldb) frame info # Current frame info
(lldb) frame variable # All variables in frame
(lldb) up # Move up one frame
(lldb) down # Move down one frameThread Return (skip execution)
(lldb) thread return # Return from current frame
(lldb) thread return false # Return false from a Bool funcUse thread return to skip the rest of a function during debugging. Useful for bypassing a crash or testing a different code path.
Memory Inspection
(lldb) memory read 0x600003a04000 # Default format
(lldb) memory read -s4 -fx -c8 addr # 8 x 4-byte hex words
(lldb) memory read -f s addr # Read as C string
(lldb) memory find 0x100000 0x200000 -e "DEADBEEF" # Search memory range
(lldb) image lookup -a 0x100004500 # Symbol at address
(lldb) image lookup -n loadData # Address of symbol
(lldb) image list # All loaded images/frameworksSwift Metadata Inspection
(lldb) e -l swift -- print(type(of: myObject))
(lldb) e -l swift -- dump(myObject) # Full mirror dump
(lldb) e -l swift -- Mirror(reflecting: myObject).children.map { $0.label }Custom Type Summaries
Add type summaries to .lldbinit for cleaner debugger output:
# ~/.lldbinit
# Show CLLocationCoordinate2D as "lat, lon"
type summary add CLLocationCoordinate2D \
--summary-string "lat=${var.latitude}, lon=${var.longitude}"
# Show Date as readable string
type summary add Foundation.Date \
--summary-string "${var.timeIntervalSinceReferenceDate} secs since 2001"
# Custom summary for your own types
type summary add MyApp.UserProfile \
--summary-string "User(${var.name}, id=${var.id})"Python Scripting
Inline Python
(lldb) script import os
(lldb) script print(os.getpid())
(lldb) script lldb.debugger.GetSelectedTarget().GetProcess().GetNumThreads()Custom Python Command
Create ~/lldb_commands/dump_views.py:
import lldb
def dump_view_hierarchy(debugger, command, result, internal_dict):
"""Dump the key window's view hierarchy."""
target = debugger.GetSelectedTarget()
process = target.GetProcess()
thread = process.GetSelectedThread()
frame = thread.GetSelectedFrame()
expr = '(NSString *)[[UIApplication sharedApplication].keyWindow recursiveDescription]'
value = frame.EvaluateExpression(expr)
result.AppendMessage(str(value.GetObjectDescription()))
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand(
'command script add -f dump_views.dump_view_hierarchy dump_views'
)Load in .lldbinit:
command script import ~/lldb_commands/dump_views.pyThen use in LLDB:
(lldb) dump_viewsUseful Symbolic Breakpoints
Set these in Xcode's Breakpoint Navigator for common debugging scenarios:
| Symbol | Purpose |
|---|---|
UIViewAlertForUnsatisfiableConstraints | Auto Layout constraint conflicts |
swift_willThrow | Break on all Swift throws |
malloc_error_break | Heap corruption detection |
_UITraitCollectionChangeObserverNotify | Track trait collection changes |
objc_exception_throw | Break on ObjC exceptions |
-[UIApplication _performMemoryWarning] | Memory warning simulation |
NSInternalInconsistencyException | Foundation assertion failures |
LLDB Init File
Place commonly used configuration in ~/.lldbinit:
# Custom aliases
command alias -- pp e -l swift -- import Foundation
command alias -- pjson e -l swift -- print(String(data: try! JSONSerialization.data(withJSONObject: %1, options: .prettyPrinted), encoding: .utf8)!)
# Type summaries
type summary add --summary-string "${var.rawValue}" -x "^.*RawRepresentable$"
# Load custom scripts
command script import ~/lldb_commands/dump_views.py
# Settings
settings set target.language swift
settings set frame-format "frame #${frame.index}: ${frame.pc}{ ${module.file.basename}{\`${function.name-with-args}{${frame.no-debug}}}}\n"Related skills
How it compares
Use debugging-instruments over generic debugging skills when the problem is iOS-specific and requires LLDB, Memory Graph, or Instruments rather than log-only triage.
FAQ
po or v for a local variable?
Use v for fast frame variable reads without executing code; po calls debugDescription and can trigger side effects.
How find retain cycles?
Use Memory Graph Debugger after reproducing navigation, look for purple warnings, and fix weak self in closures and weak delegates.
What defines a main thread hang?
Blocking over 250ms is noticeable and over 1 second is severe; use Thread Checker and signposts to locate work.
Is Debugging Instruments safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.