Skip to content

Trace Swift code: @Traced/@TraceAll macros (primary) + SwiftTrace bridge (secondary)#14

Merged
everettjf merged 6 commits into
masterfrom
feature/swift-tracing
May 24, 2026
Merged

Trace Swift code: @Traced/@TraceAll macros (primary) + SwiftTrace bridge (secondary)#14
everettjf merged 6 commits into
masterfrom
feature/swift-tracing

Conversation

@everettjf

@everettjf everettjf commented May 24, 2026

Copy link
Copy Markdown
Owner

Why

AppleTrace's automatic mode rebinds objc_msgSend, but Swift's static / vtable / witness dispatch never goes through objc_msgSend, so most Swift code is invisible to the auto-hook. This adds Swift tracing via a new SwiftPM package, following the two-route plan in docs/swift-tracing.md.

What

A SwiftPM package at the repo root (reuses the existing appletrace.mm core — single source of truth, included from the Xcode tree, not duplicated).

Primary route — Swift macros (AppleTrace). Source-level instrumentation that sidesteps dispatch entirely (begin/end inserted into the body at compile time), so it covers final classes, structs, and protocol methods alike:

  • withSpan(_:_:) + beginSection/endSection/traceInstant/traceCounter/asyncBegin/asyncEnd/flush wrappers.
  • @Traced (body macro) wraps a function body in a #function-named section.
  • @TraceAll (member-attribute macro) stamps @Traced on every method with a body.
import AppleTrace
withSpan("loadFeed") { try? loadFeed() }

@TraceAll final class FeedViewModel { func reload() {}; func render() {} }

Secondary route — SwiftTrace bridge (AppleTraceAuto, optional). Zero-annotation auto-tracing by bridging johnno1962/SwiftTrace: a SwiftTrace.Decorated subclass forwards each traced method's entry/exit to APTBeginSection/APTEndSection.

import AppleTraceAuto
AppleTraceAuto.trace(aClass: FeedViewModel.self)

The two routes complement each other: SwiftTrace can't see final/statically-dispatched methods (its documented blind spot) — the macros cover those.

Verification

  • swift test — 6 tests green on Swift 6.2.4 with no experimental feature flags: @Traced/@TraceAll expansion, a usage test that applies the macros and asserts the function names land in the trace, and a runtime test driving the C core through withSpan.
  • swift run AppleTraceAutoExample — verifies the SwiftTrace bridge end-to-end (traces a non-final class through a protocol; exits non-zero if not captured). The bridge can't be exercised from an XCTest bundle because SwiftTrace's metadata scanning needs a normal executable / app image.
  • Both AppleTrace and AppleTraceAuto build for the iOS Simulator (xcodebuild).
  • New Swift Tests CI workflow runs all of the above on macos-latest.

Demo app

sample/AppleTraceSwiftDemo (SwiftUI) consumes the local package and demonstrates both routes in one guided app: tap Generate Trace to run a multi-threaded workload (@Traced/@TraceAll/withSpan spans, an AppleTraceAuto-hooked ImageLoader, counters, async arcs, a 60-frame render loop). The screen shows the trace directory and the merge/Perfetto steps. Verified on the iPhone 17 Simulator (~490 events, 5 named tracks, both routes, no hang) and built+signed for device. CI also builds it.

Build note: use -destination only (no -sdk iphonesimulator, which forces the macro plugin onto the wrong SDK).

Notes

  • The existing Xcode framework / Objective-C objc_msgSend hook are unchanged.
  • docs/swift-tracing.md has the full landscape survey, the rationale, and per-route limitations.

🤖 Generated with Claude Code

everettjf and others added 6 commits May 24, 2026 12:39
Adds a SwiftPM package so AppleTrace can trace Swift, which the
objc_msgSend hook can't reach (Swift's static / vtable / witness dispatch
never goes through objc_msgSend). The primary route is source-level
instrumentation via Swift macros, which sidesteps dispatch entirely —
the begin/end calls are inserted into the function body at compile time,
so they cover final classes, structs, and protocol methods alike.

Package layout:
- CAppleTrace: reuses the existing appletrace.mm core (single source of
  truth — included from the Xcode tree, not duplicated).
- AppleTrace: idiomatic Swift API (withSpan, beginSection/endSection,
  traceInstant/Counter, asyncBegin/End, flush, setEnabled, traceDirectory)
  plus the macro declarations.
