The AppleTrace field guide.
A lightweight, embeddable tracer that records your app's execution timeline — manual sections, Swift macros, or every objc_msgSend — and renders it in Perfetto, right in the browser.
Introduction
AppleTrace instruments your app — by adding markers, annotating Swift functions, or hooking message sends — and writes a timeline of events into sandbox trace fragments. A small Python pipeline merges those fragments into a single trace.json that you open directly in Perfetto to explore the call timeline, durations, threads, and counters.
There are three ways to produce events. Pick whichever fits — they all land in the same trace:
Manual sections
Wrap code in APTBeginSection/APTEndSection (or the Swift macros). The lowest-risk baseline; you control exactly what is timed.
objc_msgSend hook
Automatically trace every Objective-C message send via a fishhook-style symbol rebind. Zero code changes, Objective-C only.
Swift auto-hook
The optional AppleTraceAuto product bridges SwiftTrace to trace Swift class hierarchies with no annotations.
Quick Start
The fastest path to a trace is the bundled demo app — no instrumentation to write.
- Clone & open a demo.
bash
git clone https://github.com/everettjf/AppleTrace.git cd AppleTrace open sample/AppleTraceSwiftDemo/AppleTraceSwiftDemo.xcodeproj - Run it on a Simulator (or device) and tap Generate Trace. The app runs a multi-threaded workload and prints the on-disk trace directory.
- Merge the fragments into a single trace on your Mac:
bash
python3 merge.py -d "<trace directory shown in the app>" # → writes trace.json next to the fragments - Open it in Perfetto. Go to ui.perfetto.dev and drag in
trace.json. That's it.
In a hurry? sh go.sh "<trace directory>" merges and opens Perfetto in one step.
Installation
Requirements
- Xcode, Python 3, and a browser (Perfetto runs at ui.perfetto.dev).
ldidonly if you re-sign loader builds;pytestonly for the test suite.
Swift Package (for Swift, and the cleanest path for new projects)
Add the package and depend on the AppleTrace product (and optionally AppleTraceAuto):
dependencies: [
.package(url: "https://github.com/everettjf/AppleTrace.git", branch: "master"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "AppleTrace", package: "AppleTrace"),
// optional, Simulator/macOS only:
.product(name: "AppleTraceAuto", package: "AppleTrace"),
]),
]Framework embedding (Objective-C / C / C++)
Open appletrace/appletrace.xcodeproj, build the framework, and embed appletrace.framework into your target. See sample/TraceAllMsgDemo for a working setup.
Tutorial · Manual Instrumentation
Manual sections are the recommended baseline — they work on every iOS/macOS version and produce clean, descriptively-named slices.
Objective-C
#import <appletrace/appletrace.h>
- (void)viewDidLoad {
APTBegin; // auto-named "[ClassName viewDidLoad]"
[super viewDidLoad];
APTEnd;
}
- (void)loadFeed {
APTBeginSection("network"); // explicit section name
// ... work ...
APTEndSection("network");
}APTBegin / APTEnd name the section after the enclosing class and selector. Use APTBeginSection / APTEndSection when you want a custom name. Pairs nest on the same thread (LIFO).
C / C++
#include <appletrace/appletrace.h>
void process() {
APTBeginSection("process");
// ... work ...
APTEndSection("process");
}
void safer() {
APTScopeSection("decode"); // RAII: ends automatically at scope exit
// ... work ...
}Events are buffered per thread and flushed on a size threshold, on APTFlush(), or at thread exit. Call APTFlush() (e.g. when backgrounding) before pulling the trace, or short-lived runs may look empty.
Tutorial · Tracing Swift
The objc_msgSend hook can't see Swift's static / vtable / witness dispatch, so Swift is traced at the source level. After adding the Swift package, import AppleTrace.
Scoped spans & macros
import AppleTrace
// Scoped span — closes even on throw / early return:
withSpan("loadFeed") { try? loadFeed() }
// Annotate a function. The section is named after #function, so it works
// for final classes, structs, and protocol methods alike — the begin/end
// is inserted into the body at compile time, sidestepping dispatch entirely.
@Traced
func decodeImage() { /* ... */ }
// @TraceAll stamps @Traced onto every method with a body:
@TraceAll
final class FeedViewModel {
func reload() { /* traced */ }
func render() { /* traced */ }
}
APTFlush() // or AppleTrace.flush()Zero-annotation auto-tracing
The optional AppleTraceAuto product bridges SwiftTrace to hook a class hierarchy without annotations:
import AppleTraceAuto
#if targetEnvironment(simulator)
AppleTraceAuto.trace(aClass: FeedViewModel.self) // entry/exit → AppleTrace
#endifAppleTraceAuto is Simulator / macOS only. SwiftTrace patches pointer-authenticated vtable slots, which is unsafe on real devices — always gate it with #if targetEnvironment(simulator). It also can't see final / statically-dispatched methods. The macros have neither limitation and are the on-device path.
Tutorial · Automatic objc_msgSend Hook
For Objective-C apps you can trace every message send with no manual markers. The hook is an arm64 fishhook-style symbol rebind; install it once after launch:
// e.g. early in application:didFinishLaunchingWithOptions:
if (APTInstallObjcMsgSendHook()) {
NSLog(@"AppleTrace: objc_msgSend hook installed");
}Every traced send becomes a [Class]selector begin/end pair. See sample/TraceAllMsgDemo for a full setup, including the validated handling of floating-point arguments, struct returns, and super dispatch.
arm64 only. The hook hard-errors on arm64e (its callers reach objc_msgSend through authenticated GOT entries). Build a plain arm64 slice.
Tutorial · Event Types
Beyond sections, AppleTrace records the event kinds Perfetto knows how to draw:
// Instantaneous marker on the current thread's timeline
APTInstant("cache_miss");
// A value plotted over time → a counter graph track (memory, FPS, queue depth…)
APTCounter("resident_mb", 142.5);
APTCounter("fps", 60);
// Work that flows across threads / queues, matched by (name, id) → async arc
uint64_t reqID = 42;
APTAsyncBegin("image_load", reqID);
dispatch_async(queue, ^{
APTAsyncEnd("image_load", reqID);
});| Event | API | Perfetto rendering |
|---|---|---|
| Section | APTBeginSection / APTEndSection | Nested slices on a thread track |
| Instant | APTInstant | A marker at a point in time |
| Counter | APTCounter | A graph track |
| Async | APTAsyncBegin / APTAsyncEnd | Arcs that can cross threads |
The Swift wrappers mirror these: traceInstant, traceCounter, asyncBegin, asyncEnd.
Visualize in Perfetto
1 · Pull the trace fragments
Fragments are written to <app sandbox>/Library/appletracedata.
- Simulator: the folder is already on your Mac; the demo app prints the path.
- Device: Xcode ▸ Window ▸ Devices and Simulators ▸ Download Container, or
xcrun devicectl device copy from … --domain-type appDataContainer --source Library/appletracedata --destination ./trace.
2 · Merge
python3 merge.py -d /path/to/appletracedata # → trace.json
# or the unified CLI:
python3 scripts/appletrace_cli.py open /path/to/appletracedata
# or merge AND open Perfetto:
sh go.sh /path/to/appletracedata3 · Explore
Drag trace.json into ui.perfetto.dev. Use W/S to zoom, A/D to pan, and the search box to jump to a slice.
Binary fragments (optional, smaller & faster)
Set APPLETRACE_BINARY=1 to write a compact binary fragment format instead of text. merge.py decodes both transparently — no workflow change.
Demo Apps
Two runnable samples, each a complete reference:
| Sample | Language | Demonstrates |
|---|---|---|
sample/AppleTraceSwiftDemo | Swift | withSpan + @Traced/@TraceAll macros, counters/async, and the AppleTraceAuto hook |
sample/TraceAllMsgDemo | Objective-C | Manual APTBeginSection sections and the automatic objc_msgSend hook |
Each guided app has a Generate Trace button, shows the trace directory, and prints the exact merge/Perfetto commands. The Swift demo consumes the local package, so open it from the repo so Xcode resolves the products automatically.
Environment Variables
| Variable | Default | Effect |
|---|---|---|
APPLETRACE_ENABLED | true | Master on/off switch for recording. |
APPLETRACE_BINARY | false | Write compact binary fragments instead of text. |
APPLETRACE_DATA_DIR | sandbox | Override where fragments are written. |
APPLETRACE_BLOCK_SIZE_MB | 16 | Per-fragment mmap block size (1–256 MB). |
APPLETRACE_KEEP_EXISTING | false | Keep prior trace dirs instead of replacing them. |
Runtime controls are also available in code: APTSetEnabled(BOOL), APTIsEnabled(), APTGetTraceDirectory(), APTFlush(), APTSyncWait().
Platform Support
| Mode | Where it works |
|---|---|
| Manual sections & events | Every iOS/macOS version, all languages |
Swift macros (@Traced/@TraceAll/withSpan) | Simulator and device |
objc_msgSend hook | arm64 (not arm64e) |
AppleTraceAuto (SwiftTrace) | Simulator / macOS only |
Why Swift needs source-level tracing
Swift avoids message dispatch for speed: struct/final methods and whole-module-optimized calls are statically dispatched; class methods use a vtable; protocol methods use a witness table. None go through objc_msgSend, so only the thin @objc dynamic surface is visible to the auto-hook. The macros instrument the source directly, which is why they cover all four dispatch kinds.
FAQ & Troubleshooting
My trace is empty / only has metadata
The writer batches per thread. Call APTFlush() before reading the trace (e.g. on backgrounding, or at the end of your scenario). The demo apps flush for you.
Build fails: SDK does not contain 'libarclite'
An old deployment target. Set IPHONEOS_DEPLOYMENT_TARGET to 12.0 or later — recent Xcode dropped the ARC compatibility library for pre-12 targets.
The Swift app crashes at launch with Library not loaded: @rpath/SwiftTrace.framework
The app target needs LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks so dyld can find the embedded dynamic framework on device.
AppleTraceAuto traces nothing for some methods
SwiftTrace can't hook final or statically-dispatched methods, and it's Simulator/macOS only. Use the @Traced/@TraceAll macros for those.
Command-line xcodebuild can't find SwiftSyntax for the macro plugin
Drop the -sdk iphonesimulator flag and pass only -destination; -sdk forces the host macro plugin onto the wrong SDK.