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_DIRto prevent conflicts between concurrent profiling sessions. - Install tooling: The
ettraceCLI fromemergetools/homebrew-tap/ettracecommunicates with the iOS simulator over localhost. - Build or vendor: Create
ETTrace.xcframeworkfrom source at tagv1.1.0or reuse an existing vendored copy. - Collect symbols: Run
collect_ios_dsyms.shwith--search-rootand optional--require-frameworkto gather UUID-matched dSYMs. - Capture modes: Use
--launchfor start-up profiling or omit it for runtime steady-state analysis. - Preserve immediately: Copy
output_<thread>.jsonfiles newer than the.ettrace-capture-startmarker before subsequent runs overwrite them. - Analyze hotspots: Use
analyze_flamegraph_json.pyto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →