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/ManualSectionDemo 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
+Three runnable samples, each a complete reference:
+| Sample | Language | Demonstrates |
|---|---|---|
sample/ManualSectionDemo | Objective-C | Manual sections, counters, async, threads |
sample/AppleTraceSwiftDemo | Swift | @Traced/@TraceAll/withSpan + the AppleTraceAuto hook |
sample/TraceAllMsgDemo | Objective-C | 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.