# How to Capture and Analyze ETTrace Profiles for iOS Applications

> Learn to capture and analyze ETTrace profiles for iOS apps. Follow steps for setup, dSYM collection, tracing, and performance hotspot identification with our guide.

- Repository: [OpenAI/plugins](https://github.com/openai/plugins)
- Tags: how-to-guide
- Published: 2026-07-04

---

**To capture and analyze ETTrace profiles for iOS applications, prepare an isolated run directory, install the ETTrace CLI via Homebrew, build or link the ETTrace.xcframework into your debug-simulator target, collect matching dSYMs using the provided shell script, capture the trace with launch or runtime profiling modes, and analyze the resulting flame-graph JSON using the Python helper to identify performance hotspots.**

The `openai/plugins` repository provides a complete, reproducible workflow for iOS performance profiling using the ETTrace instrumentation framework. This guide walks through the exact steps implemented in the `ios-ettrace-performance` skill to capture and analyze ETTrace profiles for iOS applications running in the simulator, ensuring you obtain symbolicated traces and actionable hotspot reports.

## Environment Setup and Prerequisites

Before capturing traces, you must prepare an isolated working environment and install the host-side tooling. This prevents concurrent profiling runs from interfering with each other and ensures consistent artifact storage.

### Prepare a Temporary Run Directory

All profiling artifacts—including the ETTrace framework, dSYMs, captured JSON files, and final summaries—are stored under a unique `$RUN_DIR`. According to [`SKILL.md`](https://github.com/openai/plugins/blob/main/SKILL.md) (lines 24–31), create this directory at the start of every profiling session:

```bash
if [ -z "${RUN_DIR:-}" ]; then
  RUN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/codex-ios-ettrace.XXXXXX")"
fi
mkdir -p "$RUN_DIR"

```

### Install the ETTrace CLI

The `ettrace` command-line tool communicates with the iOS simulator over a fixed localhost port. Install it via the Emerge Tools Homebrew tap as specified in [`SKILL.md`](https://github.com/openai/plugins/blob/main/SKILL.md) (lines 35–37):

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

```

## Building and Linking the ETTrace Framework

To instrument your app, you must link the `ETTrace.xcframework` into your debug-simulator build configuration. The framework can be vendored in your repository or built from source.

### Building ETTrace from Source

If you need to build the framework manually, clone the upstream ETTrace source at tag `v1.1.0` (or a user-provided `$ETTRACE_TAG`) and use `xcodebuild` to create the XCFramework. This process is documented in [`SKILL.md`](https://github.com/openai/plugins/blob/main/SKILL.md) (lines 55–78):

```bash
ETTRACE_TAG="${ETTRACE_TAG:-v1.1.0}"
ETTRACE_SRC="$RUN_DIR/ETTrace-src"
git clone --depth 1 --branch "$ETTRACE_TAG" https://github.com/EmergeTools/ETTrace "$ETTRACE_SRC"

xcodebuild archive \
  -scheme ETTrace \
  -archivePath "$RUN_DIR/ETTrace-iphonesimulator.xcarchive" \
  -sdk iphonesimulator \
  -destination 'generic/platform=iOS Simulator' \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  INSTALL_PATH='Library/Frameworks' \
  SKIP_INSTALL=NO \
  CLANG_CXX_LANGUAGE_STANDARD=c++17

xcodebuild -create-xcframework \
  -framework "$RUN_DIR/ETTrace-iphonesimulator.xcarchive/Products/Library/Frameworks/ETTrace.framework" \
  -output "$RUN_DIR/ETTrace.xcframework"

```

### Xcode and Bazel Integration

For Xcode projects, add `$RUN_DIR/ETTrace.xcframework` to **Link Binary With Libraries** and **Embed Frameworks** of the debug-simulator build configuration only. For Bazel projects, import it using `apple_dynamic_xcframework_import`. This linkage is temporary and should be removed after profiling to prevent shipping instrumentation code to production.

## Symbol Collection and dSYM Management

Accurate symbolication requires matching dSYM bundles for your app binary and any embedded frameworks. The [`collect_ios_dsyms.sh`](https://github.com/openai/plugins/blob/main/collect_ios_dsyms.sh) script automates this UUID-matching process.

### Using the dSYM Collection Script

Located at [`plugins/build-ios-apps/skills/ios-ettrace-performance/scripts/collect_ios_dsyms.sh`](https://github.com/openai/plugins/blob/main/plugins/build-ios-apps/skills/ios-ettrace-performance/scripts/collect_ios_dsyms.sh), this script walks searchable roots (app bundle, `DerivedData`, and user-specified directories) to find dSYMs matching the UUIDs of your binaries. Required dSYMs can be enforced with `--require-framework` or `--require-all-frameworks`:

```bash
SKILL_DIR="<absolute path to the skill folder>"
APP="<path-to-built-simulator-App.app>"
DSYMS="$RUN_DIR/dsyms"

"$SKILL_DIR/scripts/collect_ios_dsyms.sh" \
  --app "$APP" \
  --out-dir "$DSYMS" \
  --search-root "$(dirname "$APP")" \
  --search-root "$PWD" \
  --extra-dsym "$RUN_DIR/ETTrace-iphonesimulator.xcarchive/dSYMs/ETTrace.framework.dSYM"

```

The script outputs the path to the `$DSYMS` directory, which you will pass to the `ettrace` capture command.

## Capturing Performance Traces

ETTrace supports two distinct capture modes, both writing processed flame-graph JSON files named `output_<thread>.json` into your `$RUN_DIR`.

### Launch Profiling

To profile app start-up, use the `--launch` flag. Create a marker file before capturing to track which JSON files belong to this specific run:

```bash
cd "$RUN_DIR"
CAPTURE_MARKER="$RUN_DIR/.ettrace-capture-start"
: > "$CAPTURE_MARKER"
find "$RUN_DIR" -maxdepth 1 \( -name 'output.json' -o -name 'output_*.json' \) -delete

ettrace --simulator --launch --verbose --dsyms "$DSYMS"

```

### Runtime Profiling

For steady-state flow analysis, omit the `--launch` flag. This attaches to an already-running simulator instance and captures performance data until manually stopped:

```bash
ettrace --simulator --verbose --dsyms "$DSYMS"

```

## Preserving and Analyzing Results

Because subsequent ETTrace runs overwrite `output_*.json`, you must immediately preserve fresh results and run analysis to extract hotspots.

### Preserving JSON Outputs

Copy any JSON files newer than your capture marker into a preserved subdirectory and aggregate summaries. As implemented in [`SKILL.md`](https://github.com/openai/plugins/blob/main/SKILL.md) (lines 55–73):

```bash
PRESERVED_DIR="$(mktemp -d "$RUN_DIR/run-$(date +%Y%m%d-%H%M%S).XXXXXX")"
: > "$PRESERVED_DIR/summary.txt"

find "$RUN_DIR" -maxdepth 1 -name 'output_*.json' -newer "$CAPTURE_MARKER" -print |
while IFS= read -r json; do
  preserved="$PRESERVED_DIR/${json##*/}"
  cp "$json" "$preserved"
  {
    echo "## ${preserved##*/}"

    python3 "$SKILL_DIR/scripts/analyze_flamegraph_json.py" "$preserved"
  } >> "$PRESERVED_DIR/summary.txt"
done

```

### Analyzing Flame Graphs

The [`analyze_flamegraph_json.py`](https://github.com/openai/plugins/blob/main/analyze_flamegraph_json.py) script validates that the JSON conforms to the Homebrew ETTrace v1.1.0 processed shape (expecting a top-level `nodes` structure), aggregates inclusive and self-weights, filters out idle and unattributed frames, and prints three ranked tables:

- **Threads**: Total duration per thread
- **Top active self frames**: Pure work executed in specific functions
- **Top inclusive frames**: Call-stack contributions including callees

Filter for specific modules using `--pattern`, adjust the number of results with `--top`, and include wrapper frames with `--show-wrappers`:

```bash
python3 "$SKILL_DIR/scripts/analyze_flamegraph_json.py" \
  --top 20 \
  --pattern MyFeature \
  "$PRESERVED_DIR/output_main.json"

```

## Cleanup and Reporting

Present the aggregated [`summary.txt`](https://github.com/openai/plugins/blob/main/summary.txt) to the user, highlighting the profiled flow, simulator model, and total/idle/unattributed durations. Finally, remove the temporary ETTrace framework linkage from your project unless explicitly requested to keep it for additional profiling iterations.

## Summary

- **Isolate artifacts**: Use a dedicated `$RUN_DIR` to prevent conflicts between concurrent profiling sessions.
- **Install tooling**: The `ettrace` CLI from `emergetools/homebrew-tap/ettrace` communicates with the iOS simulator over localhost.
- **Build or vendor**: Create `ETTrace.xcframework` from source at tag `v1.1.0` or reuse an existing vendored copy.
- **Collect symbols**: Run [`collect_ios_dsyms.sh`](https://github.com/openai/plugins/blob/main/collect_ios_dsyms.sh) with `--search-root` and optional `--require-framework` to gather UUID-matched dSYMs.
- **Capture modes**: Use `--launch` for start-up profiling or omit it for runtime steady-state analysis.
- **Preserve immediately**: Copy `output_<thread>.json` files newer than the `.ettrace-capture-start` marker before subsequent runs overwrite them.
- **Analyze hotspots**: Use [`analyze_flamegraph_json.py`](https://github.com/openai/plugins/blob/main/analyze_flamegraph_json.py) to rank self and inclusive weights, filtering by pattern to isolate specific modules.

## Frequently Asked Questions

### What is ETTrace and why should I use it for iOS profiling?

ETTrace is an instrumentation framework that captures high-fidelity performance traces from iOS applications. It produces flame-graph JSON files that reveal exactly where CPU time is spent, including both system and application code. Unlike sampling profilers, ETTrace provides deterministic call-stack information essential for optimizing launch times and hot user flows.

### How do I ensure dSYMs match my app binaries during profiling?

Run the [`collect_ios_dsyms.sh`](https://github.com/openai/plugins/blob/main/collect_ios_dsyms.sh) script with your built `.app` path and multiple `--search-root` arguments covering `DerivedData` and your project directory. The script validates UUIDs between binaries and dSYM bundles. Use `--require-all-frameworks` to enforce that every embedded framework has matching symbols, preventing unsymbolicated traces.

### What is the difference between launch and runtime profiling?

**Launch profiling** (`--launch`) captures the entire app start-up sequence from process creation through initial view presentation, critical for analyzing cold start performance. **Runtime profiling** attaches to an already-running app and captures a specific user flow or interaction. Choose launch profiling to optimize startup time and runtime profiling to investigate specific feature performance.

### Can I filter the flame graph analysis to focus on specific code modules?

Yes. Pass the `--pattern` argument to [`analyze_flamegraph_json.py`](https://github.com/openai/plugins/blob/main/analyze_flamegraph_json.py) followed by a substring matching your module or class name. Combine this with `--top N` to limit results to the heaviest N functions. Use `--show-wrappers` to include synthetic compiler-generated wrapper frames if you suspect ABI or bridging overhead.