From 8aebdec1ae5a730fdeea9ebebed2e3e9bdbe67e8 Mon Sep 17 00:00:00 2001 From: everettjf Date: Sun, 24 May 2026 16:53:55 -0700 Subject: [PATCH] docs: add a GitHub Pages tutorial site (Tutorial/) + deploy workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A static, no-build documentation site under Tutorial/ that covers AppleTrace end to end — introduction, quick start, installation, manual / Swift / objc_msgSend tutorials, event types, the Perfetto workflow, the demo apps, environment variables, platform support, and an FAQ. English is primary (index.html), Chinese secondary (zh.html), with an EN/中文 toggle. Design: a dark "trace-viewer" theme echoing a Perfetto timeline (teal brand accent, animated track-lane hero motif), distinctive type (Bricolage Grotesque + IBM Plex Sans/Mono), sticky sidebar nav with scroll-spy, copy buttons, and a mobile menu. Plain HTML/CSS + progressive-enhancement JS, so GitHub Pages serves it with no build step (.nojekyll). .github/workflows/pages.yml deploys Tutorial/ to Pages on pushes to master. One-time setup: Settings ▸ Pages ▸ Source = "GitHub Actions". README / README_CN link to the published guide. Both pages validated (balanced markup) and rendered (headless Chrome) in English and Chinese. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/pages.yml | 38 ++++ README.md | 2 + README_CN.md | 2 + Tutorial/.nojekyll | 0 Tutorial/README.md | 21 +++ Tutorial/app.js | 56 ++++++ Tutorial/index.html | 349 ++++++++++++++++++++++++++++++++++++ Tutorial/styles.css | 191 ++++++++++++++++++++ Tutorial/zh.html | 322 +++++++++++++++++++++++++++++++++ 9 files changed, 981 insertions(+) create mode 100644 .github/workflows/pages.yml create mode 100644 Tutorial/.nojekyll create mode 100644 Tutorial/README.md create mode 100644 Tutorial/app.js create mode 100644 Tutorial/index.html create mode 100644 Tutorial/styles.css create mode 100644 Tutorial/zh.html diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 00000000..074bef40 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,38 @@ +name: Deploy Tutorial to GitHub Pages + +# Publishes the static site in Tutorial/ to GitHub Pages. +# One-time setup: repo Settings ▸ Pages ▸ Build and deployment ▸ Source = +# "GitHub Actions". + +on: + push: + branches: [master] + paths: + - 'Tutorial/**' + - '.github/workflows/pages.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress one. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: Tutorial + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 4e7c2963..a0451a73 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ **A lightweight, embeddable Objective-C tracer that produces shareable [Perfetto](https://ui.perfetto.dev) traces** +📖 **[Read the full Tutorial & Usage Guide →](https://everettjf.github.io/AppleTrace/)** + [English](README.md) | [中文](README_CN.md) diff --git a/README_CN.md b/README_CN.md index 64eee740..42a38b4b 100644 --- a/README_CN.md +++ b/README_CN.md @@ -11,6 +11,8 @@ **轻量、可内嵌的 Objective-C 追踪器,产物可直接拖入 [Perfetto](https://ui.perfetto.dev) 分享** +📖 **[阅读完整教程与使用指南 →](https://everettjf.github.io/AppleTrace/zh.html)** + [English](README.md) | [中文](README_CN.md) diff --git a/Tutorial/.nojekyll b/Tutorial/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/Tutorial/README.md b/Tutorial/README.md new file mode 100644 index 00000000..f643d8f4 --- /dev/null +++ b/Tutorial/README.md @@ -0,0 +1,21 @@ +# AppleTrace Tutorial site + +A static, no-build GitHub Pages site — the comprehensive usage guide for +AppleTrace (English primary, Chinese secondary). + +- `index.html` — English guide +- `zh.html` — 中文指南 +- `styles.css`, `app.js` — shared styling and progressive-enhancement JS +- `.nojekyll` — serve files as-is (no Jekyll processing) + +## Preview locally + +```bash +cd Tutorial && python3 -m http.server 8000 # then open http://localhost:8000 +``` + +## Publishing + +Deployed by `.github/workflows/pages.yml` on every push to `master` that +touches `Tutorial/`. One-time setup: repo **Settings ▸ Pages ▸ Source = +GitHub Actions**. diff --git a/Tutorial/app.js b/Tutorial/app.js new file mode 100644 index 00000000..81338965 --- /dev/null +++ b/Tutorial/app.js @@ -0,0 +1,56 @@ +// AppleTrace tutorial — progressive enhancement only; the page works without JS. +(function () { + 'use strict'; + + // Copy buttons on code blocks. + document.querySelectorAll('.code').forEach(function (block) { + var head = block.querySelector('.code-head'); + if (!head) return; + var btn = head.querySelector('.copy'); + if (!btn) return; + btn.addEventListener('click', function () { + var code = block.querySelector('pre'); + if (!code) return; + navigator.clipboard.writeText(code.innerText).then(function () { + var prev = btn.textContent; + btn.textContent = 'copied'; + setTimeout(function () { btn.textContent = prev; }, 1400); + }); + }); + }); + + // Mobile sidebar toggle. + var sidebar = document.querySelector('.sidebar'); + var menuBtn = document.querySelector('.menu-btn'); + var scrim = document.querySelector('.scrim'); + function close() { sidebar && sidebar.classList.remove('open'); scrim && scrim.classList.remove('show'); } + if (menuBtn && sidebar) { + menuBtn.addEventListener('click', function () { + sidebar.classList.toggle('open'); + scrim && scrim.classList.toggle('show'); + }); + } + if (scrim) scrim.addEventListener('click', close); + sidebar && sidebar.querySelectorAll('a').forEach(function (a) { a.addEventListener('click', close); }); + + // Scroll-spy: highlight the sidebar link for the section in view. + var links = Array.prototype.slice.call(document.querySelectorAll('.sidebar a[href^="#"]')); + var map = {}; + links.forEach(function (a) { + var id = a.getAttribute('href').slice(1); + var sec = document.getElementById(id); + if (sec) map[id] = a; + }); + var sections = Object.keys(map).map(function (id) { return document.getElementById(id); }); + if ('IntersectionObserver' in window && sections.length) { + var current = null; + var obs = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { + if (e.isIntersecting) current = e.target.id; + }); + links.forEach(function (a) { a.classList.remove('active'); }); + if (current && map[current]) map[current].classList.add('active'); + }, { rootMargin: '-20% 0px -70% 0px', threshold: 0 }); + sections.forEach(function (s) { obs.observe(s); }); + } +})(); diff --git a/Tutorial/index.html b/Tutorial/index.html new file mode 100644 index 00000000..aa77b5b1 --- /dev/null +++ b/Tutorial/index.html @@ -0,0 +1,349 @@ + + + + + +AppleTrace — Tutorial & Usage Guide + + + + + +
+ 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. +
  3. 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.
  4. +
  5. 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
    +
  6. +
  7. Open it in Perfetto. Go to ui.perfetto.dev and drag in trace.json. That's it.
  8. +