- AppleTraceMacrosPlugin: SwiftSyntax compiler plugin implementing
  @Traced (body macro) and @traceall (member-attribute macro that stamps
  @Traced on every method with a body).

Tests (all green on Swift 6.2.4, no experimental flags needed):
- Macro expansion tests for @Traced and @traceall.
- A usage test that actually applies the macros and asserts the function
  names land in the trace.
- A runtime test that drives the C core through withSpan and verifies a
  fragment is written.

docs/swift-tracing.md updated to record the chosen direction and status.
The SwiftTrace-style runtime hook (secondary route) lands separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds AppleTraceAuto, an optional product that bridges the proven
johnno1962/SwiftTrace runtime hook into AppleTrace: a SwiftTrace.Decorated
subclass forwards each traced method's entry/exit to APTBeginSection /
APTEndSection, so zero-annotation auto-tracing of Swift class hierarchies
lands in the same Perfetto trace.

API: AppleTraceAuto.trace(aClass:) / traceClasses(matchingPattern:) /
traceBundle(containing:) / stop(). Per SwiftTrace's design it can't see
final / statically-dispatched methods — the @Traced / @traceall macros
cover those, so the two routes complement each other.

Verification: the SwiftTrace vtable patch can't be exercised from an
XCTest bundle (its metadata scanning needs a normal executable / app
image), so the bridge is verified by the runnable AppleTraceAutoExample
target instead — `swift run AppleTraceAutoExample` traces a non-final
class through a protocol and asserts the methods land in the trace
(exits non-zero otherwise). The macro + runtime XCTests stay green.

README / README_CN gain a "Tracing Swift" section; docs/swift-tracing.md
records both routes as implemented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds sample/AppleTraceSwiftDemo, a SwiftUI app that consumes the local
SwiftPM package and demonstrates both Swift tracing routes in one guided
app:
- macros / withSpan: an "App Launch" span tree, an @Traced free function,
  a @traceall final class, async arcs, counters, and a 60-frame render
  loop across named worker threads;
- AppleTraceAuto: zero-annotation hooking of a non-final ImageLoader
  reached through a protocol.

Tap "Generate Trace" (or set APPLETRACE_AUTORUN=1) and the screen shows
the trace directory plus the exact merge/Perfetto steps. Verified on the
iPhone 17 Simulator — ~490 events across 5 named tracks, both routes
present, no hang — and built + signed for a real device.

Bridge hardening (Sources/AppleTraceAuto): subclass the lightweight
SwiftTrace.Swizzle instead of Decorated and skip super. The trampoline
does the real call and only uses onEntry/onExit as observers, so emitting
begin/end is enough; Decorated's argument-reflection/logging path hung in
the iOS app context. The AppleTraceAutoExample smoke check still passes.

Docs: README / README_CN gain a samples table; AGENT.md and
docs/swift-tracing.md describe the demo and the `-destination`-only build
note (a stray `-sdk iphonesimulator` forces the macro plugin onto the
wrong SDK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ulator

Two issues surfaced running AppleTraceSwiftDemo on a real iPhone 17 Pro:

1. dyld crash at launch — "Library not loaded: @rpath/SwiftTrace.framework".
   The embedded dynamic framework wasn't found because the project lacked
   LD_RUNPATH_SEARCH_PATHS; added @executable_path/Frameworks. (The
   Simulator happened to fall back to the on-disk build-products path, so
   it only failed on device.)

2. SwiftTrace patches pointer-authenticated vtable slots, which is unsafe
   on real devices. Gated AppleTraceAuto.trace(...) behind
   `#if targetEnvironment(simulator)`; the @Traced / @traceall macros are
   the on-device path and have no such limitation.

Verified end-to-end:
- Simulator: ~487 events, BOTH routes (macros + SwiftTrace-hooked
  ImageLoader.load/readBytes/resize).
- Real device: ~463 events, macro route only (App Launch, Frame, Draw,
  warmCaches()/reload()/parse()/layout(), counters, async arcs across 5
  named threads); SwiftTrace route correctly absent.

Docs (README / README_CN / swift-tracing.md / AppleTraceAuto header) and
the demo UI now state that AppleTraceAuto is Simulator/macOS-only and the
macros are the on-device path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@everettjf everettjf merged commit ec22177 into master May 24, 2026
10 checks passed
@everettjf everettjf deleted the feature/swift-tracing branch May 24, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant