Всем привет ? я Максим Кузнецов a.k.a. Android-developer из Альфа-Мобайл. В этой статье хочу поделиться нашим опытом внедрения механизмов мониторинга производительности в продукты компании. Почему это важно? Потому что производительность напрямую влияет на опыт пользователей, рейтинги приложений и конверсии. Мы рассмотрим статистику, проблемы, наш горький опыт и планы на будущее. Давайте начнем! ?
Зачем тратить время на перформанс?
Как уже отметил выше, перформанс мобильных приложений влияет на UX, количество звезд в Play Market и App Store, продажи и репутацию компании. Есть множество подтверждающих исследований (ссылки приложил в конце статьи).
Статистика исследований:
53% посещений, вероятно, будут прекращены, если экраны загружаются дольше 3 секунд.
Каждый второй человек ожидает, что экран загрузится быстрее, чем за две секунды.
46% людей говорят, что ожидание загрузки это то, что им больше всего не нравится.
36 % пользователей согласны с тем, что медленное приложение вызывает «пониженное мнение о компании».
Пользователи оставляют негативные отзывы и ставят низкие оценки медленным и глючным приложениям.
Если приложение тормозит, пользователи быстрее теряют интерес и меньше времени проводят в нём. Это снижает показатели вовлеченности и конверсии в покупки, что ударяет по монетизации.
С необходимостью поддержания высокой производительности мобильных приложений разобрались, двигаемся вперед.
Быстрое отступление или как понять, что экран действительно лагает?
Мы давно ходим не с кинеографом в кармане, а оптимальной частотой обновления экрана мобильных устройств считается 60 FPS.
При такой частоте обновления скорость отрисовки одного кадра должна быть равна: 1000ms / 60 = 16.6666667 миллисекундам.
Так это выглядит, когда просадок нет.
А так выглядит, когда просадка есть.
Какие есть способы измерения перформанса? Наиболее часто используемые из них:
CPU, GPU, Memory, Energy профайлеры.
Микро- и макро-бенчмарки.
System tracing.
Что-то еще.
Профайлинг CPU позволяет выявить узкие места, связанные с высокой загрузкой процессора. Это важно для плавной работа интерфейса и быстрого отклика на действия пользователя.
Например, источником просадки может быть тяжелая верстка в RecyclerView.Adapter
onCreateViewHolder()
.
Профайлинг GPU важен для приложений с интенсивной графикой, таких как игры, видеоредакторы или для работы с анимациями в Android-приложении. Он позволяет выявить проблемы с рендерингом, текстурами, шейдерами и другими аспектами, связанными с загрузкой видеокарты.
Инструменты профайлинга GPU дают детальную информацию о том, сколько времени занимает отрисовка каждого кадра, какие операции с графикой являются самыми медленными.
Это помогает оптимизировать использование видеопамяти, уменьшить количество вызовов OpenGL, избавиться от лишних вычислений в шейдерах и обеспечить стабильные 60 FPS.
Профайлинг памяти критичен для предотвращения утечек памяти и оптимизации использования ОЗУ.
Инструменты профайлинга помогают найти и устранить утечки памяти, оптимизировать алгоритмы, избавиться от ненужных копий данных и уложиться в лимиты ОЗУ даже на слабых устройствах.
Профайлинг энергопотребления важен для увеличения времени автономной работы устройства.
Инструменты профайлинга энергопотребления показывают, какие компоненты устройства (CPU, GPU, экран, сеть) потребляют больше всего энергии в каждый момент времени.
Это позволяет выявить и оптимизировать наиболее прожорливые участки кода, избавиться от ненужных вычислений, уменьшить яркость экрана, отключать неиспользуемые сервисы и добиться максимального времени работы от батареи.
Микро- и макро-бенчмарки — это инструменты для измерения производительности приложения на разных уровнях.
Микробенчмарки тестируют отдельные методы и алгоритмы, замеряя время их выполнения, позволяя выявить узкие места в коде и оптимизировать наиболее ресурсоемкие операции.
Макробенчмарки, в свою очередь, измеряют производительность приложения в целом при выполнении реальных сценариев использования. Они дают представление о том, как приложение будет работать в реальных условиях на разных устройствах
System Trace позволяет записывать активность системы в течение короткого периода времени в файл трассировки.
Основные возможности System Trace:
Записывает события и метки времени из ядра Linux, библиотек и приложений Android.
Отображает точную временную информацию о работе всех процессов на устройстве в момент записи трассировки.
Показывает, где приложение тратит время и что происходит внутри системы в конкретные моменты.
Позволяет добавлять пользовательские метки в код приложения для более детального анализа.
Для захвата трассировки System Trace можно использовать библиотеку Perfetto. Файл трассировки можно расшифровать в веб-интерфейсе Perfetto.
Choreographer
Перечисленные выше способы измерения перформанса требуют особых знаний и дополнительного времени инженера, которым они не всегда располагают. Также бывает важным находить неочевидные просадки FPS и при ручном тестировании. Поэтому мы хотели инструмент, который мог использовать любой член кросс-функциональной команды без погружения в библиотеки и документацию. Как мы этого достигли рассмотрим немного позже, а сейчас обратим внимание на Choreographer.
Наиболее внимательные разработчики замечали в консоли следующий текст:
I/Choreographer: Skipped 146 frames! The application may be doing too much work on its main thread.
Choreographer контролирует анимации и другие графические события на экране устройства. Его задача — обеспечить плавное выполнение анимаций и событий, чтобы приложение выглядело плавным для пользователя. Он планирует работу в рамках рендеринга следующего кадра.
Взглянем на последовательность вызовов методов, которые ведут к отрисовке первого кадра:
ActivityThread.handleResumeActivity()
WindowManagerImpl.addView()
WindowManagerGlobal.addView()
ViewRootImpl.setView()
ViewRootImpl.requestLayout()
ViewRootImpl.scheduleTraversals()
Choreographer.postCallback()
Choreographer.scheduleFrameLocked()
Метод Choreographer.scheduleFrameLocked()
ставит в очередь сообщение MSG_DO_FRAME
. При обработке сообщения MSG_DO_FRAME
происходит вызов метода Choreographer.doFrame()
, который как раз нас и интересует.
/**
* Implement this interface to receive a callback when a new display frame is
* being rendered. The callback is invoked on the {@link Looper} thread to
* which the {@link Choreographer} is attached.
*/
public interface FrameCallback {
/**
* Called when a new display frame is being rendered.
* <p>
* This method provides the time in nanoseconds when the frame started being rendered.
* The frame time provides a stable time base for synchronizing animations
* and drawing. It should be used instead of {@link SystemClock#UptimeMillis()}
* or {@link System#nanoTime()} for animations and drawing in the UI. Using the frame
* time helps to reduce inter-frame jitter because the frame time is fixed at the time
* the frame was scheduled to start, regardless of when the animations or drawing
* callback actually runs. All callbacks that run as part of rendering a frame will
* observe the same frame time so using the frame time also helps to syncronize effects
* that are performed by different callbacks.
* </p><p>
* Please note that the framework already takes care to process animations and
* drawing using the frame time as a stable time base. Most applications should
* not need to use the frame time information directly.
* </p>
*
* @param frameTimeNanos The time in nanoseconds when the frame started being rendered,
* in the {@link System#nanoTime()} timebase. Divide this value by {@code 1000000}
* to convert it to the {@link SystemClock#uptimeMillis()} time base.
*/
public void doFrame(long frameTimeNanos);
}
Choreographer предоставляет возможность зарегистрировать обратный вызов, который может быть запущен при любом вызове doFrame, эти колбэки будут выполняться синхронно один за другим.
Choreographer получает уведомление о начале построения следующего кадра и синхронно вызывает doFrame колбэки, которые в конечном итоге могут вызвать ваш код.
Таким образом мы можем реализовать любую механику в doFrame, например:
№1. Послать push-нотификацию с именем Activity/Fragment
, где произошла просадка.
Пример кода:
private const val SLOW_FRAME_RENDER_TIME_MS = 120L
class NotificationFrameListener : Choreographer.FrameCallback {
private val choreographer = Choreographer.getInstance()
private var lastTimeMillis = 0L
private var lastScreenOpenTimeMillis = 0L
override fun doFrame(frameTimeNanos: Long) {
val currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos)
val frameRenderTime = currentTimeMillis - lastTimeMillis
if (frameRenderTime > SLOW_FRAME_RENDER_TIME_MS && lastTimeMillis > 0) {
showNotification()
}
lastTimeMillis = currentTimeMillis
choreographer.postFrameCallback(this)
}
}
Пример отображения.
№2. Сохранить информацию о просадке в базу данных.
№3. Отрисовать FPS в оверлее приложения.
Пример кода:
private const val TICK_INTERVAL_MS = 500L
class OverlayFrameListener : Choreographer.FrameCallback {
private val choreographer = Choreographer.getInstance()
private var startTimeMillis = 0L
private var lastTimeMillis = 0L
private var framesRendered = 0
override fun doFrame(frameTimeNanos: Long) {
val currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos)
lastTimeMillis = currentTimeMillis
if (startTimeMillis > 0) {
val timeSpan = lastTimeMillis - startTimeMillis
framesRendered++
if (timeSpan > TICK_INTERVAL_MS) {
val fps = framesRendered * 1000 / timeSpan.toDouble()
showFps(fps.roundToInt())
startTimeMillis = lastTimeMillis
framesRendered = 0
}
} else {
startTimeMillis = lastTimeMillis
}
choreographer.postFrameCallback(this)
}
}
Пример отображения.
Открытие Activity может вызывать просадку FPS и это нормально
Одна из первых проблем, с которыми мы столкнулись — спам уведомлений при просадке. Это происходит из-за того, что при запуске Activity загружаются и инициализируются её компоненты: разметка, ресурсы, данные, выполнение различных жизненных циклов компонентов и т. д. Всё это может привести к блокировке главного потока (UI thread), что в свою очередь может сказаться на плавности анимаций и общей производительности приложения.
Единичные просадки до 30-40 FPS длительностью до 100-200 мс — это нормально, они, как правило, незаметны для пользователя.
Множественные просадки до 20-30 FPS длительностью до 500 мс могут быть заметны, но не критичны.
Длительные просадки ниже 20 FPS более 1 секунды — уже проблема.
Фиксится это завязкой на методы жизненного цикла активити и регулировкой ключевых значений того, что мы считаем просадкой.
Внутри класса Application
есть интерфейс ActivityLifecycleCallbacks
. Один из его методов — onActivityPreCreated
. Именно здесь мы паузируем показ нотификаций, пока активити не перейдет в состояние Resumed
.
class CurrentActivityProvider: Application.ActivityLifecycleCallbacks {
var currentActivity: Activity? = null
private set
var onActivityPreCreatedCallback: (() -> Unit)? = null
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
onActivityPreCreatedCallback?.invoke()
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {
if (activity == currentActivity) {
currentActivity = null
}
}
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstaceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit
}
P.S. Медленным мы считаем кадр, который отрисовывается более 80 мс на устройствах с частотой обновления экрана в 120 Гц, и более 30 мс на устройствах с 60 Гц.
P.P.S. Вычислено эмпирически.
// TODO или что мы сделали не так?
Со спамом нотификаций при открытии активити мы разобрались, настроили нужные трешхолды, добавили возможность временного отключения и вроде бы все хорошо.
Но результат первого a/b теста показал, что почти все разработчики выключали фичу почти сразу, так как информация о просадке не была комплексной и полезной. Очевидные просадки и так бросались в глаза, а остальные не были критичными. QA-инженеры особым вниманием новую фичу так же не потчевали.
В будущем мы планируем собирать информацию в базу данных и батчами отправлять на сервер для анализа производительности от версии к версии, на определенных девайсах, фичах и тд.
Спасибо за внимание, по любым вопросам велкам в комменты ?
molnij
КДПВ с скриншотами шутеров очень в тему конечно.
Расскажите, а что вы такое умудряетесь делать, чтобы приложение для отображения статического контента начинало тормозить?