+
💡

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

+
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

+

Three runnable samples, each a complete reference:

+ + + + + +
SampleLanguageDemonstrates
sample/ManualSectionDemoObjective-CManual sections, counters, async, threads
sample/AppleTraceSwiftDemoSwift@Traced/@TraceAll/withSpan + the AppleTraceAuto hook
sample/TraceAllMsgDemoObjective-CAutomatic 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.

+
+ +
+ +
+
+ + + + diff --git a/Tutorial/styles.css b/Tutorial/styles.css new file mode 100644 index 00000000..7160c462 --- /dev/null +++ b/Tutorial/styles.css @@ -0,0 +1,191 @@ +/* =========================================================================== + AppleTrace Tutorial — trace-viewer aesthetic + Dark, precise, developer-console feel echoing a Perfetto timeline. + =========================================================================== */ + +@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap'); + +:root { + --bg: #0c1014; + --bg-elev: #131a21; + --bg-elev-2: #1a232c; + --bg-code: #0f161d; + --border: #243039; + --border-soft: #1b242c; + --text: #e7eef4; + --text-dim: #98a6b3; + --text-faint: #65727d; + --accent: #4ecdc4; + --accent-deep: #2aa39a; + --coral: #ff6b6b; + --amber: #ffd166; + --violet: #b08cff; + --maxw: 1320px; + --sidebar-w: 264px; + --header-h: 60px; + --radius: 12px; + --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + --sans: 'IBM Plex Sans', system-ui, sans-serif; + --display: 'Bricolage Grotesque', var(--sans); +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; scroll-padding-top: calc(var(--header-h) + 20px); } + +body { + font-family: var(--sans); + background: var(--bg); + color: var(--text); + line-height: 1.65; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + /* faint trace-lane grid texture */ + background-image: + radial-gradient(900px 500px at 85% -8%, rgba(78, 205, 196, 0.10), transparent 60%), + radial-gradient(700px 500px at -5% 0%, rgba(176, 140, 255, 0.07), transparent 55%); + background-attachment: fixed; +} + +a { color: var(--accent); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* --------------------------------------------------------------- header --- */ +header.topbar { + position: fixed; inset: 0 0 auto 0; height: var(--header-h); z-index: 100; + display: flex; align-items: center; gap: 16px; + padding: 0 22px; + background: rgba(12, 16, 20, 0.82); + backdrop-filter: blur(14px); + border-bottom: 1px solid var(--border); +} +.brand { display: flex; align-items: center; gap: 10px; font-family: var(--display); font-weight: 700; font-size: 18px; letter-spacing: -0.02em; color: var(--text); } +.brand .dot { width: 11px; height: 11px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 14px var(--accent); } +.brand:hover { text-decoration: none; } +.topbar .spacer { flex: 1; } +.topbar nav { display: flex; align-items: center; gap: 6px; } +.topbar nav a { color: var(--text-dim); font-size: 14px; padding: 7px 11px; border-radius: 8px; } +.topbar nav a:hover { color: var(--text); background: var(--bg-elev); text-decoration: none; } +.lang-toggle { display: flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; } +.lang-toggle a { padding: 6px 11px; font-size: 13px; font-family: var(--mono); color: var(--text-dim); } +.lang-toggle a.active { background: var(--accent); color: #04201d; font-weight: 600; } +.lang-toggle a:hover { text-decoration: none; } +.menu-btn { display: none; background: none; border: 1px solid var(--border); color: var(--text); border-radius: 8px; width: 38px; height: 38px; font-size: 18px; cursor: pointer; } + +/* -------------------------------------------------------------- layout --- */ +.shell { display: grid; grid-template-columns: var(--sidebar-w) minmax(0, 1fr); max-width: var(--maxw); margin: 0 auto; } + +aside.sidebar { + position: sticky; top: var(--header-h); + height: calc(100vh - var(--header-h)); + overflow-y: auto; + padding: 28px 14px 60px 22px; + border-right: 1px solid var(--border-soft); +} +.sidebar h4 { font-family: var(--mono); font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--text-faint); margin: 22px 0 8px; } +.sidebar h4:first-child { margin-top: 0; } +.sidebar a { display: block; color: var(--text-dim); font-size: 14px; padding: 6px 12px; border-radius: 8px; border-left: 2px solid transparent; } +.sidebar a:hover { color: var(--text); background: var(--bg-elev); text-decoration: none; } +.sidebar a.active { color: var(--accent); border-left-color: var(--accent); background: linear-gradient(90deg, rgba(78,205,196,0.10), transparent); } + +main { padding: 40px clamp(22px, 5vw, 72px) 120px; min-width: 0; } +.content { max-width: 820px; } + +/* --------------------------------------------------------------- hero --- */ +.hero { padding-top: calc(var(--header-h) + 18px); margin-bottom: 30px; } +.eyebrow { font-family: var(--mono); font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); margin-bottom: 16px; } +.hero h1 { font-family: var(--display); font-weight: 800; font-size: clamp(40px, 7vw, 76px); line-height: 0.98; letter-spacing: -0.03em; margin-bottom: 18px; } +.hero h1 .apple { background: linear-gradient(120deg, var(--accent), var(--violet)); -webkit-background-clip: text; background-clip: text; color: transparent; } +.hero .lede { font-size: clamp(17px, 2.3vw, 21px); color: var(--text-dim); max-width: 640px; } +.hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 28px; } + +.btn { display: inline-flex; align-items: center; gap: 8px; padding: 11px 20px; border-radius: 10px; font-weight: 600; font-size: 15px; border: 1px solid transparent; cursor: pointer; } +.btn:hover { text-decoration: none; } +.btn-primary { background: var(--accent); color: #042320; } +.btn-primary:hover { background: #6fe0d8; transform: translateY(-1px); } +.btn-ghost { border-color: var(--border); color: var(--text); } +.btn-ghost:hover { border-color: var(--accent); color: var(--accent); } + +/* timeline motif — looks like a Perfetto track row set */ +.timeline { margin: 40px 0 8px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-elev); padding: 16px 18px; overflow: hidden; } +.tl-row { display: grid; grid-template-columns: 110px 1fr; align-items: center; gap: 12px; padding: 5px 0; } +.tl-label { font-family: var(--mono); font-size: 11px; color: var(--text-faint); text-align: right; white-space: nowrap; } +.tl-track { position: relative; height: 16px; } +.tl-slice { position: absolute; height: 100%; border-radius: 4px; opacity: 0; animation: slice-in 0.5s ease forwards; } +@keyframes slice-in { from { opacity: 0; transform: scaleX(0.4); transform-origin: left; } to { opacity: 0.92; transform: scaleX(1); } } + +/* --------------------------------------------------------- typography --- */ +.content section { padding-top: 14px; margin-bottom: 8px; } +.content h2 { font-family: var(--display); font-weight: 700; font-size: clamp(27px, 4vw, 36px); letter-spacing: -0.02em; margin: 46px 0 14px; padding-bottom: 10px; border-bottom: 1px solid var(--border-soft); } +.content h3 { font-family: var(--display); font-weight: 600; font-size: 21px; letter-spacing: -0.01em; margin: 30px 0 10px; color: var(--text); } +.content h4 { font-size: 15px; font-weight: 600; margin: 20px 0 6px; color: var(--accent); font-family: var(--mono); } +.content p { color: var(--text-dim); margin: 12px 0; } +.content ul, .content ol { color: var(--text-dim); margin: 12px 0 12px 22px; } +.content li { margin: 6px 0; } +.content strong { color: var(--text); font-weight: 600; } +.content em { color: var(--text); font-style: normal; border-bottom: 1px dotted var(--text-faint); } + +code { font-family: var(--mono); font-size: 0.88em; background: var(--bg-elev-2, #1a232c); color: var(--accent); padding: 2px 6px; border-radius: 5px; border: 1px solid var(--border-soft); } + +/* code blocks */ +.code { position: relative; margin: 16px 0; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-code); overflow: hidden; } +.code-head { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-bottom: 1px solid var(--border-soft); background: rgba(255,255,255,0.015); } +.code-head .lang { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-faint); } +.code-head .copy { margin-left: auto; background: none; border: 1px solid var(--border); color: var(--text-dim); font-family: var(--mono); font-size: 11px; padding: 3px 9px; border-radius: 6px; cursor: pointer; } +.code-head .copy:hover { color: var(--accent); border-color: var(--accent); } +.code pre { margin: 0; padding: 16px 18px; overflow-x: auto; } +.code code { background: none; border: none; color: #cdd9e5; padding: 0; font-size: 13.5px; line-height: 1.7; } +.code .k { color: var(--violet); } /* keyword */ +.code .s { color: var(--accent); } /* string */ +.code .c { color: var(--text-faint); font-style: italic; } /* comment */ +.code .f { color: var(--amber); } /* function / api */ +.code .n { color: var(--coral); } /* number / attr */ + +/* callouts */ +.callout { display: flex; gap: 12px; margin: 18px 0; padding: 14px 16px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-elev); } +.callout .ic { font-size: 18px; line-height: 1.5; } +.callout p { margin: 0; color: var(--text-dim); } +.callout.tip { border-left: 3px solid var(--accent); } +.callout.warn { border-left: 3px solid var(--amber); } +.callout.danger { border-left: 3px solid var(--coral); } + +/* feature / route cards */ +.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin: 20px 0; } +.card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-elev); padding: 20px; } +.card h3 { margin-top: 0; } +.card .tag { display: inline-block; font-family: var(--mono); font-size: 11px; padding: 2px 8px; border-radius: 20px; margin-bottom: 10px; } +.tag.everywhere { color: var(--accent); border: 1px solid var(--accent-deep); } +.tag.sim { color: var(--amber); border: 1px solid #6b5a1f; } +.tag.arm { color: var(--violet); border: 1px solid #4b3b78; } + +/* tables */ +table { width: 100%; border-collapse: collapse; margin: 18px 0; font-size: 14px; } +th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--border-soft); } +th { font-family: var(--mono); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--text-faint); } +td { color: var(--text-dim); } +td code { font-size: 12.5px; } + +/* step list */ +.steps { list-style: none; margin-left: 0; counter-reset: step; } +.steps > li { position: relative; padding-left: 44px; margin: 16px 0; } +.steps > li::before { counter-increment: step; content: counter(step); position: absolute; left: 0; top: 0; width: 28px; height: 28px; display: grid; place-items: center; border-radius: 50%; background: var(--bg-elev); border: 1px solid var(--accent-deep); color: var(--accent); font-family: var(--mono); font-size: 13px; font-weight: 600; } + +footer { border-top: 1px solid var(--border-soft); margin-top: 60px; padding: 30px clamp(22px,5vw,72px); color: var(--text-faint); font-size: 13px; } +footer a { color: var(--text-dim); } + +.zh-note { font-family: var(--mono); font-size: 12.5px; color: var(--text-faint); background: var(--bg-elev); border: 1px dashed var(--border); border-radius: 8px; padding: 10px 14px; margin: 16px 0; } + +/* --------------------------------------------------------- responsive --- */ +@media (max-width: 900px) { + .shell { grid-template-columns: 1fr; } + aside.sidebar { + position: fixed; top: var(--header-h); left: 0; bottom: 0; width: 80%; max-width: 320px; + background: var(--bg); z-index: 90; border-right: 1px solid var(--border); + transform: translateX(-105%); transition: transform 0.25s ease; + } + aside.sidebar.open { transform: translateX(0); } + .menu-btn { display: grid; place-items: center; } + .topbar nav .hide-sm { display: none; } + .scrim { position: fixed; inset: var(--header-h) 0 0 0; background: rgba(0,0,0,0.5); z-index: 85; opacity: 0; pointer-events: none; transition: opacity 0.2s; } + .scrim.show { opacity: 1; pointer-events: auto; } +} diff --git a/Tutorial/zh.html b/Tutorial/zh.html new file mode 100644 index 00000000..454d0875 --- /dev/null +++ b/Tutorial/zh.html @@ -0,0 +1,322 @@ + + + + + +AppleTrace — 教程与使用指南 + + + + + +
+ AppleTrace + +
+ +
+ +
+ +
+ + +
+
+ +
+
iOS / macOS 追踪 → Perfetto
+

AppleTrace》使用手册

+

一个轻量、可嵌入的 tracer:记录 App 的执行时间线——手动 section、Swift 宏、或每一次 objc_msgSend——并直接在浏览器里用 Perfetto 呈现。

+ + + +
+ +
+

简介

+

AppleTrace 给你的 App 埋点——加 marker、标注 Swift 函数、或 hook 消息发送——把事件时间线写进沙盒里的 trace 片段。一套小的 Python 流水线把这些片段合并成一个 trace.json,直接在 Perfetto 里打开,查看调用时间线、耗时、线程与计数器。

+

产生事件有三种方式,按需选用,它们都落进同一条 trace:

+
+
+ 全平台 +

手动 section

+

APTBeginSection/APTEndSection(或 Swift 宏)包住代码。风险最低,精确控制要计时的范围。

+
+
+ arm64 +

objc_msgSend hook

+

用 fishhook 式符号重绑自动追踪每一次 OC 消息发送。零改动,仅限 Objective-C。

+
+
+ 模拟器 / macOS +

Swift 自动 hook

+

可选的 AppleTraceAuto 桥接 SwiftTrace,零标注追踪 Swift 类层级。

+
+
+
+ +
+

快速开始

+

最快看到 trace 的方式是内置的示例 App——无需自己写埋点。

+
    +
  1. 克隆并打开示例。 +
    bash
    git clone https://github.com/everettjf/AppleTrace.git
    +cd AppleTrace
    +open sample/AppleTraceSwiftDemo/AppleTraceSwiftDemo.xcodeproj
    +
  2. +
  3. 运行(模拟器或真机),点击 Generate Trace。App 会跑一段多线程负载,并显示磁盘上的 trace 目录
  4. +
  5. 在 Mac 上合并片段: +
    bash
    python3 merge.py -d "<App 中显示的 trace 目录>"
    +# → 在片段旁生成 trace.json
    +
  6. +
  7. 在 Perfetto 打开。进入 ui.perfetto.dev,把 trace.json 拖进去即可。
  8. +
+
💡

想更快?sh go.sh "<trace 目录>" 一步完成合并并打开 Perfetto。

+
+ +
+

安装

+

环境要求

+
    +
  • Xcode、Python 3、浏览器(Perfetto 在 ui.perfetto.dev)。
  • +
  • ldid 仅在重签 loader 时需要;pytest 仅跑测试时需要。
  • +
+ +

Swift Package(Swift 项目,也是新项目最干净的方式)

+
swift — Package.swift
dependencies: [
+    .package(url: "https://github.com/everettjf/AppleTrace.git", branch: "master"),
+],
+targets: [
+    .target(name: "MyApp", dependencies: [
+        .product(name: "AppleTrace", package: "AppleTrace"),
+        // 可选,仅限模拟器/macOS:
+        .product(name: "AppleTraceAuto", package: "AppleTrace"),
+    ]),
+]
+ +

