Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/trace-analysis.md
1# Instruments Trace Analysis23Use this reference whenever the user references an Xcode Instruments `.trace`4file. A target SwiftUI source file is **optional** — if provided, you can5cite specific lines; without one, the trace still surfaces view names,6hot symbols, and high-severity events that tell the user where to look.78The bundled parser reads five lanes for SwiftUI responsiveness (Time9Profiler, Hangs, Animation Hitches, SwiftUI updates, and the SwiftUI10cause graph) and exposes three discovery modes (`--list-logs`,11`--list-signposts`, `--fanin-for`) plus a `--window` flag so the agent12can focus analysis on a precise slice of the trace.1314## When to invoke1516Any of these signals:1718- Message contains a path ending in `.trace`.19- User mentions "hangs", "hitches", "jank", "slow view", or performance20issues alongside an Instruments recording.21- User asks to focus analysis "after / before / between / during" a log22message or signpost.2324Triggering does **not** require a SwiftUI source file. If one is present25you'll ground recommendations in specific lines; if not, base them on the26view names and symbols the trace reveals.2728## The three CLI modes2930The scripts live alongside this skill at `scripts/` and need only the31Python 3 stdlib + `xctrace` (ships with Xcode at `/usr/bin/xctrace`).3233### 1. Full analysis (default)3435```bash36python3 "${SKILL_DIR}/scripts/analyze_trace.py" \37--trace "/path/to/file.trace" \38--top 10 --top-hitches 5 \39[--window START_MS:END_MS] \40--json-only41```4243- `--json-only` gives you structured data; omit for JSON + markdown44summary; `--markdown-only` is for pasting a digest into the chat.45- `--output <path>` writes `<path>.json` and `<path>.md` instead of stdout.46- `--window START_MS:END_MS` (optional) restricts every lane and every47correlation to that time slice.48- `--run N` selects a specific run when the trace contains more than one49recording session. Single-run traces don't need it; multi-run traces50require it and will error with the available run numbers if omitted.51Use `--list-runs` to dump per-run metadata (template, duration,52start/end dates, schemas) before analyzing.5354### 2. `--list-logs` — find os_log timestamps5556```bash57python3 "${SKILL_DIR}/scripts/analyze_trace.py" --trace <path> --list-logs \58[--log-subsystem com.myapp.net] \59[--log-category "Network"] \60[--log-type Fault] \61[--log-message-contains "loaded feed"] \62[--log-limit 10] \63[--window START_MS:END_MS]64```6566Returns JSON `{ "logs": [...], "count": N }` where each log entry includes67`time_ms`, `type`, `subsystem`, `category`, `process`, and the formatted68`message` (with args substituted) + raw `format_string`. All filters are69AND-combined; `--log-message-contains` is case-insensitive substring match.7071### 3. `--list-signposts` — find signpost intervals7273```bash74python3 "${SKILL_DIR}/scripts/analyze_trace.py" --trace <path> --list-signposts \75[--signpost-name-contains "ImageDecode"] \76[--signpost-subsystem com.myapp.feed] \77[--signpost-category "Rendering"] \78[--window START_MS:END_MS]79```8081Returns JSON `{ "intervals": [...], "events": [...] }`. Intervals are82paired `begin`/`end` signposts with `start_ms`, `end_ms`, `duration_ms`,83`name`, `subsystem`, `category`, `process`, `signpost_id`. Single-point84events (and any unpaired begins) go into `events`. All filters are85AND-combined; `--signpost-name-contains` is case-insensitive substring86match.8788### 4. `--fanin-for` — who keeps invalidating this view?8990```bash91python3 "${SKILL_DIR}/scripts/analyze_trace.py" --trace <path> \92--fanin-for "TextStyleModifier" \93[--window START_MS:END_MS] \94[--top 10]95```9697Returns JSON `{ "matches": [...] }`. Each match names a destination node98whose fmt string contains the substring (case-insensitive) and lists its99top incoming source nodes ranked by edge count. Use this after the100`swiftui` lane names an expensive view and you want to know *why it keeps101being invalidated*. For the example above, the top source is102`closure #1 in UserDefaultObserver.Target.GraphAttribute.send()` — the103canonical signature of an `@AppStorage` / `UserDefaults` feedback storm.104105## Composition pattern — scoping to a slice106107When the user says something like "focus on X", "between A and B", or108"during signpost Y", compose the three modes:1091101. **Discover** — call `--list-logs` or `--list-signposts` with filters111that match the user's description. Pick the right entries.1122. **Build the window** — take `time_ms` (logs) or `start_ms`/`end_ms`113(intervals) and form `--window START:END`.1143. **Analyse** — call the default mode with `--window`.115116Examples:117118- *"Focus on the section after the log saying 'loaded feed'."*119→ `--list-logs --log-message-contains "loaded feed"`, take the entry's120`time_ms`, set window = `[that_ms, end_of_trace_ms]` (or use the trace121`duration_s × 1000`).122- *"Between the 'begin-sync' log and the 'done-sync' log."*123→ Two `--list-logs` calls (or one with a broader filter), pick the two124timestamps, set window = `[first, second]`.125- *"During the signpost 'ImageDecode'."*126→ `--list-signposts --signpost-name-contains "ImageDecode"`, pick the127interval, set window = `[start_ms, end_ms]`.128129## JSON shape130131```json132{133"trace": "...",134"xctrace_version": "26.4 (...)",135"template": "SwiftUI",136"duration_s": 14.83,137"schemas_available": [...],138"lanes": [139{ "lane": "time-profiler", "available": true, "schema_used": "time-profile",140"metrics": { "total_samples": N, "total_weight_ms": ms, "processes": [...] },141"top_offenders": [ { "symbol", "weight_ms", "percent", "samples", "thread" } ] },142{ "lane": "hangs", "available": true, "schema_used": "potential-hangs",143"metrics": { "count", "total_duration_ms", "worst_duration_ms",144"severity_buckets": {"lt_250ms","250ms_1s","gt_1s"} },145"top_offenders": [ { "start_ms", "duration_ms", "hang_type", "thread" } ] },146{ "lane": "hitches", "available": true, "schema_used": "hitches",147"metrics": { "count", "total_hitch_ms", "worst_hitch_ms",148"narrative_breakdown": {...}, "system_hitches", "app_hitches" },149"top_offenders": [ { "start_ms", "hitch_duration_ms", "narrative", "is_system" } ] },150{ "lane": "swiftui", "available": true, "schemas_used": [...],151"metrics": { "total_events", "unique_views", "total_duration_ms",152"severity_breakdown": {"Very Low":N,"Moderate":N,"High":N},153"update_type_breakdown": {"View Body Updates":N, ...} },154"top_offenders": [ { "view", "total_ms", "count", "avg_ms" } ],155"high_severity_events": [ { "view", "severity", "duration_ms", "category",156"update_type", "description" } ] },157{ "lane": "swiftui-causes", "available": true, "schema_used": "swiftui-causes",158"metrics": { "total_edges", "unique_sources", "unique_destinations",159"top_labels": {...} },160"top_sources": [ { "source", "edges", "top_destinations": [...] } ],161"top_destinations": [ { "destination", "edges", "top_sources": [...] } ] }162],163"correlations": [164{165"trigger": { "lane": "hangs"|"hitches", "start_ms", "end_ms", "duration_ms",166"hang_type"|"frame_duration_ms" },167"time_profiler_main_thread": {168"samples_in_window": N, "samples_on_main": M,169"main_running_coverage_pct": 0–100,170"hot_symbols": [ { "symbol", "samples", "weight_ms", "percent_of_main" } ]171},172"swiftui_overlapping_updates": [ { "view", "duration_ms", "start_ms" } ]173}174]175}176```177178## Interpretation guide179180### `main_running_coverage_pct` is the key diagnostic181182Time Profiler samples the main thread every ~1ms. For a correlation window183of `N` ms, you'd expect ~`N` main-thread running samples if main were fully184CPU-bound. Coverage is the ratio of observed main-thread samples to that185expectation.186187- **< 25% coverage** → main thread was **blocked** (I/O, lock, sync XPC,188`Task.sleep`, waiting on an actor-isolated call). The `hot_symbols` you189do see are the moments main *was* executing — look there for the code190that *initiates* the blocking work, not the work itself. Common fix:191move to a background executor / `nonisolated` / `Task.detached`.192- **≥ 75% coverage** → main was **CPU-bound** the whole time. `hot_symbols`193point directly at the expensive work. Common fixes: hoist computation194out of view bodies, cache derived values, avoid per-frame allocation,195debounce `onChange`.196- **25–75%** → mix. Usually computation plus intermittent I/O; show both197hot symbols and note that main was partially blocked.198199### High-severity SwiftUI events → reference routing200201When `swiftui.high_severity_events[].description` is one of:202203| description | Likely cause | Route to |204|------------------|---------------------------|-------------------------------------|205| `onChange` | Expensive `.onChange` body | `references/performance-patterns.md`, `references/state-management.md` |206| `Gesture` | Heavy gesture handler | `references/performance-patterns.md` |207| `Action Callback`| Button/tap handler work | `references/performance-patterns.md` |208| `Update` | View body recomputation | `references/view-structure.md`, `references/performance-patterns.md` |209| `Creation` | View init cost | `references/view-structure.md` |210| `Layout` | GeometryReader churn | `references/layout-best-practices.md` |211212### Mapping trace findings to source code213214If the user gave you a specific file, use it to confirm/cite. If they didn't, the trace itself tells you which views and symbols to look up.2152161. **From `swiftui.top_offenders` and `high_severity_events`**, use the217`view` string as your search key. If a target file is open, grep it;218if not, recommend the user grep their project for that type or the219module name. A partial match (prefix / generic stripping) means it's220probably a subview.2212. **From `correlations[].time_profiler_main_thread.hot_symbols`**, treat222symbols starting with the user's module name (or in Swift free-function223form) as candidates. System frames (`swift_`, `dyld`, `objc_`, `CA*`,224`CF*`, `NS*`, `__open`, `pthread*`) identify *what* the code was doing225but the user-code caller one frame up is typically what to fix — say226so and, if you can, suggest searching the project for callers of the227equivalent Swift API (e.g. `__open` → `FileHandle` / `Data(contentsOf:)` /228`JSONDecoder.decode(from: Data)` sites).2293. **From `hitches[].narrative`**, Apple pre-attributes each hitch. The230string `"Potentially expensive app update(s)"` means SwiftUI blamed the231app (so user code is in scope); absence of narrative usually means it232was a system hitch or below the threshold.2334. **Correlating hitches with SwiftUI updates**: the234`swiftui_overlapping_updates` list on each hitch names the views that235were actively rendering when the frame dropped. Prioritise those.236237### Cause graph: finding *why* updates keep happening238239The `swiftui` lane tells you *what* is expensive; the `swiftui-causes`240lane tells you *why* it keeps being triggered. Each edge is "source node241propagated to destination node" in SwiftUI's attribute graph.242243Signatures to watch for in `top_sources`:244245- **`closure #1 in UserDefaultObserver.Target.GraphAttribute.send()`** —246an `@AppStorage` / `UserDefaults` write is fanning out to every reader.247If the destination list contains multiple `@AppStorage <Type>.<prop>`248entries with thousands of edges each, you have a feedback storm. Fix249by reading each key once at a high level and passing values down, or250wrapping settings in a single `@Observable` so only genuine readers251invalidate. Route to `references/state-management.md` and252`references/performance-patterns.md`.253- **`EnvironmentWriter: …`** with thousands of edges — a modifier (often254`.hoverEffect`, custom environment keys) is applied too widely and255being re-installed during every layout pass. Route to256`references/view-structure.md`.257- **`View Creation / Reuse`** as the #1 source — the hierarchy is258replacing children rather than mutating in place. Look for ID259instability (missing/unstable `.id(…)` on ForEach, type-erased260`AnyView` wrappers, conditional structure swaps). Route to261`references/list-patterns.md` and `references/view-structure.md`.262263When a specific view in `swiftui.high_severity_events` keeps showing up,264run `--fanin-for "<view name>"` to see the ranked list of sources265invalidating it.266267### Picking targets from a full-trace analysis268269Prioritise from most actionable to least:2702711. **Any `hangs` with `main_running_coverage_pct < 25%`** — these are272blocking-I/O smells; nearly always fixable by moving work off-main.2732. **Any `hangs` with `main_running_coverage_pct ≥ 75%`** — CPU-bound274main-thread work; fix the top `hot_symbols`.2753. **`swiftui-causes.top_sources` with > ~1k edges** — structural276invalidation bugs (feedback storms, over-applied modifiers). These277are often cheaper to fix than per-view optimisations and collapse278many downstream high-severity updates at once.2794. **`hitches` with `narrative == "Potentially expensive app update(s)"`**280and overlapping `swiftui_overlapping_updates` — specific views to281restructure.2825. **`swiftui.high_severity_events`** — `onChange`, `Gesture`, or `Action283Callback` with `duration_ms > ~16` are frame-dropping handlers. For284any that keep firing, run `--fanin-for` to find the source.2856. **`swiftui.top_offenders`** — heaviest views by total body time, even286without triggering hitches; candidates for view extraction or287memoisation (`equatable`, `@ViewBuilder` extraction).288289## Recommended output format for the user290291After running the parser, structure your response as:2922931. **One-line summary** — "Found N hangs, worst Wms; K hitches; J high-severity SwiftUI updates."2942. **Root-cause findings** — per prioritised target (see above), one paragraph with the trace evidence (coverage %, hot symbol, overlapping view) and a citation from `references/…` for the fix pattern.2953. **Plan** — numbered, file-specific edits. Cite line numbers in the user's Swift file when you know them. Don't edit the file unless the user asked for edits.296