eleven / messenger Public
π€ 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
- Signposts (always on, every build).
os_signpostintervals around the seams that hurt, viaPerf.begin("name", slowMs:)βspan.end(). Cost is nanoseconds; nothing is stored or sent. Attach Instruments to any build (subsystem = the bundle id, categoryperf) and the seams below appear as named intervals; hangs and first-frame land in the Points of Interest track.
- 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 toLibrary/Logs/perf-events.jsonlin the app's container. ~512KB cap with one.prevrotation. Events carry a fixed name, a duration, and at most a URL path or row count β never content, names, or keys.
- MetricKit (consent-gated).
MetricKitCollectorsubscribes 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 underLibrary/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 ashitches.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.