嵌入 framework(Objective-C / C / C++)

+

打开 appletrace/appletrace.xcodeproj 构建 framework,把 appletrace.framework 嵌入你的 target。可参考 sample/ManualSectionDemo

+
+ +
+

教程 · 手动埋点

+

手动 section 是推荐的基线——适配所有 iOS/macOS 版本,产出命名清晰的切片。

+

Objective-C

+
objective-c
#import <appletrace/appletrace.h>
+
+- (void)viewDidLoad {
+    APTBegin;                       // 自动命名为 "[类名 viewDidLoad]"
+    [super viewDidLoad];
+    APTEnd;
+}
+
+- (void)loadFeed {
+    APTBeginSection("network");     // 自定义 section 名
+    // ... 工作 ...
+    APTEndSection("network");
+}
+

APTBegin/APTEnd 用类名+方法名自动命名;要自定义就用 APTBeginSection/APTEndSection。同线程内按 LIFO 嵌套。

+

C / C++

+
cpp
#include <appletrace/appletrace.h>
+
+void safer() {
+    APTScopeSection("decode");      // RAII:作用域结束自动 end
+    // ... 工作 ...
+}
+
⚠️

事件按线程缓冲,在达到阈值、调用 APTFlush()、或线程退出时落盘。读取 trace 前务必调用 APTFlush()(比如进入后台时),否则短小的运行可能看起来是空的。

+
+ +
+

教程 · 追踪 Swift

+

objc_msgSend hook 看不到 Swift 的静态 / vtable / witness 派发,所以 Swift 走源码级埋点。加入 Swift 包后 import AppleTrace

+

作用域 span 与宏

+
swift
import AppleTrace
+
+// 作用域 span——即使 throw / 提前返回也会闭合:
+withSpan("loadFeed") { try? loadFeed() }
+
+// 标注函数。section 以 #function 命名,对 final 类、struct、protocol
+// 方法都生效——begin/end 在编译期插入函数体,绕开了派发问题。
+@Traced
+func decodeImage() { /* ... */ }
+
+// @TraceAll 给每个有函数体的方法都加上 @Traced:
+@TraceAll
+final class FeedViewModel {
+    func reload() { /* 已追踪 */ }
+    func render() { /* 已追踪 */ }
+}
+
+APTFlush()   // 或 AppleTrace.flush()
+

零标注自动追踪

+

可选的 AppleTraceAuto 桥接 SwiftTrace,无需标注即可 hook 类层级:

+
swift
import AppleTraceAuto
+
+#if targetEnvironment(simulator)
+AppleTraceAuto.trace(aClass: FeedViewModel.self)   // 进入/退出 → AppleTrace
+#endif
+
🚫

