Привет, это Пьер-Ив Рико, в течение последних трех лет я рулил вниманием Square в области мобильной производительность и создавал фреймворк для ее осмысления и определения приоритетов в работе. В этой статье я рассказываю о своем подходе.
Полезные метрики
По мере роста кодовой базы приложения и числа пользователей командам разработчиков становится все труднее оценивать эффективность приложения, просто играясь с ним.
Говоря о производительности, важно быть точным и обозначать ее объективными критериями, которые можно измерить количественно. Эти критерии называются метриками.
Но если метрика основана на объективных критериях и может быть измерена количественно, это еще не значит, что такое измерение полезно.
Источник: web.dev
Полезны для чего? Команды разработчиков продуктов имеют дело с конкурирующими приоритетами, и им нужен сигнал, чтобы знать, когда и где нужно расставить приоритеты в работе над производительностью, т.е. они должны знать, когда низкая производительность влияет на опыт клиентов и, в конечном счете, снижает бизнес-показатели.
Показатели производительности мобильных устройств часто заимствуются из мира бэкенда и измеряют использование ресурсов (загрузка процессора, памяти) и длительность рабочей нагрузки (сколько времени требуется для выполнения фрагмента кода).
Хотя производительность приложения напрямую влияет на пользовательский опыт, метрики, показывающие высокую загрузку процессора или медленное чтение базы данных, не являются полезными измерениями для отслеживания реального пользовательского опыта. Они не ориентированы на пользователя.
Например, при экспорте видеоролика Insta360 я, как пользователь, хочу, чтобы приложение максимально использовало GPU и CPU, чтобы экспорт проходил быстрее.
Показатели эффективности, ориентированные на пользователя
Команды разработчиков мобильных приложений должны в первую очередь отслеживать показатели производительности, ориентированные на пользователя, которые отражают опыт использования приложения.
Это помогает продуктовым командам делать более эффективные инвестиции в производительность. В отличие от технических показателей, мы не можем устраивать пляски вокруг показателей производительности, ориентированных на пользователя. Если мы еще можем спорить о правильности использования памяти или длительности запроса к базе данных, то мы не можем спорить с показателями, которые демонстрируют плохой опыт работы с приложением.
Конечно, команды разработчиков приложений должны отслеживать использование ресурсов и техническую нагрузку в качестве вторичных показателей, чтобы помочь понять, какие компоненты влияют на показатели производительности, ориентированные на пользователя.
Существует две широкие категории показателей производительности, ориентированных на пользователя:
- Метрики плавности, которые позволяют определить, воспринимается ли движение на экране как неровное.
- Метрики отзывчивости, которые отслеживают задержку между действием пользователя и видимым ответом системы.
Пороговые значения, основанные на человеческом факторе
Исследования пользовательского опыта показывают, что люди не воспринимают улучшения задержки, превышающие определенные пороговые значения: 11 мс для задержки движения перетаскивания на экране (плавность) и 69 мс для задержки взаимодействия с экраном (отзывчивость).
В документации Apple по отзывчивости приложений проводится аналогичное различие:
Приложение, которое мгновенно реагирует на действия пользователя, создает впечатление, что оно поддерживает его рабочий процесс. Если приложение реагирует на жесты и касания в реальном времени, у пользователей создается впечатление, что они непосредственно манипулируют объектами на экране. Приложения с заметной задержкой при взаимодействии с пользователем (зависание) или движениями на экране, которые кажутся скачкообразными (заминка), разрушают эту иллюзию. В результате пользователь начинает сомневаться, правильно ли работает приложение. Чтобы избежать зависаний и заминок, при разработке и тестировании приложения придерживайтесь следующих приблизительных пороговых значений.
- < 100 мс: Синхронная работа основного потока в ответ на дискретное взаимодействие с пользователем.
- < 1 интервала обновления дисплея (8 или 17 мс): Работа основного потока и работа в ответ на непрерывное взаимодействие с пользователем.
Плавность
Любой вид движения на экране требует синхронизации обновления кадров с частотой обновления дисплея, иначе человек заметит дрожание (джанк).
Движение на экране может быть интерактивным и неинтерактивным. Интерактивное — это когда палец касается дисплея, а пользовательский интерфейс следует за ним, т.е. перетаскивание (медленная прокрутка списка или drag & drop). Неинтерактивными могут быть анимация или прокрутка на основе взмаха руки.
Джанк при интерактивном движении экрана оказывает более сильное негативное влияние на пользовательский опыт, чем джанк при неинтерактивном движении экрана.
Плавность обычно измеряется частотой кадров и количеством пропущенных кадров. Следует помнить, что плавность имеет значение только тогда, когда на экране происходит реальное движение, и в большей степени, когда это движение интерактивно.
Например, вполне нормально, если обновление пользовательского интерфейса в ответ на нажатие занимает 50 мс. Тап — это не перетаскивание, здесь нет движения. Однако если через 50 мс мы запускаем анимацию для открытия нового пользовательского интерфейса, то эта анимация должна рендерить каждый кадр.
Отзывчивость
Метрика отзывчивости — это любая метрика, которая отслеживает задержку между действием пользователя и видимым ответом системы, т.е. взаимодействием. Вот несколько примеров:
- Запуск приложения при нажатии на его иконку в лаунчере.
- Возвращение приложения на передний план из «Недавних”.
- Нажатие кнопки Like и увеличение счетчика лайков.
- Нажатие на элемент списка, чтобы открыть подробную информацию на новом экране.
- Ввод текста с помощью подключенной аппаратной клавиатуры.
- Использование подключенных часов для запуска съемки с телефона.
Пороговые значения отзывчивости
Мы по-разному оцениваем продолжительность этих взаимодействий: запуск приложения занимает значительно больше времени, чем увеличение счетчика после нажатия кнопки Like.
Эти разные ожидания обусловлены нашей способностью к формированию шаблонов. Люди очень хорошо умеют улавливать тенденции и определять отклонения. Если большинство приложений запускается за 2 секунды, то пользователи сразу же замечают приложения, которые запускаются за 500 мс или 5 секунд.
Это означает, что мы можем иметь два порога отзывчивости для каждой метрики:
- низкий порог, ниже которого приложение «значительно лучше большинства приложений»
- высокий порог, при превышении которого приложение «значительно хуже большинства приложений».
Эти пороги не могут быть универсальными, они зависят от конкретного контекста (low-end и high-end устройства, какие еще приложения используются пользователями и т.д.).
Аналогичный подход можно найти в документации Interaction to Next Paint (INP), где два порога длительности (200 и 500 мс) определяют оценку GOOD/NEEDS IMPROVEMENT/POOR для каждого измеренного образца.
Я использовал аналогичные пороговые значения в Square, но не был уверен в том, что смогу превратить их в единый показатель производительности, поэтому в основном ориентировался на P90.
Тай Смит только что указал мне на показатель Apdex, который также определяет два пороговых значения для разделения образцов на три группы (удовлетворительные, терпимые, разочаровывающие), а затем выдает оценку как средневзвешенное значение количества.
Критические показатели
Время запуска приложения имеет решающее значение, если клиенты используют приложение в течение короткого времени. Время запуска приложения менее критично, если клиент намерен использовать приложение постоянно в течение длительного времени (например, приложение для водителей Lyft, приложение для точек продаж, приложение для регистрации посетителей и т.д.).
Приняв заказ в ресторане, официант должен уметь быстро, не задумываясь и полагаясь на мышечную память, ввести его в приложение точки продаж. Для этого требуется предсказуемый пользовательский интерфейс и постоянная задержка при нажатии. В этом контексте задержка при нажатии является критичной, а время запуска приложения (которое происходит раз в день) — нет.
Аналогично, плавность прокрутки, вероятно, более важна для ленты новостей, чем для списка настроек.
Продуктовые команды должны определить, какие взаимодействия являются критическими для их клиентов, а затем использовать пороговые значения отзывчивости для определения приоритетов в работе над производительностью.
Предвзятость в агрегированных данных
Производственные метрики дают большие объемы данных, и продуктовые команды смотрят на агрегированные цифры. При использовании равномерной выборки или процентильных агрегатов результирующее число будет смещено в сторону опыта высокоактивных пользователей.
В контексте быстрого использования, где пользователям не нужно задерживаться (например, в Twitter), высокая производительность обычно коррелирует с высокой активностью, поэтому метрики не учитывают пользователей, которые использовали бы приложение, если бы производительность была выше. Таким образом, за хорошими показателями производительности может скрываться действительно низкая производительность, которая привела к тому, что пользователи перестали пользоваться приложением. Возможным решением, позволяющим избежать этого, является получение единой цифры для каждого устройства (или пользователя), а затем агрегирование этой цифры.
В контексте высокой заинтересованности, когда пользователям абсолютно необходимо использовать приложение (например, для принятия платежа), агрегированные показатели с большей вероятностью будут отражать весь спектр пользователей, а высокая активность часто связана с клиентами, которые более важны для бизнеса.
Правильный учет этих показателей затруднен
Как Google, так и Apple не смогли предоставить полезные API для наблюдения за производительностью, которые позволили бы разработчикам приложений легко отслеживать показатели производительности, ориентированные на пользователя.
Вендоры наблюдаемости предоставляют SDK, которые отслеживают эти показатели, но проблема в том, что все они делают это ужасно. Серьезно.
Пример 1: запуск приложения
Руководства и инструментарий для измерения времени запуска приложений на Android отсутствуют и непоследовательны, давайте разберемся в деталях.
Play Store Android Vitals
Показатели Play Store предоставляют выборочные и анонимные сводные данные о холодных, теплых и горячих запусках.
- Возможность выборки на основе кастомных свойств отсутствует, поэтому использовать ее для исследования или построения метрик в соответствии с потребностями бизнеса очень сложно.
- Нет никаких подробностей о том, как реализовано отслеживание. Мне пришлось заглянуть в исходные тексты AOSP!
Play Store считывает данные из внутреннего лог-трекера (не logcat), в который пишет процесс system_server. Сообщаемое время запуска совпадает с тем, что записывает в logcat ActivityTaskManager при запуске:
I/ActivityTaskManager: Displayed com.example.logstartup/.MainActivity: +1s185ms
Я исследовал записанные значения в статье “Как adb измеряет запуск приложений” . В результате этого исследования выяснилось, что:
- Измеренное время старта — это время, когда system_server получает интент запуска activity, что может добавить много времени (в моем отладочном тесте: ~300 мс/30% запуска приложения) до момента, на который разработчики приложений могут оказать влияние (это когда APK-файл начинает загружаться).
- Измеренное время окончания — это временная метка окончания отрисовки окна первой возобновленной активности.
Макробенчмарк Jetpack
Macrobenchmark получает данные о времени запуска из Perfetto, который вычисляет их на основе логов atrace. Например, конструктор ActivityMetricsLogger.LaunchingState запускает трассировку:
LaunchingState() { if (!Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { return; } // Use an id because the launching app is not yet known before resolving intent. sTraceSeqId++; mTraceName = "launchingActivity#" + sTraceSeqId; Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, mTraceName, 0); }
Затем трассировка подтягивается Perfetto.
В итоге Jetpack Macrobenchmark, Perfetto, Play Store Android Vitals и logcat выдают практически одно и то же значение (трассировка Perfetto запускается с небольшим смещением). Это замечательно, хотя и должно быть более системно задокументировано.
К сожалению, измерение времени запуска приложения в приложении для составления продуктовых аналитических отчетов — это совсем другая история.
Продуктовая аналитика
В большинстве реализаций холодный запуск приложения фиксируется путем измерения:
- Начало в виде временной метки при загрузке первого класса или в колбеке onCreate().
- Конец, когда первая активность возобновляется.
Оба измерения начала и конца некорректны, однако именно так поступает Firebase Analytics:
Такой подход неверен по нескольким причинам:
- В этом случае не учитываются «горячий» и «теплый» запуск, о которых сообщает Play Store Android Vitals.
- Также в подсчет холодных запусков включаются случаи, когда процесс сначала запускается в фоновом режиме по несвязанным причинам, а затем немного позже (в течение, например, минуты после запуска процесса) запускается активность. Это приводит к завышению суммарной длительности метрик «холодного» запуска и не должно учитываться. Такие фальшивые холодные старты следует игнорировать, обращая внимание на важность процесса в Application.onCreate() и убеждаясь, что первый Activity.onResume() произойдет до того, как будет запущен основной поток, запланированный из Application.onCreate() (см. “Android Vitals — Почему запустился мой процесс”).
- Время запуска совершенно не связано с тем, о чем сообщает Play Store Android Vitals: загрузка случайного класса или вызов ContentProvider.onCreate() могут происходить через значительный промежуток времени после того, как system_server получает интент запуска.
- К сожалению, не существует API, предоставляющего временную метку намерения запуска. А он должен быть!
- Process.getStartUptimeMillis() (API 28+) перехватывается непосредственно перед загрузкой APK и является гораздо лучшим вариантом отчета о начале запуска, чем ContentProvider.onCreate() или время загрузки класса. К сожалению, в реальных производственных условиях выяснилось, что при использовании API 28+ мы иногда получаем время начала запуска на несколько часов раньше (см. “Android Vitals — Почему запустилось мое приложение?”).
- Время окончания должно соответствовать моменту фиксации первого кадра после первого возобновления активности (см. ViewTreeObserver.registerFrameCommitCallback). Первый обход, скорее всего, включает в себя достаточно много работы, и запись окончания с помощью Activity.onResume() игнорирует всю эту работу.
Пример 2: задержка взаимодействия с тапом
Провайдеры наблюдаемости предоставляют API для записи интервалов. Заманчиво использовать их для регистрации задержки взаимодействия, например, как здесь, сколько времени потребовалось для перехода к экрану «О программе» при нажатии на кнопку «О программе»:
showAboutScreenButton.setOnClickListener { val span = tracer.buildSpan("showAboutScreen").start() findNavController().navigate(R.id.about_screen) span.finish() }
К сожалению, это не учитывает реальный опыт пользователя, поскольку между моментом, когда палец покидает экран, и моментом, когда вызывается листенер клика, а также между моментом, когда обновляется иерархия представления, и моментом, когда изменения становятся заметны на экране, происходит много работы. Вот трассировка Perfetto, демонстрирующая эту распространенную ошибку:
Разработчики приложений часто не имеют возможности погрузиться в эту сложность и в итоге используют некорректные, но более простые в реализации измерения.
Лучший способ?
Вот что нам нужно от Google для запуска приложения:
- Документация, описывающая, как все инструменты экосистемы (Play Store Android Vitals, Logcat, Perfetto, Macrobenchmark) в настоящее время измеряют время запуска приложений (для холодного, горячего и теплого запуска).
- Новые API-интерфейсы Android для доступа к метке времени, когда намерение запуска активити было получено сервером system_server.
- SDK для Android, реализующий корректный способ отслеживания запуска приложений, как указано в разделе выше. Это должно включать в себя «холодный», «теплый» и «горячий» старт. Я попробовал реализовать это на примере небольшой (экспериментальной) библиотеки: square/papa.
- Firebase (и сторонние производители, OpenTelemetry и т.д.) могли бы использовать этот SDK.
Перспективы
Моя долгосрочная цель, связанная с этими статьями и библиотекой square/papa, заключается в том, что либо Apple и Google сделают шаг вперед и предоставят сильное руководство и более полезные API для наблюдаемости (например, JankStats), либо мы объединимся как сообщество и создадим надежные SDK с открытым исходным кодом для измерений.