Налаштування App Hang / UI Freeze мониторингу для мобільних додатків
App Hang — це не крах. Додаток живий, але не реагує на дотики. Користувачі бачать зависаний екран, натискають кнопку ще раз, потім ще — і йдуть. У Crashlytics немає нічого, користувач мовчить, конверсія падає.
iOS Watchdog завершує процес при hang > 4–8 секунд. Android генерує ANR при > 5 секунд. Але зависання 200–500ms не викликають системних подій — вони просто убивають UX.
Джерела зависань
На iOS найчастіший джерело коротких фризів — синхронний виклик на main thread у реакції на UI-событие:
// Антипаттерн: декодуємо великий JSON на main thread
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductCell
// Якщо product.image — це Data, яку потрібно декодувати — це блокує main thread
cell.imageView?.image = UIImage(data: product.imageData)
return cell
}
UIImage(data:) синхронно декодує JPEG/PNG. На iPhone SE з 1900x1200 зображенням — 30–80ms блокування main thread при кожному cellForRowAt.
На Android основний вплинувач Compose-екранів — лишні recomposition у LazyColumn:
// Антипаттерн: нестабільний тип у LazyColumn
@Composable
fun ProductList(products: List<Product>) { // List<> не стабільний тип
LazyColumn {
items(products) { product ->
ProductCard(product) // перерисовується при будь-якій зміні батька
}
}
}
Заміна List<Product> на ImmutableList<Product> (kotlinx.collections.immutable) або використання @Stable-анотації на data class убирає лишні recomposition.
Інструменти для виявлення
iOS — MetricKit Hang Diagnostics
func didReceive(_ payloads: [MXDiagnosticPayload]) {
for payload in payloads {
payload.hangDiagnostics?.forEach { hang in
let duration = hang.hangDuration
let callStack = hang.callStackTree
// hang.hangDuration — MXAverage, не точне значення
// Мінімальний трекінг для MetricKit: hang > 250ms
print("Hang duration: \(duration.averageMeasurement)")
}
}
}
MetricKit віддає дані з суточною затримкою. Для real-time потрібне власне рішення.
iOS — Sentry App Hang Detection
SentrySDK.start { options in
options.dsn = "https://[email protected]/project"
options.enableAppHangTracking = true
options.appHangTimeoutInterval = 0.25 // 250ms — поріг для репорта
}
Sentry запускає watchdog-поток, який пингує main thread кожні 100ms. Якщо відповіді немає > appHangTimeoutInterval — знімає стек через backtrace_thread та відправляє як Issue.
Android — Jetpack Janky Frames
// FrameMetricsAggregator з AndroidX
val frameMetrics = FrameMetricsAggregator(FrameMetricsAggregator.JANK_DATA)
frameMetrics.add(activity)
// Пізніше:
val metrics = frameMetrics.metrics
metrics?.get(FrameMetricsAggregator.JANK_INDEX)?.let { jankArray ->
val jankyFrames = jankArray.size
// Кількість фреймів > 16ms
}
Android — Android Profiler та Perfetto
У продакшені ці інструменти не використовуються, але для локальної діагностики — незамінні. Android Profiler показує System Trace: де main thread зайнятий, які lock очікуються, де GC-паузи.
Perfetto з android.view.Choreographer треком показує dropped frames за екранами.
Налаштування мониторингу у Datadog
// Трекінг Long Tasks у Datadog RUM
RUM.enable(with: RUM.Configuration(
applicationID: "your-rum-app-id",
longTaskThreshold: 0.1 // 100ms — все, що довше, потрапляє як Long Task
))
У Datadog дашборді будуємо віджет:
count:rum.long_task{env:production,service:ios-app}
group_by: @view.name
visualize_as: top_list
Це покаже, на яких екранах більше всього Long Tasks — і саме там копаємо далі.
Що ми робимо
- Підключаємо Sentry
enableAppHangTrackingз порогом 250ms (iOS) - Налаштовуємо MetricKit subscriber для daily diagnostics
- Включаємо Datadog RUM Long Task tracking з порогом 100ms
- На Android налаштовуємо
FrameMetricsAggregatorдля ключових Activity - Будуємо дашборд по екранах з найбільшим числом Long Tasks
- Аналізуємо stack traces та виявляємо конкретних вплинувачів
Часові оцінки
Базова настройка через Sentry та Datadog: 4–8 годин. Повна діагностика з MetricKit та кастомними метриками за екранами: 1–2 дні. Ціна розраховується індивідуально.







