eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2#if canImport(MetricKit)
3import MetricKit
4#endif
5
6// 🤖 MetricKit intake: the OS's own APM. Payloads arrive about once a day
7// (metrics: launch-time histograms, hang totals, memory peaks; diagnostics:
8// hang call trees — the gold for chasing "little cuts" on devices nobody is
9// profiling). Each arrival is filed verbatim as JSON under
10// Library/Logs/perf-diagnostics/ plus one compact ledger line, so the
11// Settings report carries both the summary and the raw material. Started and
12// stopped by Perf.setRecording — no consent, no collection.
13#if canImport(MetricKit)
14final class MetricKitCollector: NSObject, MXMetricManagerSubscriber {
15 private var started = false
16
17 func start() {
18 guard !started else { return }
19 started = true
20 MXMetricManager.shared.add(self)
21 }
22
23 func stop() {
24 guard started else { return }
25 started = false
26 MXMetricManager.shared.remove(self)
27 }
28
29 func didReceive(_ payloads: [MXMetricPayload]) {
30 for p in payloads {
31 file(json: p.jsonRepresentation(), prefix: "metrics")
32 Perf.record(kind: "metric", name: "metrickit.daily", ms: nil, detail: summarize(p))
33 }
34 }
35
36 func didReceive(_ payloads: [MXDiagnosticPayload]) {
37 for p in payloads {
38 file(json: p.jsonRepresentation(), prefix: "diagnostics")
39 let hangs = p.hangDiagnostics ?? []
40 guard !hangs.isEmpty else { continue }
41 let total = hangs.reduce(0.0) {
42 $0 + $1.hangDuration.converted(to: .milliseconds).value
43 }
44 Perf.record(kind: "diag", name: "metrickit.hangs", ms: total,
45 detail: "\(hangs.count) \(hangs.count == 1 ? "hang" : "hangs") with call trees")
46 }
47 }
48
49 /// "launch avg 812ms · hang 3.2s/day" — enough for the ledger to show
50 /// trend; the filed payload holds the full histograms.
51 private func summarize(_ p: MXMetricPayload) -> String {
52 var parts: [String] = []
53 if let avg = Self.histogramAverageMs(p.applicationLaunchMetrics?.histogrammedTimeToFirstDraw) {
54 parts.append("launch avg \(Int(avg))ms")
55 }
56 if let hang = Self.histogramTotalMs(p.applicationResponsivenessMetrics?.histogrammedApplicationHangTime) {
57 parts.append("hang total \((hang / 1000).formatted(.number.precision(.fractionLength(1))))s")
58 }
59 return parts.isEmpty ? "payload filed" : parts.joined(separator: " · ")
60 }
61
62 // MXHistogram walks: buckets carry (start, end, count); average uses the
63 // bucket midpoint, total sums midpoint×count — the standard reduction.
64 private static func histogramAverageMs(_ h: MXHistogram<UnitDuration>?) -> Double? {
65 var weighted = 0.0, count = 0.0
66 forEachBucket(h) { midMs, n in
67 weighted += midMs * n
68 count += n
69 }
70 return count > 0 ? weighted / count : nil
71 }
72
73 private static func histogramTotalMs(_ h: MXHistogram<UnitDuration>?) -> Double? {
74 var total = 0.0, any = false
75 forEachBucket(h) { midMs, n in
76 total += midMs * n
77 any = true
78 }
79 return any ? total : nil
80 }
81
82 private static func forEachBucket(_ h: MXHistogram<UnitDuration>?,
83 _ body: (_ midMs: Double, _ count: Double) -> Void) {
84 guard let h else { return }
85 let e = h.bucketEnumerator
86 while let b = e.nextObject() as? MXHistogramBucket<UnitDuration> {
87 let mid = (b.bucketStart.converted(to: .milliseconds).value
88 + b.bucketEnd.converted(to: .milliseconds).value) / 2
89 body(mid, Double(b.bucketCount))
90 }
91 }
92
93 /// One file per arrival, ISO-stamped so a directory sort is
94 /// chronological; keep the newest 24 (a few weeks of dailies). The stamp
95 /// alone isn't unique — a batch (first subscribe delivers the accumulated
96 /// backlog in one call) lands within a second — so a random suffix keeps
97 /// same-second payloads from overwriting each other.
98 private func file(json: Data, prefix: String) {
99 guard Perf.isRecording else { return }
100 let stamp = ISO8601DateFormatter().string(from: Date())
101 let unique = UUID().uuidString.prefix(8)
102 let url = Perf.diagnosticsDir.appendingPathComponent("\(stamp)-\(prefix)-\(unique).json")
103 try? json.write(to: url, options: .atomic)
104 let fm = FileManager.default
105 guard let names = try? fm.contentsOfDirectory(atPath: Perf.diagnosticsDir.path),
106 names.count > 24 else { return }
107 for name in names.sorted().dropLast(24) {
108 try? fm.removeItem(at: Perf.diagnosticsDir.appendingPathComponent(name))
109 }
110 }
111}
112#else
113final class MetricKitCollector: NSObject {
114 func start() {}
115 func stop() {}
116}
117#endif
118