Всем привет! Меня зовут Алексей, я техлид Android-направления в компании Домклик.
Этой статьёй я открываю цикл материалов, в которых будут собраны мысли, советы и инструкции о том, как сделать приложение и процессы его разработки измеримыми, как находить «узкие места», диагностировать проблемы и причины их возникновения, а также фиксировать их наличие. Минимум кода, сложных терминов и «rocket science» — только практический опыт и наработки по различным метрикам.
Эта тема далеко не новая, и многие моменты могут показаться очевидными. Однако, чтобы разобраться в нюансах, необходимо начать с базы. Вовсе не обязательно делать именно так. Буду рад узнать, как вы работаете с теми или иными метриками.

Введение
Предположим, у нас есть приложение, которым пользуются люди, есть компания с некоторыми процессами выпуска новых версий приложения. Вроде бы всё хорошо. Но так ли это? Как понять работоспособность приложения? Как оценить влияние изменений на продукт? Во сколько обходится разработка и поддержка? Для ответа на эти вопросы мы должны обладать системой «наблюдения» (Observability), включающей сбор данных (журналов), метрик (числовых показателей чего-либо), трассировок (цепочек событий) и т.д., их хранение, агрегацию в каком-то упрощённом для понимания виде, мониторинг, оповещения и прочее.
Значимость таких систем не нуждается в дополнительной аргументации. А с внедрением ИИ в процессы разработки и тестирования тема метрик и внешнего контроля за происходящим приобрела дополнительный импульс. Далее мы разберём некоторые примеры (в произвольном порядке), наблюдение за которыми представляется наиболее важным или интересным. Для простоты будем и дальше называть их «метриками».
Crash-free
Начнём с самой известной, относительно понятной и отслеживаемой метрики — Crash-free. Это отношение уникальных пользователей, у которых не было крашей (аварийных завершений приложения), к общему числу активных пользователей за определённый период. Так как после краша приложение зачастую завершается, Crash-free часто рассматривают как метрику «доступности» приложения. Другими словами, если показатель держится на уровне 99,99%, то можно считать, что всё хорошо. Это не совсем так. Тем не менее отслеживать Crash-free крайне необходимо.
Со сбором и измерением Crash-free проблем обычно нет (почти наверняка вы уже используете Firebase, AppMetrica, Sentry и т.п.). Самое интересное начинается при выборе правильных SLO/SLA, то есть уровня доступности или надёжности приложения, отражающего ваши потребности, вклад в поддержание, а также требования бизнеса и нормативные составляющие.
Например, для приложения, совершающего финансовые транзакции, может быть необходим уровень 99,99% и выше, поскольку для пользователя и компании (в том числе её репутации) важно не потерять деньги при сбое. А для витрины небольшого магазина добиваться уровня более 99,5%, возможно, будет экономически нецелесообразным, поскольку затраты на исправление очень редких крашей могут превышать потенциальную выгоду от удержания небольшого процента пользователей, которые просто не смогли пролистать ленту с товаром. Таким образом, Crash-free 99,9% считается хорошим показателем, но для многих критически важных сервисов или приложений с большой аудиторией ориентиром остаются четыре девятки.
Подробнее об SLO/SLA можно почитать в книгах от Google про SRE, где можно вдохновиться множеством хороших практик по построению надёжных систем. Правда, речь там не о мобильных приложениях, но большинство концепций заслуженно являются стандартами индустрии.
При выборе SLO следует обратить внимание на то, что Crash-free очень зависим от рассматриваемого периода. Даже для одной и той же версии приложения показатели за сутки и за месяц, а также в разные периоды суток, могут различаться. Это объясняется тем, что в расчёте учитываются уникальные пользователи. В то время как количество сбоев просто суммируется, пользователи приходят и уходят, некоторые могут столкнуться с несколькими сбоями и прочими комбинациями, что влияет на итоговые показатели.

