| 1 | import Foundation |
| 2 | |
| 3 | // 🤖 The main-thread hang watchdog — ClickLab's stall ticker (LChatMac), |
| 4 | // generalised so both apps report hangs into the perf ledger. A background |
| 5 | // timer pings the main queue every 200ms; any ping serviced late means main |
| 6 | // was wedged, and the lag ≈ how long. Cheap enough to run for a whole |
| 7 | // session: one async block per 200ms, no stack sampling (MetricKit's hang |
| 8 | // diagnostics carry the call trees; this catches the sub-hang stutters |
| 9 | // that MetricKit's threshold ignores but fingers feel). The 250ms floor is |
| 10 | // Apple's own responsiveness bar — the first field day showed felt hangs |
| 11 | // living below the original 400ms floor, invisible; HitchRecorder covers |
| 12 | // the band below this. |
| 13 | final class HangWatchdog { |
| 14 | private var ticker: DispatchSourceTimer? |
| 15 | // A long hang queues several pings that all drain in one burst; the first |
| 16 | // serviced carries the full lag, the rest shrink toward zero. Report the |
| 17 | // first, swallow the burst. Main-thread state (pings land on main). |
| 18 | private var suppressUntil = DispatchTime.now() |
| 19 | |
| 20 | func start() { |
| 21 | guard ticker == nil else { return } |
| 22 | let t = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "lchat.perf.watchdog")) |
| 23 | t.schedule(deadline: .now() + 1, repeating: .milliseconds(200)) |
| 24 | t.setEventHandler { |
| 25 | let sent = DispatchTime.now() |
| 26 | DispatchQueue.main.async { [weak self] in |
| 27 | guard let self else { return } |
| 28 | let now = DispatchTime.now() |
| 29 | let lagMs = Double(now.uptimeNanoseconds &- sent.uptimeNanoseconds) / 1e6 |
| 30 | guard lagMs > 250, now > self.suppressUntil else { return } |
| 31 | self.suppressUntil = now + .seconds(1) |
| 32 | Perf.recordHang(ms: lagMs) |
| 33 | } |
| 34 | } |
| 35 | t.resume() |
| 36 | ticker = t |
| 37 | } |
| 38 | |
| 39 | func stop() { |
| 40 | ticker?.cancel() |
| 41 | ticker = nil |
| 42 | } |
| 43 | } |
| 44 | |