How to Capture and Analyze ETTrace Profiles for iOS Applications

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 (lines 24–31), create this directory at the start of every profiling session:

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 (lines 35–37):

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 (lines 55–78):

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 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, 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:

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:

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:

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 (lines 55–73):

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 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:

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 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 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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →