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 os(iOS) && canImport(UIKit)
3import UIKit
4
5// 🤖 Frame-hitch capture (iOS): the layer between "smooth" and a watchdog
6// hang. The first day of field data showed hangs people FEEL that never
7// trip the 250ms watchdog — scroll-storm frame drops in the 100–250ms band
8// (the same class HitchLogger was built to chase). A display link ticks
9// once per rendered frame; a gap ≥120ms is a properly dropped bunch of
10// frames and records as a "hitch" ledger event. Started/stopped with
11// recording (Perf.setRecording), so Release builds without consent never
12// run the link. Mac has no CADisplayLink — the watchdog covers it there
13// (deliberately iOS-only).
14final class HitchRecorder {
15 private var link: CADisplayLink?
16 private var last: CFTimeInterval = 0
17 private var observers: [NSObjectProtocol] = []
18
19 func start() {
20 guard link == nil else { return }
21 let l = CADisplayLink(target: self, selector: #selector(tick))
22 l.add(to: .main, forMode: .common)
23 link = l
24 // The link pauses in the background; the first tick after resume
25 // spans the whole suspension and would read as a monster hitch.
26 // Reset the baseline on every foreground transition instead.
27 let nc = NotificationCenter.default
28 observers.append(nc.addObserver(forName: UIApplication.willEnterForegroundNotification,
29 object: nil, queue: .main) { [weak self] _ in self?.last = 0 })
30 }
31
32 func stop() {
33 link?.invalidate()
34 link = nil
35 last = 0
36 observers.forEach { NotificationCenter.default.removeObserver($0) }
37 observers.removeAll()
38 }
39
40 @objc private func tick(_ l: CADisplayLink) {
41 defer { last = l.timestamp }
42 guard last > 0 else { return }
43 let gapMs = (l.timestamp - last) * 1000
44 // >5s can only be a suspension the notification didn't cover
45 // (scene detach, system UI) — never a real render gap.
46 guard gapMs >= 120, gapMs < 5000 else { return }
47 Perf.recordHitch(ms: gapMs)
48 }
49}
50#else
51final class HitchRecorder {
52 func start() {}
53 func stop() {}
54}
55#endif
56