# Instruments Guide

Companion to the `debugging-instruments` skill: what each template is for, how
to read it, and how to drive recordings from scripts.

## Workflow That Fits Every Template

Product > Profile (Cmd+I) builds the app for profiling, which uses the Release
configuration unless the scheme says otherwise. Then:

1. Choose a template.
2. Pick the device and the process to launch or attach to.
3. Record while you reproduce the problem.
4. Read the timeline, the detail pane and the call tree.
5. Narrow the data with Hide System Libraries and the search field.

Profile on a physical device. Simulator runs on the Mac's CPU and memory and
does not tell you how the app performs on a phone.

## Time Profiler

Use it for high CPU, slow screens, stuttering animations and for finding the
most expensive functions.

Reading the call tree:

1. Drag across the timeline to select the slow period.
2. Open the Call Tree and turn on Separate by Thread, Invert Call Tree (leaf
   functions first) and Hide System Libraries.
3. Sort by Weight and double-click a function to see its source with per-line
   cost.

Weight is time spent in the function including its callees; Self Weight counts
only the function's own code. Functions with a high Self Weight are the ones
doing the work.

Tips:
- Perform the interaction three times so the numbers settle.
- Record before and after a change and diff them in the comparison view.
- Flatten Recursion collapses deep recursive trees into one entry.

## Allocations

Generation analysis:

1. Click Mark Generation.
2. Open the screen under suspicion.
3. Leave it again.
4. Click Mark Generation once more.

The second generation holds what the round trip left behind. Expand it to see
allocations grouped by category and filter by your module name.

Lifespan filters:

| Category | Meaning |
|----------|---------|
| Created & Still Living | Allocated in the range and still alive |
| Created & Destroyed | Allocated and freed in the range |
| Persistent | Long-lived by design: singletons, caches |
| Transient | Short-lived temporaries and autoreleased objects |

In a generation diff, "Created & Still Living" is where leaks hide.

For overall heap growth, sort the Allocations List by Persistent Bytes. Watch
for types with surprisingly high counts, images or `Data` buffers that are
never released, and view controllers that outlive their dismissal.

## Leaks

- A leak check runs automatically every 10 seconds.
- Red markers on the timeline show where leaks were found; click one to see the
  object and its retain and release history.
- Cycles & Roots draws the reference graph; arrows in both directions mean a
  cycle.
- A *leak* is an object nothing refers to any more; a *root leak* is the top of
  a leaked object graph.

Cycles this usually finds: a closure stored on an object that captures that
object, strong delegate references, block-based `NotificationCenter` observers
that capture `self`, and `CADisplayLink` or `Timer` retaining its target.

## Network

Each HTTP request appears as a bar on the timeline. Selecting one shows the
URL, method, status code, headers, a timing split (DNS, connect, TLS, request,
response) and the payload size.

What to look for:
- Gaps in a waterfall where requests run one after another but could run in
  parallel.
- Large responses that need pagination or compression.
- The same request sent more than once.
- Slow DNS or TLS setup that connection prewarming could hide.

## SwiftUI

Use it for bodies that run too often, redraws with no visible change, and list
rows whose identity keeps changing.

Lanes to read:
- **Update Groups**: batches of work SwiftUI performed together.
- **Long View Body Updates**: bodies that took longer than the threshold.
- **Cause and Effect Graph**: the chain of changes that made a view update.

Filter by view name and cross-check hot spots against Time Profiler. How to fix
what you find belongs to the `swiftui-performance` skill.

## Animation Hitches and Core Animation

Record Animation Hitches on a real device when scrolling or animations stutter
or frames are missed. For lower-level rendering detail add Core Animation FPS,
Core Animation Commits or Core Animation Activity, and read the FPS, hitch,
commit and frame-lifetime lanes.

Rendering diagnostics live in Xcode (Debug > View Debugging > Rendering) and
in Simulator's Debug menu rather than in current Instruments:
- **Color Blended Layers**: overlapping translucent layers show in red.
- **Color Off-screen Rendered**: layers rendered off screen show in yellow.
- **Color Hits Green and Misses Red**: rasterization cache hits and misses.

| Problem | Why it costs | Fix |
|---------|--------------|-----|
| Stacked transparent views | Blending, red in Color Blended Layers | Give views opaque backgrounds |
| Rounded corners with `masksToBounds` over image content | Off-screen pass | Round the image once before display, or avoid masking |
| Shadows without a path | Off-screen pass to find the shape | Set `shadowPath` |
| Full-size images in small views | Memory and decode cost | Downsample to display size first |

## Power Profiler

Use it for battery complaints, energy spent in the background, or an App Store
rejection that cites energy use. Record on a physical device.

Read the power, thermal state and energy impact lanes, and the per-component
split: CPU, network, location, GPU and background tasks.

Tips:
- Record a realistic session of 5 to 10 minutes.
- Compare the app in the foreground with the app in the background.
- Make sure background tasks finish and stop.
- Cross-check against MetricKit energy metrics from the field.

## File Activity

Use it for slow file operations, heavy disk I/O and MetricKit disk-write
exceptions. Look for the same file written again and again, large reads on the
main thread and access patterns you did not expect.

## System Trace

Use it when you need to see thread scheduling, virtual memory faults, system
calls or IPC. Start from the main thread's lane and look for:
- threads blocked on locks, semaphores or dispatch queues,
- VM page faults,
- a high number of context switches.

It is the most detailed template. Reach for it after Time Profiler, when you
need to know what the operating system was doing.

## xctrace

```bash
xcrun xctrace record --template "Time Profiler" --time-limit 30s \
  --output scroll.trace --launch -- /path/to/Atlas.app
xcrun xctrace record --template "Allocations" --attach Atlas --output memory.trace
xcrun xctrace export --input scroll.trace --toc
xcrun xctrace export --input scroll.trace \
  --xpath '/trace-toc/run/data/table[@schema="time-profile"]' --output time-profile.xml
xcrun xctrace list instruments
```

- `--time-limit` stops the recording on its own.
- `--attach` records an app that is already running, instead of `--launch`.
- `--toc` lists the tables inside a trace; `--xpath` picks one to export.

## Custom Intervals with Signposts

```swift
import os

let signposter = OSSignposter(subsystem: "com.example.atlas", category: "Catalog")

func loadCatalog(from url: URL) async throws -> [Product] {
    let id = signposter.makeSignpostID()
    let state = signposter.beginInterval("loadCatalog", id: id)
    defer { signposter.endInterval("loadCatalog", state) }

    let (data, _) = try await URLSession.shared.data(from: url)
    signposter.emitEvent("downloaded", id: id, "\(data.count) bytes")
    return try JSONDecoder().decode([Product].self, from: data)
}
```

To see them, start from a Blank document and add the os_signpost instrument,
or use any template that includes Points of Interest. Intervals appear as
labelled bars and events as single markers; filter by subsystem and category.

Signposts written to a log from `MXMetricManager.makeLogHandle(category:)` are
also counted in MetricKit payloads; the `metrickit-diagnostics` skill covers
that side.

## Automation and CI

A script a CI job can run:

```bash
#!/bin/bash
set -euo pipefail

stamp=$(date +%Y%m%d-%H%M%S)
trace="traces/launch-$stamp.trace"
mkdir -p traces

xcrun xctrace record --template "Time Profiler" --time-limit 60s \
  --output "$trace" --launch -- "$APP_PATH"
xcrun xctrace export --input "$trace" --toc > "traces/launch-$stamp-toc.xml"
```

XCTest can measure a flow and compare it with a stored baseline:

```swift
import XCTest

final class FeedScrollPerformanceTests: XCTestCase {
    @MainActor
    func testFeedScrolling() {
        let app = XCUIApplication()
        app.launch()

        let options = XCTMeasureOptions()
        options.iterationCount = 5

        measure(metrics: [XCTClockMetric(),
                          XCTCPUMetric(application: app),
                          XCTMemoryMetric(application: app),
                          XCTStorageMetric(application: app)],
                options: options) {
            app.swipeUp()
            app.swipeDown()
        }
    }
}
```

`XCUIApplication` and its gestures are main-actor isolated, hence `@MainActor`
on the test method. Set a baseline for each measurement in Xcode's test report; later runs that
exceed it fail, so CI flags the regression without anyone reading traces.
