Как стать автором
Обновить
487.81
Альфа-Банк
Лучший мобильный банк по версии Markswebb

Как мы отслеживаем просадки FPS в Альфа-Мобайл?

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров1.7K

Всем привет 👋 я Максим Кузнецов 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-инженеры особым вниманием новую фичу так же не потчевали.

В будущем мы планируем собирать информацию в базу данных и батчами отправлять на сервер для анализа производительности от версии к версии, на определенных девайсах, фичах и тд.

Спасибо за внимание, по любым вопросам велкам в комменты 🌞

Источники

Теги:
Хабы:
+24
Комментарии5

Публикации

Информация

Сайт
digital.alfabank.ru
Дата регистрации
Дата основания
1990
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель
София Никитина