AppleTrace
iOS / macOS Tracing → Perfetto

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:

all platforms

Manual sections

Wrap code in APTBeginSection/APTEndSection (or the Swift macros). The lowest-risk baseline; you control exactly what is timed.

arm64

objc_msgSend hook

Automatically trace every Objective-C message send via a fishhook-style symbol rebind. Zero code changes, Objective-C only.

Simulator / macOS

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.

  1. Clone & open a demo.
    bash
    git clone https://github.com/everettjf/AppleTrace.git
    cd AppleTrace
    open sample/AppleTraceSwiftDemo/AppleTraceSwiftDemo.xcodeproj
  2. 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.
  3. 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
  4. 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).
  • ldid only if you re-sign loader builds; pytest only 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):

swift — Package.swift
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

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

cpp
#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

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

swift
import AppleTraceAuto

#if targetEnvironment(simulator)
AppleTraceAuto.trace(aClass: FeedViewModel.self)   // entry/exit → AppleTrace
#endif
🚫

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

objective-c
// 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:

objective-c
// 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);
});
EventAPIPerfetto rendering
SectionAPTBeginSection / APTEndSectionNested slices on a thread track
InstantAPTInstantA marker at a point in time
CounterAPTCounterA graph track
AsyncAPTAsyncBegin / APTAsyncEndArcs 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

bash
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/appletracedata

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

SampleLanguageDemonstrates
sample/AppleTraceSwiftDemoSwiftwithSpan + @Traced/@TraceAll macros, counters/async, and the AppleTraceAuto hook
sample/TraceAllMsgDemoObjective-CManual 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

VariableDefaultEffect
APPLETRACE_ENABLEDtrueMaster on/off switch for recording.
APPLETRACE_BINARYfalseWrite compact binary fragments instead of text.
APPLETRACE_DATA_DIRsandboxOverride where fragments are written.
APPLETRACE_BLOCK_SIZE_MB16Per-fragment mmap block size (1–256 MB).
APPLETRACE_KEEP_EXISTINGfalseKeep prior trace dirs instead of replacing them.

Runtime controls are also available in code: APTSetEnabled(BOOL), APTIsEnabled(), APTGetTraceDirectory(), APTFlush(), APTSyncWait().

Platform Support

ModeWhere it works
Manual sections & eventsEvery iOS/macOS version, all languages
Swift macros (@Traced/@TraceAll/withSpan)Simulator and device
objc_msgSend hookarm64 (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.