AppleTraceAuto 仅限模拟器 / macOS。SwiftTrace 改写经过指针认证的 vtable 槽,真机上不安全——务必用 #if targetEnvironment(simulator) 包起来。它也看不到 final / 静态派发的方法。宏没有这些限制,是真机上的首选。

+
+ +
+

教程 · 自动 objc_msgSend Hook

+

Objective-C app 可以无埋点追踪每一次消息发送。该 hook 是 arm64 上 fishhook 式的符号重绑,启动后安装一次:

+
objective-c
// 例如在 application:didFinishLaunchingWithOptions: 早期
+if (APTInstallObjcMsgSendHook()) {
+    NSLog(@"AppleTrace: objc_msgSend hook 已安装");
+}
+

每次被追踪的发送会变成 [类]方法 的 begin/end 对。完整示例见 sample/TraceAllMsgDemo,其中验证了浮点参数、结构体返回与 super 派发的处理。

+
⚠️

仅限 arm64。该 hook 在 arm64e 上会硬报错(调用方通过认证的 GOT 入口到达 objc_msgSend)。请构建纯 arm64 切片。

+
+ +
+

教程 · 事件类型

+

除了 section,AppleTrace 还记录 Perfetto 能绘制的事件种类:

+
objective-c
// 在当前线程时间线上打一个点
+APTInstant("cache_miss");
+
+// 随时间变化的数值 → counter 曲线(内存、FPS、队列深度…)
+APTCounter("resident_mb", 142.5);
+
+// 跨线程/队列的工作,按 (name, id) 匹配 → 异步弧
+uint64_t reqID = 42;
+APTAsyncBegin("image_load", reqID);
+dispatch_async(queue, ^{
+    APTAsyncEnd("image_load", reqID);
+});
+ + + + + + +
事件APIPerfetto 呈现
SectionAPTBeginSection / APTEndSection线程轨道上的嵌套切片
InstantAPTInstant时间点 marker
CounterAPTCounter曲线轨道
AsyncAPTAsyncBegin / APTAsyncEnd可跨线程的弧
+

Swift 封装一一对应:traceInstanttraceCounterasyncBeginasyncEnd

+
+ +
+

在 Perfetto 查看

+

1 · 拉取 trace 片段

+

片段写在 <app 沙盒>/Library/appletracedata

