# 🤖 Native performance instrumentation (iOS + Mac)

The answer to "the app has lots of little cuts": both native apps carry a
small, always-there instrumentation layer (`ios/LChatCore/Sources/LChatCore/
Perf.swift`) so a slow moment on any device leaves a record — without a
third-party APM SDK, and without anything leaving the device. Streaming
member telemetry to Datadog/Sentry/Firebase would sit badly with the E2EE
posture (and add the kind of dependency the second rule exists to avoid), so
the stack is Apple-native + self-owned end to end.

## The three layers

1. **Signposts (always on, every build).** `os_signpost` intervals around
   the seams that hurt, via `Perf.begin("name", slowMs:)` → `span.end()`.
   Cost is nanoseconds; nothing is stored or sent. Attach Instruments to any
   build (subsystem = the bundle id, category `perf`) and the seams below
   appear as named intervals; hangs and first-frame land in the
   Points of Interest track.

2. **The slow-event ledger (consent-gated).** A span that beats its budget,
   a main-thread hang (`HangWatchdog` — ClickLab's 200ms ping generalised
   into LChatCore for both apps), or a noteworthy event (socket reconnect)
   appends one JSON line to `Library/Logs/perf-events.jsonl` in the app's
   container. ~512KB cap with one `.prev` rotation. Events carry a fixed
   name, a duration, and at most a URL path or row count — never content,
   names, or keys.

3. **MetricKit (consent-gated).** `MetricKitCollector` subscribes to the
   OS's own APM: daily payloads (launch-time histograms, hang totals,
   memory) and hang diagnostics **with call trees** — real-device stack
   traces for hangs nobody was profiling. Raw payloads are filed under
   `Library/Logs/perf-diagnostics/` (newest 24 kept) with a compact ledger
   line each. The Mac delivers a subset (no hang diagnostics) — that's why
   the watchdog exists on both platforms.

## The gate

- **Debug builds record by default** (opt out: the Settings toggle, or
  `defaults write <bundle id> perfDiagnostics -bool NO`).
- **Release builds record only with the consent toggle** — iOS: the server
  settings cog → Performance; Mac: Settings → Advanced → Performance
  (Paul's call, 2026-07-20). Signposts stay on either way.
- The extensions (NSE, share) never call `Perf.install()`, so for them the
  layer is inert.

## Instrumented seams

| Span | Where | Budget |
|---|---|---|
| `boot.firstFrame` | launch hook → RootView first appear (iOS) | recorded always |
| `threads.hydrate` / `threads.refresh` | ThreadStore | 500ms / 2500ms |
| `feed.openThread` | ChatFeedModel.loadHistory (open a chat → rendered) | 1500ms |
| `feed.foldPage` | ChatFeedModel.foldPageDetached (200-row decrypt+fold) | 250ms |
| `api.get` / `api.post` | LChatAPI choke points (detail = path, never query/token) | 1000ms |
| `socket.connect` | ChatSocket connect → handshake (failed attempts unrecorded) | 3000ms |
| `image.load` | BlobImageLoader fetch+decrypt+decode | 1000ms |
| `main.hang` | HangWatchdog (>250ms main-thread wedge — Apple's responsiveness bar) | — |
| `frame.hitch` | HitchRecorder (iOS only: rendered-frame gap ≥120ms; the felt-jank band below the watchdog — the Mac has no CADisplayLink and keeps just the watchdog) | — |
| `socket.reconnect` | note event, 30s same-name floor | — |

Adding one is two lines (`Perf.begin`/`end`) — budgets are per-call-site, so
pick the number at which you'd actually want a report.

## Getting the data off a device

- **Settings → Performance → Prepare performance report** (iOS shares the
  file; the Mac reveals it in Finder): one self-contained
  `performance-report.json` — device model, OS, app version, the ledger,
  and the newest MetricKit payloads (2MB cap).
- Dev pull without the UI:
  `xcrun devicectl device copy from --device <id> --source Library/Logs/perf-events.jsonl --domain-type appDataContainer --domain-identifier com.elevenmessenger.messenger` (same pattern as `hitches.log`).
- Also free, zero code: **Xcode Organizer** (Hangs / Launch / Terminations
  from TestFlight + opted-in App Store users) and the App Store Connect
  perf-power-metrics + diagnostic-signature APIs (the existing ASC key can
  drive a periodic pull).

## What this is not (yet)

The report is the streaming unit by design (`PerfExport` is
self-contained), but nothing streams anywhere today. The intended follow-up
is an **opt-in** upload of that JSON to a webhook-ingest feed
(`docs/webhooks.md`) so reports land as encrypted messages in a team
thread — an Aikido-style stream we own, with the server still seeing only
ciphertext. Third-party APM stays off the table.

Related: `HitchLogger.swift` (iOS, DEBUG-only frame-drop breadcrumbs —
still marked temporary) and `ClickLab.swift` (Mac click-eating rig) predate
this layer and stay as-is; fold them in only when their bugs close.
