Меня зовут Никита Красавин, я тимлид команды разработки Flutter для ОС Аврора в Открытой мобильной платформе. Сегодня я расскажу вам об одной из нашумевших фич Flutter, или, правильнее сказать, о компоненте, который мы адаптировали для Авроры с целью повышения производительности приложений. Повысилась она или нет — ответим ниже в статье. Встречайте: Impeller для ОС Аврора.

Impeller — это библиотека в составе Flutter Engine, написанная на C++. Impeller был разработан специально для Flutter, чтобы избавиться от проблемы зависания анимаций, на которую разработчики постоянно жаловались.

Анимации зависали, потому что ранее на всех платформах Flutter по умолчанию использовал Skia, которая компилирует шейдеры в рантайме. Для компиляции нужно время, поэтому при отрисовке сложных (и не очень) UI-сценариев "на холодную" fps падал. Решалось это прекомпиляцией шейдеров и прогревом приложения. Решение рабочее, но требующее постоянного внимания при обновлениях UI. Умельцы добавляли прекомпиляцию шейдеров в UI-тесты, после чего при сборке автоматически прогревали приложение. Но многие просто ничего не делали и жили с лагами. Присутствие таких приложений на рынке породило ошибочное мнение о забагованности Flutter.

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

Для iOS Impeller включен по умолчанию начиная с релиза Flutter 3.10. Для Android — с 3.27 и только для API 29+. Мы решили не отставать и добавили возможность включения Impeller для Авроры начиная с Flutter 3.32. Открытый исходный код Flutter для ОС Аврора можно найти по ссылке.

Поддержка Impeller для Авроры

Кроссплатформенный Impeller поддерживает разные графические api, такие как Metal, Vulkan, OpenGL. iOS оказался для Impeller самой простой платформой, потому что на ней работает небольшой набор конкретных устройств и драйверов, для которого стабилизировать Impeller было сильно проще.

На Авроре отрисовка графики происходит через Wayland и OpenGL, поэтому мы включили соответствующий OpenGL-бэкенд. Этот вариант в Impeller стабилизирован хуже, чем вариант для Metal или Vulkan, тем не менее он доступен в экспериментальном режиме. Дословный статус для OpenGL Impeller:

Skia is the default. Impeller may or may not work. The team is not actively working on this and doesn't recommend using it.

Для включения Impeller на Авроре нам потребовалось передать параметр --enable-impeller в движок через Aurora Embedder:

void CommandLine::Parse(int argc, char* argv[]) {
  binary_name_ = std::filesystem::path(argv[0]).filename();
  engine_args_.Append(argv[0]);

  static struct option options[] = {
      {"dart-entry-point", required_argument, nullptr, 'e'},
      {"no-gui", no_argument, nullptr, 'n'},
      {"enable-impeller", no_argument, nullptr, 'i'},
      {0},
  };

  int opt;

  while ((opt = getopt_long(argc, argv, "e:n", options, NULL)) != -1) {
    switch (opt) {
      case 'e':
        dart_entry_point_ = optarg;
        break;
      case 'n':
        no_gui_ = true;
        break;
      case 'i':
        engine_args_.AppendUnique("--enable-impeller");
        break;
      default:
        break;
    }
  }
  
  if (optind < argc) {
    dart_entry_point_args_.Append(argc - optind, argv + optind);
  }
}

Aurora Embedder — это компонент с открытым исходным кодом, отвечающий за работу Flutter-приложений на Авроре и обеспечивающий их взаимодействие с ОС.

После передачи параметра для включения Impeller заработал, но с рядом ограничений. Основное — OpenGL-бэкенд не поддерживал альбомную ориентацию. Для решения проблемы мы написали и применили код формирования матрицы транcформации GL:

impeller::Matrix ToImpellerMatrix(const SkMatrix& sk) {
  return impeller::Matrix::MakeRow(
    sk[SkMatrix::kMScaleX], sk[SkMatrix::kMSkewX], 0.0f, sk[SkMatrix::kMTransX],
    sk[SkMatrix::kMSkewY], sk[SkMatrix::kMScaleY], 0.0f, sk[SkMatrix::kMTransY],
    0.0f, 0.0f, 1.0f, 0.0f,
    sk[SkMatrix::kMPersp0], sk[SkMatrix::kMPersp1], 0.0f, sk[SkMatrix::kMPersp2]
  );
}

Другие ограничения выражались в некорректной отрисокве конкретных UI-сценариев. С учётом результатов тестирования производительности (про которые расскажем далее) мы посчитали проделанную работу достаточной для поддержки экспериментального режима.

Как включить Impeller

Чтобы включить Impeller при отладке проекта на Авроре добавьте флаг --enable-impeller к команде flutter run:

flutter run --enable-impeller