+
    +
  • 模拟器:目录就在 Mac 本地,示例 App 会显示路径。
  • +
  • 真机:Xcode ▸ Window ▸ Devices and Simulators ▸ Download Container,或 xcrun devicectl device copy from … --domain-type appDataContainer --source Library/appletracedata --destination ./trace
  • +
+

2 · 合并

+
bash
python3 merge.py -d /path/to/appletracedata     # → trace.json
+# 或统一 CLI:
+python3 scripts/appletrace_cli.py open /path/to/appletracedata
+# 或合并并打开 Perfetto:
+sh go.sh /path/to/appletracedata
+

3 · 浏览

+

trace.json 拖进 ui.perfetto.dev。用 W/S 缩放、A/D 平移,搜索框可跳转到切片。

+

二进制片段(可选,更小更快)

+

设置 APPLETRACE_BINARY=1 改写紧凑的二进制片段格式。merge.py 对两种格式都透明解码——工作流不变。

+
+ +
+

示例 App

+ + + + + +
示例语言演示
sample/ManualSectionDemoObjective-C手动 section、counter、async、多线程
sample/AppleTraceSwiftDemoSwift@Traced/@TraceAll/withSpan + AppleTraceAuto hook
sample/TraceAllMsgDemoObjective-C自动 objc_msgSend hook
+

每个引导式 App 都有 Generate Trace 按钮,显示 trace 目录并打印合并/Perfetto 命令。Swift demo 依赖本地包,请从仓库内打开,让 Xcode 自动解析 product。

+
+ +
+

环境变量

+ + + + + + + +
变量默认作用
APPLETRACE_ENABLEDtrue录制总开关。
APPLETRACE_BINARYfalse写二进制片段而非文本。
APPLETRACE_DATA_DIR沙盒覆盖片段写入目录。
APPLETRACE_BLOCK_SIZE_MB16每个片段的 mmap 块大小(1–256 MB)。
APPLETRACE_KEEP_EXISTINGfalse保留旧 trace 目录而非覆盖。
+

代码内也有运行时控制:APTSetEnabled(BOOL)APTIsEnabled()APTGetTraceDirectory()APTFlush()APTSyncWait()

+
+ +
+

平台支持

+ + + + + + +
模式适用范围
手动 section 与事件所有 iOS/macOS 版本,全语言
Swift 宏(@Traced/@TraceAll/withSpan模拟器与真机
objc_msgSend hookarm64(非 arm64e)
AppleTraceAuto(SwiftTrace)仅模拟器 / macOS
+

为什么 Swift 需要源码级追踪

+

Swift 为性能绕开消息派发:struct/final 方法与全模块优化的调用走静态派发;类方法走 vtable;protocol 方法走 witness table。它们都不经过 objc_msgSend,所以自动 hook 只能看到很薄的 @objc dynamic 表面。宏直接在源码插桩,因此覆盖全部四种派发。

+
+ +
+

FAQ 与排查

+

trace 是空的 / 只有元数据

+

writer 按线程批量缓冲。读取前调用 APTFlush()(如进入后台、或场景结束时)。示例 App 已替你 flush。

+

构建报错:SDK does not contain 'libarclite'

+

部署目标过旧。把 IPHONEOS_DEPLOYMENT_TARGET 设为 12.0 或更高——新版 Xcode 移除了 pre-12 的 ARC 兼容库。

+

Swift app 启动崩溃 Library not loaded: @rpath/SwiftTrace.framework

+

app target 需要 LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks,dyld 才能在真机上找到内嵌的动态 framework。

+

AppleTraceAuto 某些方法追踪不到

+

SwiftTrace 无法 hook final / 静态派发的方法,且仅限模拟器/macOS。这类请用 @Traced/@TraceAll 宏。

+

命令行 xcodebuild 找不到宏插件的 SwiftSyntax

+

去掉 -sdk iphonesimulator,只用 -destination-sdk 会把 host 宏插件错误地按目标 SDK 编译。

+
+ +
+ +
+
+ + + +