eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2import os
3
4// πŸ€– Perf β€” the shared performance-instrumentation core for both native apps.
5// Three layers, all local to the device:
6//
7// 1. os_signpost intervals around the seams that hurt (API calls, page
8// decrypt, thread-open, socket connects). Always on: signposts cost
9// nanoseconds, never leave the device, and make any build inspectable
10// in Instruments (os_signpost track, subsystem = bundle id).
11// 2. A slow-event ledger: a span that overruns its budget, a main-thread
12// hang (HangWatchdog), or a noteworthy event (socket reconnect) appends
13// one JSON line to Library/Logs/perf-events.jsonl β€” so a device
14// accumulates its own "little cuts" record in the field, exportable
15// from Settings as a single report.
16// 3. MetricKit daily payloads + hang diagnostics (MetricKitCollector),
17// filed beside the ledger.
18//
19// Recording (layers 2–3) is gated: Debug builds record by default; Release
20// builds only when the Settings consent toggle is on. Nothing recorded ever
21// includes message content, names, or keys β€” a span carries a fixed name, a
22// duration, and at most a URL path or a row count. The apps call install()
23// at launch; the extensions (NSE/share) never do, so for them this whole
24// file is inert signpost emission.
25public enum Perf {
26 /// The consent/preference key ("perfDiagnostics"): unset means the build
27 /// default below; the Settings toggle writes it on both platforms.
28 public static let consentKey = "perfDiagnostics"
29
30 /// Debug daily-drivers record without being asked; Release waits for the
31 /// consent toggle β€” the deal Paul set for shipping APM at all.
32 public static var defaultEnabled: Bool {
33 #if DEBUG
34 return true
35 #else
36 return false
37 #endif
38 }
39
40 public static var enabledPreference: Bool {
41 (UserDefaults.standard.object(forKey: consentKey) as? Bool) ?? defaultEnabled
42 }
43
44 // MARK: - Lifecycle (apps only)
45
46 private static let lock = NSLock()
47 private static var installed = false
48 private static var recording = false
49 private static var installedAt = DispatchTime.now()
50 private static var watchdog: HangWatchdog?
51 private static var hitches: HitchRecorder?
52 private static var metrics: MetricKitCollector?
53
54 static var isRecording: Bool {
55 lock.lock(); defer { lock.unlock() }
56 return recording
57 }
58
59 /// Call once from each app's launch hook (never from an extension).
60 public static func install() {
61 lock.lock()
62 let already = installed
63 installed = true
64 installedAt = .now()
65 lock.unlock()
66 guard !already else { return }
67 setRecording(enabledPreference)
68 }
69
70 /// The Settings toggle's runtime half (the toggle itself persists the
71 /// preference). Starts/stops the watchdog + MetricKit subscription and
72 /// gates ledger writes; signposts stay on either way. Main thread only
73 /// (install() and the Settings toggles) β€” watchdog/metrics are unlocked.
74 public static func setRecording(_ on: Bool) {
75 lock.lock()
76 recording = on
77 lock.unlock()
78 if on {
79 if watchdog == nil { watchdog = HangWatchdog() }
80 watchdog?.start()
81 if hitches == nil { hitches = HitchRecorder() }
82 hitches?.start()
83 if metrics == nil { metrics = MetricKitCollector() }
84 metrics?.start()
85 } else {
86 watchdog?.stop()
87 hitches?.stop()
88 metrics?.stop()
89 }
90 }
91
92 // MARK: - Signpost spans
93
94 private static let subsystem = Bundle.main.bundleIdentifier ?? "com.elevenmessenger.messenger"
95 private static let signposter = OSSignposter(subsystem: subsystem, category: "perf")
96 // Points-of-interest events (hangs, first frame) show up in Time
97 // Profiler's track without opening the os_signpost instrument.
98 private static let poi = OSSignposter(logHandle: OSLog(subsystem: subsystem, category: .pointsOfInterest))
99
100 /// An open interval. end() closes the signpost and, when the elapsed time
101 /// beats the span's budget, files a "slow" ledger event.
102 public struct Span {
103 fileprivate let name: StaticString
104 fileprivate let slowMs: Double
105 fileprivate let start: DispatchTime
106 fileprivate let state: OSSignpostIntervalState
107
108 /// `record: false` closes the signpost without a ledger event β€” for
109 /// spans that ended abnormally (a failed socket connect isn't a slow
110 /// one) where a duration would mislead.
111 public func end(_ detail: String? = nil, record: Bool = true) {
112 Perf.endSpan(self, detail: detail, record: record)
113 }
114 }
115
116 public static func begin(_ name: StaticString, slowMs: Double = 500) -> Span {
117 let id = signposter.makeSignpostID()
118 let state = signposter.beginInterval(name, id: id)
119 return Span(name: name, slowMs: slowMs, start: .now(), state: state)
120 }
121
122 private static func endSpan(_ span: Span, detail: String?, record: Bool) {
123 if let detail {
124 // Public, or trace exports show "<private>" (learned from the
125 // 2026-07-20 session). Details are paths/counts, never content,
126 // and signposts never leave the device.
127 signposter.endInterval(span.name, span.state, "\(detail, privacy: .public)")
128 } else {
129 signposter.endInterval(span.name, span.state)
130 }
131 guard record else { return }
132 let ms = Double(DispatchTime.now().uptimeNanoseconds &- span.start.uptimeNanoseconds) / 1e6
133 if ms >= span.slowMs {
134 self.record(kind: "slow", name: "\(span.name)", ms: ms, detail: detail)
135 }
136 }
137
138 // MARK: - Ledger events
139
140 // A same-name floor so a repeating condition (socket retrying every 3s on
141 // a dead network) can't flood the ledger: at most one event per name per
142 // 30s window. Slow spans skip the floor β€” each one is a distinct cut.
143 private static var eventFloor: [String: DispatchTime] = [:]
144
145 /// A noteworthy moment with no duration (e.g. "socket.reconnect").
146 public static func event(_ name: StaticString, detail: String? = nil) {
147 guard isRecording else { return }
148 let key = "\(name)"
149 lock.lock()
150 if let last = eventFloor[key],
151 DispatchTime.now().uptimeNanoseconds &- last.uptimeNanoseconds < 30_000_000_000 {
152 lock.unlock()
153 return
154 }
155 eventFloor[key] = .now()
156 lock.unlock()
157 poi.emitEvent(name)
158 record(kind: "note", name: key, ms: nil, detail: detail)
159 }
160
161 /// The watchdog's report: main was wedged for ~ms.
162 static func recordHang(ms: Double) {
163 poi.emitEvent("hang")
164 record(kind: "hang", name: "main.hang", ms: ms, detail: nil)
165 }
166
167 /// The hitch recorder's report: a rendered-frame gap of ~ms (iOS).
168 static func recordHitch(ms: Double) {
169 poi.emitEvent("hitch")
170 record(kind: "hitch", name: "frame.hitch", ms: ms, detail: nil)
171 }
172
173 private static var firstFrameNoted = false
174
175 /// Called by the app when its first real screen is up β€” the honest
176 /// "launch felt like this" number (since install(), not process start;
177 /// MetricKit's launch histograms cover the pre-main part).
178 public static func noteFirstFrame() {
179 lock.lock()
180 let already = firstFrameNoted
181 firstFrameNoted = true
182 let since = Double(DispatchTime.now().uptimeNanoseconds &- installedAt.uptimeNanoseconds) / 1e6
183 lock.unlock()
184 guard !already else { return }
185 poi.emitEvent("firstFrame")
186 record(kind: "boot", name: "boot.firstFrame", ms: since, detail: nil)
187 }
188
189 static func record(kind: String, name: String, ms: Double?, detail: String?) {
190 guard isRecording else { return }
191 let event = PerfEvent(t: Int64(Date().timeIntervalSince1970 * 1000),
192 kind: kind, name: name,
193 ms: ms.map { ($0 * 10).rounded() / 10 }, detail: detail)
194 PerfLedger.shared.append(event)
195 }
196
197 // MARK: - Files & export
198
199 static var logsDir: URL {
200 let dir = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]
201 .appendingPathComponent("Logs", isDirectory: true)
202 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
203 return dir
204 }
205
206 static var eventsURL: URL { logsDir.appendingPathComponent("perf-events.jsonl") }
207 static var diagnosticsDir: URL {
208 let dir = logsDir.appendingPathComponent("perf-diagnostics", isDirectory: true)
209 try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
210 return dir
211 }
212
213 /// Everything recorded so far, oldest first (current + rotated file).
214 public static func recordedEvents() -> [PerfEvent] {
215 let prev = (try? String(contentsOf: PerfLedger.rotatedURL, encoding: .utf8)) ?? ""
216 let cur = (try? String(contentsOf: eventsURL, encoding: .utf8)) ?? ""
217 return PerfEvent.parse(jsonl: prev + cur)
218 }
219
220 /// Assemble the shareable report (device/OS/app + the ledger + the most
221 /// recent MetricKit payloads, size-capped) and return its file URL.
222 /// Blocking file IO β€” call off the main actor.
223 public static func assembleExport() -> URL? {
224 var diags: [String] = []
225 if let names = try? FileManager.default.contentsOfDirectory(atPath: diagnosticsDir.path) {
226 // Filenames start with an ISO timestamp, so lexicographic = chronological.
227 for name in names.sorted().reversed() {
228 guard let data = try? Data(contentsOf: diagnosticsDir.appendingPathComponent(name)),
229 let json = String(data: data, encoding: .utf8) else { continue }
230 diags.append(json)
231 }
232 }
233 let info = Bundle.main.infoDictionary
234 let export = PerfExport(
235 app: Bundle.main.bundleIdentifier ?? "?",
236 version: "\(info?["CFBundleShortVersionString"] ?? "?") (\(info?["CFBundleVersion"] ?? "?"))",
237 os: ProcessInfo.processInfo.operatingSystemVersionString,
238 device: hardwareModel(),
239 generated: Int64(Date().timeIntervalSince1970 * 1000),
240 events: recordedEvents(),
241 metrickit: PerfExport.capDiagnostics(diags, maxBytes: 2_000_000))
242 guard let data = try? JSONEncoder().encode(export) else { return nil }
243 let url = logsDir.appendingPathComponent("performance-report.json")
244 guard (try? data.write(to: url, options: .atomic)) != nil else { return nil }
245 return url
246 }
247
248 /// Wipe the ledger + collected payloads (the Settings "clear" affordance).
249 public static func clearRecorded() {
250 PerfLedger.shared.clear()
251 let fm = FileManager.default
252 if let names = try? fm.contentsOfDirectory(atPath: diagnosticsDir.path) {
253 for name in names { try? fm.removeItem(at: diagnosticsDir.appendingPathComponent(name)) }
254 }
255 }
256
257 /// "hw.machine" (iPhone17,1 / Mac15,6) β€” a model id, not an identifier of
258 /// the person; it's what makes a hang report actionable across devices.
259 private static func hardwareModel() -> String {
260 var size = 0
261 sysctlbyname("hw.machine", nil, &size, nil, 0)
262 guard size > 0 else { return "?" }
263 var chars = [CChar](repeating: 0, count: size)
264 sysctlbyname("hw.machine", &chars, &size, nil, 0)
265 return String(cString: chars)
266 }
267}
268
269// MARK: - The on-disk ledger
270
271/// Serialized appends to perf-events.jsonl with ClickLab-style rotation: the
272/// file caps at ~512KB and rotates once to .prev β€” the interesting window is
273/// recent, and unbounded append only grows.
274final class PerfLedger {
275 static let shared = PerfLedger()
276 static var rotatedURL: URL { Perf.eventsURL.deletingPathExtension().appendingPathExtension("prev.jsonl") }
277
278 private let queue = DispatchQueue(label: "lchat.perf.ledger", qos: .utility)
279 private var handle: FileHandle?
280 private var bytes: Int = -1 // unknown until first append
281
282 func append(_ event: PerfEvent) {
283 queue.async { [self] in
284 guard var data = try? JSONEncoder().encode(event) else { return }
285 data.append(0x0A)
286 let url = Perf.eventsURL
287 if bytes < 0 {
288 let attrs = try? FileManager.default.attributesOfItem(atPath: url.path)
289 bytes = (attrs?[.size] as? Int) ?? 0
290 }
291 if bytes > 512 * 1024 {
292 try? handle?.close()
293 handle = nil
294 try? FileManager.default.removeItem(at: Self.rotatedURL)
295 try? FileManager.default.moveItem(at: url, to: Self.rotatedURL)
296 bytes = 0
297 }
298 if handle == nil {
299 if !FileManager.default.fileExists(atPath: url.path) {
300 FileManager.default.createFile(atPath: url.path, contents: nil)
301 }
302 handle = try? FileHandle(forWritingTo: url)
303 _ = try? handle?.seekToEnd()
304 }
305 try? handle?.write(contentsOf: data)
306 bytes += data.count
307 }
308 }
309
310 func clear() {
311 queue.async { [self] in
312 try? handle?.close()
313 handle = nil
314 bytes = 0
315 try? FileManager.default.removeItem(at: Perf.eventsURL)
316 try? FileManager.default.removeItem(at: Self.rotatedURL)
317 }
318 }
319}
320
321// MARK: - Event & export shapes (pure; pinned by PerfTests)
322
323/// One ledger line. Kinds: "slow" (a span beat its budget), "hang" (main
324/// thread wedged), "boot" (first frame), "note" (an event), "metric" /
325/// "diag" (MetricKit arrivals).
326public struct PerfEvent: Codable, Equatable, Sendable {
327 public var t: Int64 // epoch ms
328 public var kind: String
329 public var name: String
330 public var ms: Double?
331 public var detail: String?
332
333 public init(t: Int64, kind: String, name: String, ms: Double? = nil, detail: String? = nil) {
334 self.t = t; self.kind = kind; self.name = name; self.ms = ms; self.detail = detail
335 }
336
337 /// Lenient JSONL parse: a torn or garbage line (a crash mid-append) skips,
338 /// never sinks the export.
339 public static func parse(jsonl: String) -> [PerfEvent] {
340 jsonl.split(separator: "\n").compactMap {
341 try? JSONDecoder().decode(PerfEvent.self, from: Data($0.utf8))
342 }
343 }
344
345 /// The Settings footer's one-liner: counts, pluralised, worst slow span
346 /// last. Nil when there's nothing to say.
347 public static func summary(_ events: [PerfEvent]) -> String? {
348 let hangs = events.filter { $0.kind == "hang" }.count
349 let hitches = events.filter { $0.kind == "hitch" }.count
350 let slows = events.filter { $0.kind == "slow" }
351 guard hangs > 0 || hitches > 0 || !slows.isEmpty else { return nil }
352 var parts: [String] = []
353 if hangs > 0 {
354 parts.append("\(hangs.formatted()) \(hangs == 1 ? "hang" : "hangs")")
355 }
356 if hitches > 0 {
357 parts.append("\(hitches.formatted()) \(hitches == 1 ? "hitch" : "hitches")")
358 }
359 if !slows.isEmpty {
360 parts.append("\(slows.count.formatted()) slow \(slows.count == 1 ? "moment" : "moments")")
361 }
362 if let worst = slows.max(by: { ($0.ms ?? 0) < ($1.ms ?? 0) }), let ms = worst.ms {
363 parts.append("worst \(worst.name) \((ms / 1000).formatted(.number.precision(.fractionLength(1))))s")
364 }
365 return parts.joined(separator: " Β· ")
366 }
367}
368
369/// The shareable report β€” also the unit a future opt-in stream would POST to
370/// a webhook-ingest feed, so its shape is deliberately self-contained.
371public struct PerfExport: Codable {
372 public var app: String
373 public var version: String
374 public var os: String
375 public var device: String
376 public var generated: Int64
377 public var events: [PerfEvent]
378 public var metrickit: [String] // raw MetricKit payload JSONs, newest first
379
380 /// Keep the newest payloads under a byte budget β€” hang diagnostics carry
381 /// full call trees and a report must stay shareable, not gigantic.
382 public static func capDiagnostics(_ newestFirst: [String], maxBytes: Int) -> [String] {
383 var kept: [String] = []
384 var total = 0
385 for json in newestFirst {
386 let size = json.utf8.count
387 if total + size > maxBytes { break }
388 kept.append(json)
389 total += size
390 }
391 return kept
392 }
393}
394