Определившись с SLO, важно сразу продумать систему мониторинга, обнаружения и реагирования, которая будет обеспечивать удержание этого уровня. Недостаточно просто подключить Firebase: необходимо настроить Velocity-оповещения (мгновенные уведомления в мессенджерах или на телефон о резком падении метрик), а также организовать разбор крашей (как срочных, так и накопившихся) и анализировать причины возникновения того или иного нарушения SLO. Здесь стоит рассматривать комплекс возможностей сервисов сбора данных и внутренних процессов в компании.
Многие компании, включая нас, используют собственные сервисы мониторинга и даже сбора данных. Это связано с рядом факторов: необходимостью оповещений через корпоративные каналы связи, интеграцией данных во внутренние дашборды, связью с другими метриками, а также интеграцией во внутренние процессы разбора инцидентов и т.д.
Всю систему работы с Crash-free можно представить в виде ключевых блоков:
Сервис сбора данных
Сервис агрегации
Сервис мониторинга
Бюджеты
Инциденты
Сервис сбора данных — это, как правило, фреймворк, интегрированный в приложение. Его задача — зафиксировать факт сбоя, предшествующие события, трассировку стека, скриншот экрана и другие данные, а затем отправить их в сыром виде в облачное хранилище. Очевидным лидером рынка здесь является Firebase. Однако его популярность в России сильно упала в связи с блокировками в некоторых регионах. Кроме того, его сложно интегрировать в корпоративную среду, поэтому многие крупные компании отдают предпочтение собственным разработкам или Self-hosted решениям.
Сервис агрегации выступает дополнительной прослойкой между «сырыми» данными, среди которых вряд ли кто-то в состоянии адекватно лавировать. Краши следует группировать по смыслу, платформам, устройствам, времени, регионам и др. При получении ошибки, скорее всего, вы захотите автоматически создать тикет/issue в трекере задач и назначить ответственного, приложив максимально полезную для решения проблемы информацию. Другими словами, сервис превращает некоторое событие с телефона пользователя в измеримую сущность в плоскости компании. Популярные фреймворки лишь частично закрывают эти потребности, однако у многих есть возможность получать необходимые данные по API. Таким образом, сервис «ходит» по «сырым» данным, обновляя и создавая тикеты, передаёт информацию в сервис мониторинга.
Сервис мониторинга предоставляет всевозможные дашборды для отслеживания динамики показателей. Это могут быть суточные и месячные отчёты, аналитика по каждому релизу и прочие графики для контроля ситуации релизными инженерами, лидами и разработчиками. Помимо этого, сервис пушит оповещения или уведомления в чаты и каналы по нарушению SLO или просто информационные — с текущей статистикой. В качестве базового решения может подойти Grafana, хотя консоли Firebase, AppMetrica и т.п. также могут быть эффективны (с учётом ограничений, упомянутых ранее). Этот сервис понадобится нам и для других метрик.
Бюджеты — это некоторая квота ошибок, которую вы можете себе позволить в рамках заданного SLO. Например, если SLO = 99,9%, то допустимый уровень крашей составляет 0,1%. Имея данные о количестве затронутых пользователей и частоте крашей, можно рассчитать их влияние на общий показатель. Поскольку краш уже преобразован в тикет и закреплён за ответственным, мы можем установить некий лимит для разработчиков или команд, что позволяет им продолжать заниматься фичами, а не исправлением крашей, пока бюджет не будет исчерпан. При приближении к критической отметке система автоматически уведомляет ответственных. Такой подход позволяет гибко планировать время, не тратя ресурсы на действительно редкие, случайные или невоспроизводимые ошибки, и при этом оставаться в рамках SLO.
Инциденты — это процесс, который начинается с момента получения дежурным или лидом оповещения о нарушении SLO. Он включает в себя реакцию на оповещение (сбор данных, остановку эксперимента или частичную раскатку), принятие мер по решению или исправлению (фикс бага, хотфикс приложения) и ретроспективу проблемы (обсуждение и выявление причин, принятие системных мер для предотвращения подобного в будущем). Идеологически этот подход почти полностью повторяет концепцию в SRE, адаптированную под устоявшиеся церемонии компании. В целом это исчерпывающая система для работы с Crash-free. Она не обязательно должна быть именно в таком виде, но обычно все так или иначе приходят к таким же концепциям. Кроме того, всю систему или её части можно переиспользовать для других метрик.
ANR
ANR (Application Not Responding) — это, по сути, разновидность крашей, возникающих при длительной блокировке главного потока приложения. Обычно их рассматривают отдельно, так как причина чаще всего более комплексная, чем NullPointerException в каком-то чётко определённом классе. Тем не менее нет необходимости создавать для них отдельную систему. Обрабатывать и мониторить их аналогично или вместе с крашами вполне приемлемо.
Время сборки
На время отложим мониторинг стабильности приложения в проде и посмотрим, что можно измерить на этапе разработки. Первое, что приходит в голову, — это время сборки проекта, релиза, библиотеки и т.д., как минимум потому, что это напрямую конвертируется в деньги. Длительные сборки занимают ресурсы, замедляют разработку и доставку ценностей для пользователей (Time to Market).
Сборка Android-проекта (чаще используют Gradle) — довольно сложный многофазный процесс, где замедление может возникнуть на любом этапе. Даже если у вас настроены все рекомендации по работе с Gradle, такие как кеш (и сборки, и конфигурации), параллелизм, последние версии Gradle-плагина и прочее, существует множество причин для деградации производительности, которые требуют постоянного контроля.
Важно различать типы сборок и задач:
Холодные сборки: кеш отсутствует (например, при сборке нового релиза на CI)
Горячие сборки: часть или все этапы/задачи сборки закешированы
Minify/Shrink (минификация и шринкинг): пожалуй, самая долгоиграющая задача при сборке более-менее крупного релизного Android-приложения
Кодогенерация (KSP/KAPT): в больших проектах отнимает много времени и часто становится причиной «поломки» кеша, превращая горячие сборки в холодные
-
Загрузка зависимостей и сборка includeBuild библиотек и плагинов: самая редкая и самая болезненная разновидность холодной сборки
Список можно продолжить, но это будут скорее опциональные вещи, «стреляющие» не в каждом проекте.
Возникает вопрос: как измерить время сборки? Всё уже давно придумано: наиболее удобным инструментом является Gradle-profiler. Обычно его размещают в CI, который, например, каждую ночь совершает необходимые прогоны и формирует отчёт. Профайлер позволяет сконфигурировать различные сборки и задать количество «прогревочных» сборок (для формирования демона, кешей и других артефактов).
Пример конфигурации сценариев прогонов
assemble_warm_build_release { tasks = [":app:assembleRelease"] warm-ups = 3 iterations = 5 } assemble_cold_build_debug { tasks = [":app:assembleDebug"] gradle-args = [ "--no-configuration-cache", "--no-build-cache" ] cleanup-tasks = ["clean"] warm-ups = 1 iterations = 3 system-properties = { "org.gradle.caching" = "false" } } assemble_cold_build_release { tasks = [":app:assembleRelease"] gradle-args = [ "--no-configuration-cache", "--no-build-cache", ] cleanup-tasks = ["clean"] warm-ups = 1 iterations = 3 system-properties = { "org.gradle.caching" = "false" } }
Отчёты предоставляются в виде HTML- и CSV-файлов, что не всегда удобно для дальнейшей работы. Поэтому имеет смысл вытащить полученные значения и перенаправить их в свою систему сбора метрик (мы используем внутренний сервис с собственной базой данных и Prometheus), а затем вывести результаты в дашборды. Такой подход позволяет не только контролировать время сборки, но и сравнивать разные сборки (например, при планировании переезда на другой DI-фреймворк можно заранее спрогнозировать влияние).
Снимать профили мы научились. Но как быть, если в какой-то момент произошёл резкий рост длительности релизных сборок? Чтобы разобраться в причинах, необходимо хотя бы локализовать проблему. Для этого могут понадобиться как минимум дополнительные метрики по времени выполнения каждой Gradle-задачи и фазы конфигурации, объёмам используемых ресурсов на машине и т.д.
Для замеров длительности выполнения задач есть готовые инструменты — например, Talaiot. Однако ни он, ни альтернативные варианты нам не подошли. Поэтому мы решили написать собственный Gradle-плагин на базе BuildService со слушателем завершения задач:
abstract class BuildTimeTrackerService : BuildService<BuildServiceParameters.None>, OperationCompletionListener{ override fun onFinish(event: FinishEvent) { if (event is TaskFinishEvent) { val taskEvent = event.descriptor val duration = event.result.startTime..event.result.endTime val taskName = taskEvent.name } } }
Далее регистрируем сервис в плагине:
val serviceProvider = project.gradle.sharedServices.registerIfAbsent( "buildTimeTrackerService", BuildTimeTrackerService::class.java ) {} getEventsListenerRegistry().onTaskCompletion(serviceProvider)
Осталось только кешировать полученную информацию, подключить плагин к проекту и настроить его запуск, например, на те же прогоны через Gradle-profiler. Важно учитывать, что профайлер выполняет несколько прогонов, включая прогревочные. По завершении стоит передать собранные метрики в систему мониторинга, добавив данные по объёмам используемой памяти (это особенно актуально для исследования работы задачи Minify). Для измерения времени конфигурации можно использовать разницу между стартом работы и project.rootProject.gradle.projectsEvaluated, а для передачи параметров или финальной записи/отправки результатов использовать FlowAction.
Когда полученные данные отобразятся на дашбордах, можно будет делать более точечные выводы. Например, выявить, что время релиза выросло в 4 раза из-за того, что при подключении новой библиотеки для задачи Minify не хватает установленных лимитов памяти. Это типичная проблема для крупных проектов, и важно узнавать о ней как можно раньше. Также не помешает утвердить SLO, чтобы оперативно отслеживать систематический или внезапный рост времени сборки.

Заключение
Мы разобрали две метрики, лежащие на разных полюсах жизненного цикла приложения: Crash-free (индикатор стабильности в продакшене) и время сборки (один из индикаторов эффективности процесса разработки и публикации). Несмотря на кажущуюся простоту, каждая из них требует продуманной системы сбора, агрегации и мониторинга, а главное — осознанного выбора SLO, соответствующего бизнес-целям продукта.
В следующих статьях мы поговорим о метриках перформанса приложения, техническом долге и оценке качества работы AI-инструментов, Lead Time и др. Если у вас есть опыт внедрения подобных метрик — буду рад обсудить его в комментариях.