ios-ettrace-performance

$npx mdskill add dpearson2699/swift-ios-skills/ios-ettrace-performance

Captures and analyzes ETTrace profiles for iOS launch or runtime flows.

  • Solves capturing and analyzing focused iOS performance profiles with ETTrace.
  • Depends on ETTrace framework/CLI and exact-build dSYMs for symbolication.
  • Decides based on capture contract, user flow boundaries, and dSYM UUID matching.
  • Delivers per-thread flamegraph JSON with sampled inclusive/exclusive time.

SKILL.md

.github/skills/ios-ettrace-performanceView on GitHub ↗
---
name: ios-ettrace-performance
description: "Use when capturing or analyzing ETTrace profiles for a focused iOS launch or runtime flow, including exact-build dSYM UUID matching, Simulator or device capture, processed per-thread flamegraph JSON, sampled inclusive/exclusive time, unresolved symbols, and comparable verification. Use debugging-instruments or swiftui-performance for generic profiling instead."
---

# iOS ETTrace Performance

Use ETTrace for a bounded, symbolicated sampling experiment. Treat capture
conditions and symbolication as part of the evidence, not as setup trivia.

## Contents

- [Boundary](#boundary)
- [Capture Contract](#capture-contract)
- [Workflow](#workflow)
- [Interpret the Report](#interpret-the-report)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## Boundary

This skill owns ETTrace framework/CLI capture, exact-build dSYMs, processed
flamegraph JSON, and like-for-like verification. Use Instruments for broad CPU,
hitch, energy, or concurrency triage. Use SwiftUI performance guidance for view
identity, invalidation, and layout remediation.

ETTrace periodically samples thread stacks. Its durations reconstruct sampled
attribution from those intervals; do not present them as wall-clock production
timings or compare them directly with a differently configured profiler.

## Capture Contract

Write these down before capturing:

- one exact user flow and its start/stop boundary;
- launch capture or already-running runtime capture;
- app commit, build configuration, optimization settings, and architecture;
- ETTrace runner and framework versions, plus single-thread or multi-thread mode;
- simulator/device model and OS build;
- launch method, data state, cache state, and repetition count.

Reject a before/after claim when any of these materially differ. Rebuild and
repeat under one contract instead.

## Workflow

### 1. Triage before instrumenting

Confirm that ETTrace's sampled flamechart is the right next tool. Prefer
Instruments first when the slow interval is unknown, spans many subsystems, or
needs system-level blocking, I/O, hitch, or concurrency context.

Choose one flow small enough to repeat. Launch, first screen construction,
opening one document, or applying one edit are useful boundaries; "use the app"
is not.

### 2. Record and hold ETTrace versions fixed

Install the runner from the official tap and link the ETTrace package product
into the app target being measured. Record the runner version and the framework
revision or tag separately, then keep both fixed across compared captures. The
bundled analyzer supports the verified v1.1.1 processed output shape; re-check
upstream and the parser before using a different format.

```bash
brew install emergetools/homebrew-tap/ettrace
```

Run the instrumented app once and confirm the `Starting ETTrace` log. Absence of
that message means capture evidence is not trustworthy. Keep this wiring out of
shipping configurations unless the project deliberately owns that tradeoff.

### 3. Build first, then collect matching dSYMs

Use the final instrumented build for both capture and dSYM collection. Never
choose a dSYM by filename, modification time, or Derived Data proximity.

```bash
mkdir -p /tmp/myapp-ettrace
python3 scripts/collect_dsyms.py \
  --app /path/to/Build/Products/Release-iphonesimulator/MyApp.app \
  --search-root /path/to/Build/Products \
  --search-root /path/to/Archives \
  --output /tmp/myapp-ettrace/dsyms \
  --pretty > /tmp/myapp-ettrace/dsym-report.json
```

The helper compares `dwarfdump --uuid` output for the app executable and
embedded binaries. Missing or ambiguous UUID matches stop the run by default.
Read and preserve `dsym-report.json` rather than assuming every copied symbol
file is relevant. The helper fails when UUID matches cannot be copied to unique
flat destination names that ETTrace 1.1.1 can discover.

### 4. Capture from a clean artifact directory

Run ETTrace from an empty directory because processed files are written to the
current working directory. Use `--simulator` for Simulator and `--dsyms` for the
exact dSYM directory. Add `--launch` only for launch work, and follow the
runner's two-launch prompts exactly.

```bash
mkdir -p /tmp/myapp-ettrace
mkdir /tmp/myapp-ettrace/run-01
cd /tmp/myapp-ettrace/run-01
ettrace --simulator --dsyms /tmp/myapp-ettrace/dsyms
```

The second `mkdir` must fail if that per-run directory already exists. Choose a
new run name instead of mixing processed captures from retries.

Launch by tapping the app on the Home Screen when the runner asks. Launching
from Xcode can change the launch path and timing. For device capture, omit
`--simulator`; keep all other experiment fields stable.

Stop immediately after the bounded flow and preserve every fresh
`output_<threadId>.json` with the capture contract. ETTrace 1.1.1 creates these
processed files after symbolication. Its internal raw runner `output.json` is a
different artifact and is not accepted by the analyzer below.

### 5. Validate and summarize processed output

```bash
python3 scripts/analyze_ettrace.py \
  /tmp/myapp-ettrace/run-01/output_*.json \
  --top 25 --pretty > /tmp/myapp-ettrace/run-01/summary.json
```

The helper validates the v1.1.1 processed node shape, rejects duplicate inputs
or mixed `osBuild`/`device`/`isSimulator` metadata, handles the serializer's
object-or-array `children` field, and emits deterministic JSON. It does not
rewrite the capture files. Keep those originals beside the summary.

Stop and repair symbolication when important app frames are `<unknown>`, raw
addresses, or attributed to the wrong binary. An unsymbolicated hot address is
an evidence gap, not a code recommendation. ETTrace 1.1.1 address-bearing nodes
are listed under `unresolved_frames` and excluded from ordinary hotspots.

### 6. Change one cause and repeat

Make the smallest code or configuration change supported by a hot app-owned
path. Rebuild, recollect UUID-matched dSYMs, and capture the same flow at least
twice. Report variance and the full capture contract with the result.

## Interpret the Report

- **Exclusive seconds** approximate sampled time charged directly to a symbol
  after subtracting direct child durations. Start with high exclusive app-owned
  work.
- **Inclusive seconds** show the weight of an entire call subtree. Use them to
  find expensive entry paths, not to blame every parent frame.
- Aggregated inclusive time can exceed the root duration because recursive or
  nested appearances count each frame. Exclusive percentages use root duration.
- `<unattributed>` is reported separately. A large value weakens conclusions
  about what happened in the missing interval.
- A framework hotspot may still be caused by app call frequency, data shape, or
  configuration. Walk upward to the first controllable app-owned caller.
- Multi-thread files sum thread time, not wall-clock latency. Do not describe
  their combined root duration as elapsed time.

## Common Mistakes

- Profiling an unbounded session and then guessing which samples match the bug.
- Capturing one build while supplying dSYMs from another build or architecture.
- Treating ETTrace's raw runner JSON as a processed flamegraph.
- Comparing launch capture with runtime capture, or single-thread with
  multi-thread capture.
- Optimizing the top inclusive parent without inspecting exclusive work and
  child paths.
- Claiming improvement from one noisy run.
- Leaving ETTrace instrumentation in production by accident.

## Review Checklist

- [ ] One reproducible flow and start/stop boundary are recorded.
- [ ] Launch/runtime capture mode, single/multi-thread mode, runner version,
      and framework revision match.
- [ ] Build configuration, architecture, target/OS, launch method, and app
      data/cache state match.
- [ ] Every capture starts in a fresh, empty per-run artifact directory.
- [ ] App and embedded-binary UUIDs match the supplied dSYMs.
- [ ] The dSYM collection JSON report is preserved with source/destination and
      missing, ambiguous, collision, or incompatibility evidence.
- [ ] Processed `output_<threadId>.json` files are preserved unchanged.
- [ ] Important app frames are symbolicated.
- [ ] Exclusive, inclusive, unattributed, and multi-thread semantics are clear.
- [ ] The recommendation names an app-controlled path and supporting evidence.
- [ ] Verification repeats the same contract and reports more than one run.
- [ ] Temporary instrumentation is removed or intentionally scoped.

## References

- [ETTrace repository and runner workflow](https://github.com/EmergeTools/ETTrace)
- [ETTrace 1.1.1 release](https://github.com/EmergeTools/ETTrace/releases/tag/v1.1.1)

More from dpearson2699/swift-ios-skills

SkillDescription
accessorysetupkitDiscover and configure Bluetooth and Wi-Fi accessories using AccessorySetupKit. Use when presenting a privacy-preserving accessory picker, defining discovery descriptors for BLE or Wi-Fi devices, handling accessory session events, migrating from CoreBluetooth permission-based scanning, or setting up accessories without requiring broad Bluetooth permissions.
activitykitImplement, review, or improve Live Activities and Dynamic Island experiences in iOS apps using ActivityKit. Use when building real-time updating widgets for the Lock Screen and Dynamic Island — delivery tracking, sports scores, ride-sharing status, workout timers, media playback, or any time-sensitive information that updates in real time. Also use when working with ActivityKit, ActivityAttributes, Activity lifecycle (request/update/end), Dynamic Island layouts (compact/minimal/expanded), push-to-update Live Activities, or Lock Screen live widgets.
adattributionkitMeasure ad effectiveness with privacy-preserving attribution using AdAttributionKit. Use when registering ad impressions, handling attribution postbacks, updating conversion values, implementing re-engagement attribution, configuring publisher or advertiser apps, or replacing SKAdNetwork with AdAttributionKit for ad measurement.
alarmkitImplement AlarmKit alarms and countdown timers for iOS and iPadOS with Lock Screen, Dynamic Island, StandBy, and paired Apple Watch system UI. Covers AlarmManager scheduling, AlarmAttributes and AlarmPresentation, system Stop and AlarmButton secondary actions, authorization, state observation, countdown widget-extension handoff, and Live Activity integration. Use when building wake-up alarms, countdown timers, or alarm-style alerts that need Apple's system alarm experience.
app-clipsBuild iOS App Clips with invocation URLs, App Clip Codes, NFC, QR codes, Safari banners, Maps, Messages, target setup, App Store Connect experiences, size/capability constraints, NSUserActivity routing, SKOverlay promotion, App Group/keychain handoff, ephemeral notifications, location confirmation, and full-app migration. Use when creating App Clips or wiring App Clip invocation, experience configuration, or full-app handoff.
app-intentsImplement App Intents for Siri, Shortcuts, Spotlight, widgets, Control Center, and Apple Intelligence on iOS. Covers AppIntent actions, AppEntity and EntityQuery models, AppShortcutsProvider phrases, IndexedEntity Spotlight indexing, WidgetConfigurationIntent, SnippetIntent, and assistant schemas. Use when exposing app actions or entities to system surfaces.
app-store-reviewAudits App Store submission readiness and rejection risk across current review guidelines, PrivacyInfo.xcprivacy and required-reason APIs, privacy labels, ATT, StoreKit payments, metadata, entitlements, widgets, and Live Activities. Use when preparing a submission, responding to rejection, reconciling privacy evidence, or separating upload blockers from cleanup.
apple-on-device-aiBuild private, on-device AI features on iPhone, iPad, and Mac with Foundation Models, Core ML, MLX Swift, or llama.cpp. Use when choosing an Apple-local model runtime, building an Apple Intelligence chatbot or tool-calling feature, running an LLM on Apple Silicon, converting or compressing a Python model for Core ML, or comparing on-device inference backends. For Swift Core ML loading and prediction code, use the coreml skill.
appmigrationkitTransfer app data to or from other platforms using AppMigrationKit. Use when implementing system-orchestrated one-time migration between iOS and Android or another platform, building an AppMigrationExtension, packaging transportable resources with ResourcesArchiver, importing resources on the destination device, reporting import progress, handling migration errors and app group cleanup, checking MigrationStatus, or testing migration code with AppMigrationTester.
audioaccessorykitSupport automatic audio switching for paired third-party Bluetooth headphones or earbuds with AudioAccessoryKit. Use when a companion app registers an audio accessory, an app extension reports worn/removed placement or connected source-device changes, or AccessoryControlDevice capabilities and errors need handling. Do not use for general AVAudioSession routing, Bluetooth transport, or initial accessory pairing.