Налаштування Watchdog Termination мониторингу для iOS-додатків
Watchdog — системний механізм iOS, який примусово завершує додаток, якщо він перестає відповідати на события занадто довго. На відміну від звичайного крахе, це не exception та не сигнал. MetricKit називає таке завершення MXHangDiagnostic, а ProcessInfo.processInfo.reason не містить трейс — тільки факт завершення.
Починаючи з iOS 13, Watchdog активується при зависанні main thread > 8 секунд у foreground. На iOS 16+ поріг знизили до ~4 секунд у деяких сценаріях. Користувачі просто бачать, як додаток закривається без діалогів.
Чому це складно мониторити
Firebase Crashlytics не реєструє Watchdog Terminations — вони не проходять через NSSetUncaughtExceptionHandler та не генерують сигнал. Crashlytics бачить тільки стандартні крахи.
Sentry з версії 8.0 може детектувати Watchdog через SentryWatchdogTerminationTracker, але тільки якщо SDK жив у пам'яті до моменту завершення — при холодному старті після Watchdog Sentry бачить його через флаги у UserDefaults.
Apple's MetricKit надає точні дані, але з затримкою: діагностичні payload приходять раз на добу.
Детектування через MetricKit
import MetricKit
class MetricsManager: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
// Метрики продуктивності
}
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
if let hangs = payload.hangDiagnostics, !hangs.isEmpty {
hangs.forEach { hang in
// hang.callStackTree — дерево вызовів у момент зависання
reportHangToServer(hang)
}
}
}
}
}
// Реєстрація
MXMetricManager.shared.add(MetricsManager())
MXHangDiagnostic.callStackTree містить символізований (якщо є dSYM) стек main thread у момент зависання. Це єдиний спосіб дізнатися, в якому саме методі повис потік.
Власний Watchdog-детектор
Якщо чекати MetricKit нельзя — робимо детектор вручну:
final class WatchdogDetector {
private let queue = DispatchQueue(label: "watchdog.monitor", qos: .utility)
private var pingTime: Date = Date()
private let threshold: TimeInterval = 3.0
func start() {
scheduleMainThreadPing()
scheduleBackgroundCheck()
}
private func scheduleMainThreadPing() {
DispatchQueue.main.async { [weak self] in
self?.pingTime = Date()
self?.scheduleMainThreadPing() // плануємо наступний ping
}
}
private func scheduleBackgroundCheck() {
queue.asyncAfter(deadline: .now() + 1.0) { [weak self] in
guard let self = self else { return }
let elapsed = Date().timeIntervalSince(self.pingTime)
if elapsed > self.threshold {
// Зафіксувати зависання main thread
self.captureHang(duration: elapsed)
}
self.scheduleBackgroundCheck()
}
}
private func captureHang(duration: TimeInterval) {
// Thread.callStackSymbols — тільки поточного потока
// Для main thread: використовуйте BSBacktraceLogger або PLCrashReporter
SentrySDK.capture(error: NSError(
domain: "WatchdogHang",
code: Int(duration * 1000),
userInfo: [NSLocalizedDescriptionKey: "Main thread hung for \(duration)s"]
))
}
}
Обережно: Thread.callStackSymbols захоплює стек поточного потока (background), не main. Для снятття стека з іншого потока потрібний BSBacktraceLogger (стороння бібліотека) або PLCrashReporter.
Sentry Watchdog Tracking
SentrySDK.start { options in
options.dsn = "https://[email protected]/project"
options.enableWatchdogTerminationTracking = true
options.appHangTimeoutInterval = 3.0 // поріг App Hang у секундах
}
Sentry зберігає флаг sentryWatchdogTermination у UserDefaults при кожному старті. Якщо при наступному запуску флаг установлений, а останнє завершення не було штатним — фіксується Watchdog Termination. Це не абсолютно точно (false positive при force-kill через Xcode), але у продакшені працює.
Типові джерела Watchdog Terminations
- Синхронний CoreData fetch у
viewDidLoadна main thread -
DispatchSemaphore.wait()абоDispatchGroup.wait()без timeout на main thread - Deadlock між
@MainActorта синхронним Swift Concurrency кодом - Важкий JSON decode в
URLSession.dataTaskcompletion без dispatch на background queue
Що ми робимо
- Включаємо
MetricKitsubscriber для отриманняMXHangDiagnostic - Підключаємо Sentry
enableWatchdogTerminationTrackingдля real-time детектування - При необхідності реалізуємо кастомний Watchdog-детектор для нижчого порога
- Налаштовуємо алерти на ріст Watchdog Termination Rate
- Аналізуємо
callStackTreeдля виявлення конкретних вплинувачів зависання
Часові оцінки
Базова настройка через Sentry: 4–8 годин. Інтеграція MetricKit з відправкою діагностик: 1–2 дні. Ціна розраховується індивідуально.







