Commit 4b0aef9
feat: fatal main-thread hang detection (#56)
* feat: add fatal main-thread hang detection
Add opt-in detection and reporting of fatal main-thread hangs on iOS,
tvOS, and macOS. A new BugSplatHangTracker class runs a low-QoS
watchdog thread that dispatches a "ping" block to the main queue every
threshold/5 seconds (clamped to at least 100ms). The block, when
serviced, resets an atomic counter; if the counter accumulates enough
unanswered pings to cover `hangDetectionThreshold` seconds, the main
thread is considered hung. When the main thread later services a ping,
a recovery callback fires so the persisted report can be discarded -
only fatal hangs survive to the next launch.
BugSplat persists the report as a synthetic PLCrashReport-style live
report (marking the main thread as crashed via its captured Mach port)
in the crashes directory with bugsplat-hang-* attributes and a per-
launch UUID for correlation with any crash from the same launch. Reuses
the existing next-launch scanner and upload pipeline.
Public API on BugSplat:
* `enableHangDetection` (BOOL, default NO) - opt in before -start
* `hangDetectionThreshold` (NSTimeInterval, default 2.0) - tune the
unresponsive duration that counts as a hang; clamped to >= 0.1s
Detection is suppressed when a debugger is attached or the app is
inactive (iOS/tvOS background). No-op inside app extensions. -start
must be invoked on the main thread when hang detection is enabled so
the main thread's Mach port can be captured.
Tests cover the watchdog state machine (detection, throttle, recovery,
suspension guard) deterministically via injectable clock and dispatch
blocks, and the BugSplat-side persistence (file layout, metadata,
recovery cleanup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(samples): demonstrate hang detection in example apps
Wire enableHangDetection into each example app and add a "Simulate Hang"
button that blocks the main thread forever (sleep loop with observable
side effects so the C++ forward-progress rule can't elide it). On the
next launch, a fatal-hang report uploads automatically.
Apps updated: SwiftUI, UIKit-Swift, UIKit-ObjC, macOS-UIKit-ObjC, and
the macOS C++ command-line tool. The CLI tool's loop alternates between
mainObjCRunLoop pumping and getline; the recovery callback discards any
hang report queued during idle prompt waits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(hang-detector): address Copilot review feedback
* Gate ping dispatch on an outstanding-ping flag so a long hang doesn't
accumulate an unbounded backlog of ping blocks on the main queue
(they would all flush at once when main resumed).
* Use millisecond precision for hang report filenames so a same-second
hang -> recover -> hang cycle can't overwrite a previous report.
* Drop unused imports (pthread, mach, UIKit) left over from the
CFRunLoopObserver design and add an explicit <math.h> for ceil().
* Sample apps' "Simulate Hang" buttons sleep inside their loops
instead of busy-spinning at 100% CPU - the main thread is still
blocked, which is what the demo needs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent bdc863b commit 4b0aef9
18 files changed
Lines changed: 1583 additions & 133 deletions
File tree
- BugSplat.xcodeproj
- Example_Apps
- BugSplatTest-SwiftUI/BugSplatTest-SwiftUI
- BugSplatTest-UIKit-ObjC/BugSplatTest-UIKit-ObjC
- BugSplatTest-UIKit-Swift/BugSplatTest-UIKit-Swift
- BugSplatTest-macOS-Tool-CPlusPlus/BugSplatTest-macOS-Tool-CPlusPlus
- BugSplatTest-macOS-UIKit-ObjC/BugSplatTest-macOS-UIKit-ObjC
- Tests/BugSplatTests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | | - | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
25 | 26 | | |
| 27 | + | |
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
| |||
83 | 85 | | |
84 | 86 | | |
85 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
86 | 104 | | |
87 | 105 | | |
88 | 106 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
168 | 211 | | |
169 | 212 | | |
170 | 213 | | |
| |||
0 commit comments