A tiny main-thread hang detector for iOS/macOS. It catches freezes and shows the
main-thread stack at the moment of the block — i.e. the exact function that held main.
- A background thread posts a tiny job to
DispatchQueue.maineverypollseconds. - It measures how long that job takes to run. If longer than
threshold, the main thread is currently blocked. - While it is still stuck in the guilty code, its stack is captured via Mach
thread_suspend+ frame-pointer walking, symbolicated (with Swift demangling), and delivered as a report.
Runs only in #if DEBUG. Stack capture is a tiny C target (CMachBacktrace).
Locally: Xcode → File → Add Package Dependencies… → Add Local… → pick this folder.
Or in another Package.swift:
.package(path: "../MainThreadWatchdog")import SwiftUI
import MainThreadWatchdog
@main
struct MainActorDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.thresholdChecker(.medium) // .soft (0.5s) / .medium (0.25s) / .hard (0.1s)
}
}
}On a hang, the console prints a human-readable report and a ready-made prompt you can paste into Claude to get a fix:
⚠️ [Watchdog] Main thread hung for 412 ms — what held it:
→ ContentViewModel.loadWrongBlocking() async
→ MainActorDemoApp.$main()
🤖 Paste into Claude to get a fix ↓↓↓
My iOS/SwiftUI app is hitching: the main thread was blocked for 412 ms ...
🤖 ↑↑↑ end of prompt
Custom handler (e.g. log to a file/analytics). It is called on a background thread — if you touch UI, hop to main yourself:
ContentView()
.thresholdChecker(.hard) { report in
MyLogger.log(report.formatted)
// report.prompt — the Claude-ready text
}Manual start without SwiftUI:
MainThreadWatchdog.shared.start(threshold: 0.25) // on the main thread
MainThreadWatchdog.shared.stop()swift run WatchdogDemo
The demo blocks the main thread inside heavyBlockingWork(), and a background sample
captures its stack — the output shows heavyBlockingWork as the culprit.
swift test
If swift test fails at the codesign step (common when the package lives on the Desktop,
which carries Finder/iCloud extended attributes), run tests in Xcode (⌘U) or move the
package somewhere like ~/Developer.
- Apple platforms only (arm64 / x86_64): uses the Mach thread API.
- The stack shows full system frames (
libswiftCore, etc.) above your function by default;appOnly(on by default) hides them. - Not for production releases on user devices (this is a debug tool); for production hang reports use MetricKit/Instruments.
MIT — see LICENSE.