Более подробную инструкцию по работе с Impeller на Авроре вы можете найти в нашей официальной документации.

Тесты производительности

Давайте посмотрим на реальных примерах, что нам даёт включение Impeller на Авроре. Тесты ниже проводились на планшете KVADRA_T с ОС Аврора 5.2.0. Приложения запускались командой flutter run --profile с использованием Skia и Impeller (флаг --enable-impeller). О недавнем крупном обновлении Авроры 5.2 есть отдельная статья, которую я рекомендую к прочтению.

Переход на новый экран без прогрева

В приложении Fluttery ToDo попробуем без прогрева открыть экран деталей todo сразу после запуска приложения. Экран открывается с анимацией увеличения из элемента списка. Контент не содержит тяжёлых для отрисовки элементов. Однако при использовании Skia переход все равно порождает фризы:

Fluttery ToDo: Skia
Fluttery ToDo: Skia

На изображении явно видно, что UI- и Raster-потоки последовательно задержали два фрейма. Dev Tools Performance View сигнализирует об обнаружении UI Jank и Raster Jank.

Теперь сделаем то же самое с включенным Impeller:

Fluttery ToDo: Impeller
Fluttery ToDo: Impeller

На графике остался фриз, который порождает UI-поток. При этом Raster-поток стабильно держит 60 fps. Это означает, что в данном случае отрисовка через GPU в Impeller отработала лучше, и для дальнейшей оптимизации стоит обратить внимание на реализацию build-методов страницы деталей в коде приложения. Также вы наверняка заметили, что выключенные checkbox-ы неверно отрисованы. Это пример бага реализации для OpenGL. Подобный эффект наблюдается и в других приложениях при включении Impeller.

Нагрузка на UI и Raster

Для универсальных тестов производительности я использую специально написанное приложение. Оно умеет нагружать UI- и Raster-потоки и создавать "тяжёлые" эффекты. Вот результаты, которые я получил при использовании Skia:

UI Pref Lab: Skia
UI Pref Lab: Skia

Приложение содержит 3 экрана с анимированным контентом. В рантайме накапливается статистика по отрисовке последних 2000 кадров с использова��ием SchedulerBinding. На панели внузу экрана выводится среднее время отрисовки кадра в миллисекундах для UI- и Raster-потоков, а также 90-й и 99-й перцентиль этого значения.

Левый tab содержит список ListView из 1000 элементов с бэкграундом, меняющим цвет. Средний tab рисует 8000 фигур на канвасе через CustomPainter. Правый tab содержит GridView c тяжёлыми элементами, у которых есть blur, градиент, тень и прозрачность. Количество элементов на экранах можно менять, я подобрал подходящие значения по умолчанию для своего устройства.

Вот аналогичные скриншоты с включенным Impeller:

UI Pref Lab: Impeller
UI Pref Lab: Impeller

Пришло время сравнить полученные значения.

Таблица и график для UI-потока:

List build, ms

Canvas Paint, ms

Effects, ms

Skia UI

avg: 13.5
p90: 18.3
p99: 26.6

avg: 17.2
p90: 23.3
p99: 29.0

avg: 19.8
p90: 28.0
p99: 33.6

Impeller UI

avg: 13.4
p90: 18.6
p99: 25.3

avg: 16.8
p90: 22.3
p99: 26.3

avg: 18.4
p90: 22.3
p99: 27.4

Производительность UI-потока
Производительность UI-потока

Таблица и график для Raster-потока:

List build, ms

Canvas Paint, ms

Effects, ms

Skia Raster

avg: 12.4
p90: 16.8
p99: 22.6

avg: 27.4
p90: 28.3
p99: 29.5

avg: 49.4
p90: 51.6
p99: 53.0

Impeller Raster

avg: 11.5
p90: 15.2
p99: 24.2

avg: 27.0
p90: 27.5
p99: 32.3

avg: 22.9
p90: 25.1
p99: 29.4

Производительность Raster-потока
Производительность Raster-потока

Мы видим, что на дистанции в 2000 кадров разница между Impeller и Skia для построения списков и рисования на канвасе незначительна. Impeller в среднем работает чуть быстрее на обоих потоках. А вот для рисования эффектов Impeller действительно показал себя значительно лучше, производительность Raster-потока стала в два раза выше, чем у Skia. Подведём итоги.

Итоги

  • Impeller на Авроре доступен начиная с Flutter 3.32 в экспериментальном режиме

  • Используется реализация Impeller для OpenGL

  • Включение Impeller в большинстве случаев даст незначительный прирост производительности и уменьшит фризы при холодном запуске

  • Включение Impeller заметно ускорит UI, использующий тени, прозрачность, blur и градиенты

  • Impeller может неправильно отрисовать UI в конкретных случаях (мы нашли пример с Checkbox)

Ссылки