Skip to content

MorozPavlo/MainThreadWatchdog

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MainThreadWatchdog

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.

How it works

  1. A background thread posts a tiny job to DispatchQueue.main every poll seconds.
  2. It measures how long that job takes to run. If longer than threshold, the main thread is currently blocked.
  3. 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).

Installation (Swift Package Manager)

Locally: Xcode → File → Add Package Dependencies… → Add Local… → pick this folder. Or in another Package.swift:

.package(path: "../MainThreadWatchdog")

Usage — a single modifier

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()

Verify

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.

Tests

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.

Limitations

  • 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.

License

MIT — see LICENSE.

About

Tiny iOS/macOS debug tool: detects main-thread hangs and shows the exact call stack that blocked the main thread — with a ready-to-paste prompt to fix it.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages