На новом устройстве тестировщики заметили баг: при загрузке система сильно фризит, а анимация идет рывками и пропускает кадры, из-за чего она выглядит дерганой и неплавной. Анализ показал, что это вызвано новой функцией в Android, которая активируется только на более свежих версиях системы. Эта проблема отличается от тех, что мы встречали раньше.

Предварительный анализ

При лаге загрузочного экрана в первую очередь проверяют, не изменялись ли файлы анимации. Возможно, анимация стала более ресурсоёмкой, увеличилось её разрешение, или в процессе запуска ядра генерируется слишком много логов, а инициализация драйверов добавляет ненужные задержки. Можно также сравнить старую и новую версии, стандартную анимацию загрузки AOSP и другие варианты, чтобы примерно понять, какие изменения могли вызвать задержку.

Уточнение проблемы: просадка FPS возникает только при перезапуске zygote

Загрузка Android проходит через через три основные фазы: запуск ядра, запуск zygote и запуск system_server. Эти этапы могут по-разному влиять на плавность анимации. Было проведено три эксперимента:

  1. Запуск BootAnimation вручную 

При включении устройства и нахождении в состоянии Launcher запускается BootAnimation, который отображает загрузочную анимацию поверх текущего интерфейса. Это можно сделать с помощью команды setprop service.bootanim.exit 0; setprop ctl.start bootanim. Этот способ запуска полностью соответствует стандартам AOSP и нативному методу загрузки с анимацией.

Если в ходе эксперимента анимация пропускает кадры, значит, проблема в самом BootAnimation. Если же кадры не сбрасываются, это говорит о том, что на анимацию влияют другие модули, замедляющие её работу.

Результат эксперимента: анимация идет плавно, и нет падения кадров, что указывает на то, что проблема вызвана другими компонентами, а не самим BootAnimation.

  1. Перезапуск system_server 

Перезапустите только system_server с помощью команды am restart или killall system_server. Во время перезапуска наблюдайте за загрузочной анимацией. Если в это время происходит падение FPS, то, учитывая результаты первого эксперимента, это говорит о том, что задержка и процесс запуска system_server связаны.

Результат эксперимента: анимация плавная, без падения FPS, это означает, что проблема фризов не связана с процессом запуска system_server.

  1. Перезапуск zygote 

Перезапустите zygote, выполнив сначала команду stop, чтобы завершить его работу, а затем команду start, чтобы запустить его снова. Если в это время происходит падение FPS, это указывает на сильную связь задержки с процессом запуска zygote.

Результат эксперимента: наблюдаются лаги и падение FPS, что подтверждает, что проблема связана с процессом запуска zygote. На данный момент удалось свести проблему к процессу запуска zygote, но поскольку zygote выполняет много задач, необходимы дополнительные эксперименты для дальнейшего сужения области поиска.

Исключение аппаратных узких мест: проблема не связана с ограничениями CPU и IO

Прежде чем продолжить анализ влияния процесса запуска zygote на частоту кадров BootAnimation, давайте настроим политику распределения аппаратных ресурсов для BootAnimation. Это необходимо, чтобы получить четкое представление о том, насколько задержка связана с производительностью оборудования. Основными объектами настройки являются BootAnimation и zygote. Настройте их нагрузку и ресурсы, такие как CPU, RAM и IO, чтобы проверить, как частота кадров соотносится с производительностью оборудования.

  1. Уменьшите нагрузку на BootAnimation и увеличьте его приоритет во время работы.

  2. Снизьте частоту кадров BootAnimation до 24 fps.

  3. Настройте приоритет процесса BootAnimation в конфигурации init (bootanim.rc), увеличив его приоритет.

  4. Настройте конфигурацию приоритета ресурсов cgroup для BootAnimation в init-конфигурации bootanim.rc, поместив процесс BootAnimation под узел cgroup с самой высокой производительностью. Увеличьте task_profiles в bootanim.rc для достижения максимальной производительности (ProcessCapacityMax).

  5. Отрегулируйте конфигурацию приоритета I/O для BootAnimation в init-конфигурации bootanim.rc, установив для него самый высокий приоритет I/O (добавьте iopriority 0 в bootanim.rc).

  6. Уменьшите потребление ресурсов, создаваемое процессом Zygote. Процесс перезапуска Zygote с пониженным приоритетом будет осуществляться через rc-файл, что позволит избежать перезапуска медиа, камеры, сети и других служб.

  7. Удалите объявления о перезапуске этих служб, чтобы избежать их случайного запуска и уменьшить использование аппаратных ресурсов.

  8. Отрегулируйте rc-файл init для zygote, удалите его высокоприоритетный task profile и т. д. Рассмотрите возможность уменьшения количества предзагруженных классов zygote, так как процесс их загрузки является ресурсоемким, потребляя значительное количество CPU и IO, что может привести к блокировке DDR на максимальной частоте.

  9. Записывайте использование CPU и IO в реальном времени во время задержки процесса.

Эта серия модификаций может частично снизить нагрузку на производительность во время запуска Zygote и перераспределить ресурсы в пользу BootAnimation. После применения этих модификаций и перезапуска zygote мы всё еще наблюдали падение кадров и задержки. При этом загрузка процессора и ввода-вывода оставалась на низком уровне, что указывает на то, что задержка не связана с обычным узким местом в аппаратной производительности.

Декомпозиция анимационного потока BootAnimation: обнаружение трудоемких операций и сильной корреляции с GPU

Если мы сузили проблему до "обрезанного" потока запуска Zygote, можем ли мы выделить её на уровне функций, как на диаграмме пламени? Для анализа потока отображения BootAnimation мы можем использовать простой подход — добавить журналы для замера времени выполнения функций. Основной процесс BootAnimation довольно прост: он включает получение и разбор файла загрузочной анимации, после чего анимация предоставляет последовательность изображений для декодирования и последующего отображения с помощью OpenGLES. Этот процесс относительно прост, и объём кода невелик.

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

  • Получение событий обновления экрана (processDisplayEvents)

  • Завершение выделения буфера и декодирование кадра загрузочной анимации (initTexture и glClear)

  • Загрузка на GPU и запуск отрисовки (calling draw, done draw)

После отрисовки кадра мы выводим общее время, затраченное на него, и количество миллисекунд, которое потребовалось для его отрисовки без задержек (past и delay). Результаты журналов приведены ниже.

Как видно из анализа, задержка при падении кадров составляет 100-400 мс по сравнению с нормальным значением. Основные результаты следующие:

  • initTexture: время выполнения стабильно и составляет около 50 мс.

  • Рисование: это очень трудоемкий процесс с нестабильным временем выполнения (100-300 мс).

Поскольку более 99% трудоемких операций связаны с OpenGL, можно сделать вывод, что они имеют сильную корреляцию с производительностью и использованием GPU. Это также подчеркивает, что данные о производительности в этом измерении не были учтены на предыдущем этапе анализа.

Сужаем круг проблемы: задержка в процессе перезагрузки SurfaceFlinger

Задержка возникает при перезагрузке SurfaceFlinger, что является подмножеством процесса перезагрузки Zygote. На предыдущих этапах мы выявили две ключевые проблемы: первая связана с GPU, а вторая — с Zygote. В Android управление GPU осуществляется компонентами SurfaceFlinger, RenderEngine и HWC, которые взаимодействуют с GPU во время загрузки. Кроме того, SurfaceFlinger запускается в процессе загрузки Zygote (инициализационный файл rc настраивает зависимость: при перезапуске Zygote также перезапускается SurfaceFlinger).

Учитывая это, можно предположить, что SurfaceFlinger может сильно влиять на задержки. Мы не блокировали перезагрузку SurfaceFlinger во время тестирования Zygote "lite", так как его завершение могло бы привести к отсутствию изображения на экране, и мы не смогли бы проверить наличие отстающих кадров в загрузочной анимации.

Предлагаем провести эксперимент с помощью следующей команды, чтобы запустить только SurfaceFlinger, не перезапуская Zygote, и посмотреть, возникнут ли задержки в анимации загрузки.

stop # Остановить Zygote — эта команда завершает процесс Zygote.
start surfaceflinger # Запустить SurfaceFlinger — эта команда запускает процесс SurfaceFlinger.

Результаты эксперимента показывают, что при загрузке анимация зависает и происходит падение кадров. Теперь можно утверждать, что проблема ограничена процессом запуска SurfaceFlinger.

Определение коренной причины проблемы: SkiaRenderEngine конкурирует за доступ к GPU с BootAnimation

Взаимодействие SurfaceFlinger с GPU осуществляется через RenderEngine. В более новых версиях Android SkiaRenderEngine заменил GLESRenderEngine, используемый в предыдущих версиях. Поэтому большинство операций с GPU связано именно с RenderEngine.
Анализ кода, просмотр логов и опыт работы с процессами запуска SurfaceFlinger и RenderEngine позволили быстро установить, что операции кэширования шейдеров, выполняемые RenderEngine во время старта, занимают почти 5 секунд.

После завершения кэширования шейдеров, частота кадров BootAnimation увеличилась, что указывает на снижение нагрузки на GPU.

Что такое кэширование шейдеров? В конце статьи будет представлено краткое объяснение. Для начала рассмотрим, как оно влияет на использование ресурсов GPU.

Во время запуска SkiaRenderEngine шейдеры кэшируются с помощью нескольких многослойных вложенных циклов for, как показано на следующем изображении. Эти операции требуют значительных ресурсов GPU.

Добавив «проверочные» логи в процесс кэширования шейдеров, можно убедиться, что он действительно занимает много времени.

Поскольку эти операции кэширования предназначены исключительно для «оптимизации производительности», их отключение не приведет к функциональным проблемам. Мы провели эксперимент: убрав все вызовы кэширования, мы заметили, что анимация загрузки больше не теряет кадры и работает плавно.

Теперь можно уверенно утверждать, что причиной зависаний является нехватка производительности GPU, а виновником — код, отвечающий за кэширование шейдеров.

Как оптимизировать: отключение Prime Shader Cache

Этот кэш шейдеров создается в процессе загрузки SurfaceFlinger. Он имитирует типичные операции рисования приложений, записывая их в пустой буфер, что позволяет GPU создать кэш ресурсов OpenGLES. Эти кэши уменьшают время генерации ресурсов при первом рисовании, что в определенной степени улучшает производительность.

В коде этот процесс называется Prime Shader Cache — заполнение/инициализация кэша шейдеров. Поскольку он служит только для оптимизации, мы можем просто удалить этот код.

На самом деле в коде предусмотрен параметр service.sf.prime_shader_cache, который позволяет включать или отключать эту функцию. Если этот параметр установлен в 0, то SurfaceFlinger не будет вызывать SkiaRenderEngine для выполнения операций кэширования.

Если кэш не создан, то во время запуска приложений, при возможности, он все равно будет генерироваться. Информацию о кэше можно извлечь с помощью команды dump в SurfaceFlinger.

Таким образом, финальное решение проблемы с зависанием оказалось довольно простым: нужно было просто установить свойство service.sf.prime_shader_cache в значение, отключающее кэширование шейдеров.

Комментарии (0)