Настройка мониторинга 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 жил в памяти до момента завершения — cold start после Watchdog SDK видит через флаги в 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() // планируем следующий пинг
}
}
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на главном потоке -
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 дня. Стоимость рассчитывается индивидуально.







