oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3/// Measures how long the main thread goes without answering.
4///
5/// A thread of its own posts a block to the main queue every 5 ms and
6/// times how long each takes to run. A main thread that is free answers
7/// within a millisecond or two; one that is blocked answers only when it
8/// is free again, and the wait is the stall. Several posts can be waiting
9/// at once, so the longest single wait is the longest stall, to within
10/// the 5 ms period.
11///
12/// It measures the main *queue*, which is what the main actor runs on and
13/// what a SwiftUI app's run loop drains: a beachball is this number above
14/// about a second.
15final class MainThreadWatchdog: @unchecked Sendable {
16 struct Stall: Sendable {
17 /// Seconds since the watchdog started.
18 var at: Double
19 var seconds: Double
20 }
21
22 private let lock = NSLock()
23 private var running = false
24 private var longest: Stall?
25 private var stalls: [Stall] = []
26 private let started = CFAbsoluteTimeGetCurrent()
27 private let period: Double
28 /// Waits at or above this are kept, for the report.
29 private let noteAbove: Double
30
31 init(period: Double = 0.005, noteAbove: Double = 0.05) {
32 self.period = period
33 self.noteAbove = noteAbove
34 }
35
36 func start() {
37 lock.lock()
38 running = true
39 lock.unlock()
40 let thread = Thread { [self] in
41 while isRunning {
42 let posted = CFAbsoluteTimeGetCurrent()
43 DispatchQueue.main.async { [self] in
44 let now = CFAbsoluteTimeGetCurrent()
45 record(Stall(at: posted - started, seconds: now - posted))
46 }
47 Thread.sleep(forTimeInterval: period)
48 }
49 }
50 thread.qualityOfService = .userInteractive
51 thread.start()
52 }
53
54 private var isRunning: Bool {
55 lock.lock()
56 defer { lock.unlock() }
57 return running
58 }
59
60 private func record(_ stall: Stall) {
61 lock.lock()
62 defer { lock.unlock() }
63 guard running else { return }
64 if stall.seconds > (longest?.seconds ?? -1) { longest = stall }
65 if stall.seconds >= noteAbove { stalls.append(stall) }
66 }
67
68 /// Forgets what was measured; the next `longest` is from now.
69 func reset() {
70 lock.lock()
71 longest = nil
72 stalls = []
73 lock.unlock()
74 }
75
76 /// Stops posting and gives the longest wait since the last reset, and
77 /// every wait above `noteAbove`.
78 func stop() -> (longest: Double, stalls: [Stall]) {
79 lock.lock()
80 defer { lock.unlock() }
81 running = false
82 return (longest?.seconds ?? 0, stalls)
83 }
84
85 /// The longest wait so far, without stopping.
86 var longestSoFar: Double {
87 lock.lock()
88 defer { lock.unlock() }
89 return longest?.seconds ?? 0
90 }
91}
92