Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз

О работе DevOps в геймдеве, или как собирать в Unity в 2,5 раза больше контента за то же время, что и раньше.

Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз

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

Наверняка вы слышали о таких понятиях, как DevOps и непрерывная интеграция. Так вот, тот самый DevOps — это и есть набор методов, благодаря которым можно сократить время разработки и ускорить выпуск обновлений. А DevOps-инженер — соответственно, тот, кто этим занимается. CI/CD, или непрерывная интеграция и доставка — это не сколько технология, сколько целая культура, позволяющая чаще и надежнее производить небольшие изменения в игре с частыми коммитами, доставлять модули проекта в разные отделы и автоматизировать их тестирование. Все это представляет собой целый пласт работ — эдакий невидимый игрокам фронт.

Изначально, до старта работ над War Robots Remastered, у нас уже был выстроенный пайплайн CI/CD для всех проектов, и оригинальная War Robots не была исключением. Сам проект тогда в среднем собирался 40-100 минут. Но чем дальше продвигалась работа над ремастером, чем больше накапливалось проблем со скоростью сборок. Спустя полгода проект стал собираться от 3-х часов и больше с периодическими зависаниями, которые могли доходить до 7-10 часов. Это становилось совсем неприемлемым: QA в динамике не могли проверять билды, разработчикам тоже приходилось тратить время на ожидание, чтобы посмотреть результат или начать профилировать. Пришлось серьезно подумать над тем, как все это чинить и возвращать время сборок к исходному значению.

Как было раньше и какие проблемы это повлекло

Раньше наш пайплайн сборки приложения выглядел следующим образом. В качестве сервера CI мы использовали TeamCity от JetBrains: на War Robots тогда было выделено 22 агента, которые могли выполнять скрипты сборки. Они располагались на четырех физических компьютерах-нодах. Конфигурации нод приблизительно похожи, средняя тачка имела следующие характеристики:

  • OS: Windows;
  • ЦП: AMD Ryzen Threadripper 1950X 16-Core Processor 3,40 ГГц;
  • RAM: 128 ГБ;
  • Диски: SSD не менее 1ТБ на одного агента и не более двух агентов на один диск;

Для сервера Unity Cache V1 мы использовали:

  • VM: Linux CentOS 7;
  • RAM: 24 ГБ;
  • Диск: 150 ГБ.

Дополнительно был установлен Mac mini для сборок на iOS. Более подробно о старом пайплайне можно прочитать здесь.

Агенты у нас запущены как служба, работающая фоново независимо от пользователя, а не как отдельное приложение. Сделано это для возможности более гибкого менеджмента непосредственно самих агентов. Кроме того, таким образом можно выделить для каждого агента отдельного юзера со своими правами и реестром. Это оказалось удобно, но породило проблему, которую оказалось не так-то просто диагностировать.

Заключалась она в том, что во время сборки Unity неожиданно начинала крешиться без особых опознавательных логов. Но небольшой ресерч в Интернете помог определить, что проблема связана с desktop heap, или кучей рабочего стола на Windows.

В чем дело? У Windows есть неочевидная настройка: чем больше запущено процессов одновременно, тем быстрее происходит переполнение кучи. И поскольку у нас параллельно запускается несколько Unity для сборки билдов, это очень сильно влияло на скорость переполнения.

Экспериментально увеличив значение HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SubSystems\Windows, креши удалось локализовать. Но с этим стоит быть аккуратнее: Microsoft не рекомендует выделять на кучу памяти больше, чем 20480 КБ:

Если вы выделяете слишком много памяти на кучи рабочего стола, может возникнуть отрицательная производительность. Поэтому мы не рекомендуем устанавливать значение более 20480 КБ.

Больше пресетов графики — дольше собирается билд. Что делать?

Как уже говорилось выше, по итогу время сборки билда выросло критически: с 40-100 минут до 3-4 часов с периодическими зависаниями импорта до 10 часов. Связано это с тем, что мы разделили текущий пресет качества графики и добавили новые. Если изначально у нас был только один пресет — Legacy, то теперь их стало четыре: Legacy, ULD (Ultra-Low Definition), LD (Low Definition) и HD (High Definition).

Разница между пресетами, слева направо: ULD, LD, HD:

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

Экран импортера ассетов
Экран импортера ассетов

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

Кроме того, изначально у нас были дополнительные самописные импортеры, которые работали с разными типами ассетов. Преимущественно они писались для работы с шейдерами и материалами.

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

Hashing assets (38480 files)... 66.051 seconds file read: 37.856 seconds (42992.805 MB) wait for write: 22.787 seconds (I/O thread blocked by consumer, aka CPU bound) wait for read: 9.976 seconds (CPUT thread waiting for I/O thread, aka disk bound) hash: 53.298 seconds

Они не говорят ни о чем плохом — лишь показывают, что система начала хешировать ассеты для последующих действий над ними. В нашем случае проблема была в том, что такое сообщение появлялось в логе после импорта каждого пятого-десятого ассета. Полный рехеш одного ассета выполняется за минуту, так что время импорта взлетело до небес. Обычно такое происходит, когда что-то в коде вызывает AssetDatabase.Refresh() — именно из-за него перехватчики других событий зацикливаются, и Unity приходится пересчитывать весь ассет полностью.

Мы нашли все такие места — которых, конечно, оказалось немало, ведь эта функциональность нужна многим плагинам, — и обложили их трейсами. Затем мы нашли, где происходили рефреши — это оказалась подписка на AssetPostprocessor.OnPostprocessAllAssets. Так мы выяснили, что комбинация нашего скрипта и GPGSUpgrader (Google Play third-party файл) дает тот самый эффект увеличения времени сборки.

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

Следующим нашим шагом была оптимизация текущего кэш-сервера Unity.

Мало кэш-серверов = большие нагрузки

Когда мы создаем проект в Unity, нельзя просто добавить в него ассет — Unity переформатирует его в собственные форматы отдельно для iOS и Android. Для хранения таких файлов и существует кэш-сервер, откуда можно запросить уже собранные и переформатированные ассеты, и их не нужно заново пересчитывать. Если ассеты на сервере не хранятся — в таком случае происходит их пересчет, и он занимает какое-то время.

Для справки приведем немного цифр нашей конфигурации проекта:

  • Размер репозитория — более 150 ГБ;
  • Общее количество веток в репозитории — более 1000;
  • Количество файлов в проекте — более 170 000, из них большая часть — ассеты.

На момент старта работы над War Robots Remastered у нас было использовано три кэш-сервера на весь проект: один использовался разработчиками, остальные — для нашего окружения CI и Dev/Release.

Проблема заключалась в том, что проект имеет очень большое количество мелких файлов, так что сервер не выдерживал нагрузки и просто не успевал их вовремя раздавать. Редко возникали ошибки сети, гораздо чаще — ошибки типа «Disk I/O is overloaded» из-за огромного количества обращений к диску. Из-за этого — те самые зависания на 5-7 часов, вызванные тем, что при невозможности кэш-сервера отдать файлы клиентам Unity начинала полный импорт проекта.

В результате мы пришли к тому, чтобы увеличить число кэш-серверов для CI для перераспределения нагрузки на дисках. Таким образом, 22 агента мы развели на сеть серверов — по три на каждую платформу: Android-Dev, iOS-Dev, Win-Dev, Android-Release, iOS-Release, Win-Release, — что значительно улучшило ситуацию. Также мы провели 10-гигабитную сеть до агентов. В результате проблемы сети перестали появляться, а количество ошибок I/O значительно снизилось.

Но все же время импорта оставляло желать лучшего. В среднем сам импорт с кэш-сервера достигал полутора часов, и мы решили провести эксперимент с новой версией кэш-сервера — Unity Accelerator.

На момент начала наших экспериментов у нас использовалась версия Unity 2018.4, которая не позволяла использовать Asset Database V2 — а он был необходим для того, чтобы использовать Unity Accelerator, поскольку формат хранения ассетов в нем был несовместим с версией Unity Cache Server V1. Параллельно нам пришлось обновлять версию Unity до 2019.4.22f1 — тогда-то и получилось совершить переход на новый кэш-сервер, который пошел проекту только на пользу. Теперь импорт с платформы iOS с очисткой папки Library выглядел следующим образом:

Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз

Таким образом, Unity Accelerator оказался вполне рабочим вариантом: время импорта у него приемлемое, но прогрев не происходит за 1 прогон, и необходимо несколько итераций для наполнения кэшей.

Так, решив проблему с импортом, мы стали разбираться дальше.

А что с бандлами?

Следующим шагом оптимизации времени стала непосредственно сборка бандлов в проекте.

Мы используем разделение качеств для разных типов девайсов (подробнее о том, как это устроено у нас в проекте, можно почитать здесь). Отсюда следует, что практически весь контент у нас разбит на паки качеств, в которых уже лежат необходимые бандлы с ассетами.

Мы выявили сразу две проблемы:

  • Проблема переиспользования бандлов из прошлых сборок;
  • Проблема однопоточных сборок бандлов.

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

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

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

Вторая проблема заключается в том, что для каждого пресета качества Unity собирала бандлы последовательно, друг за другом, так что на сборку всех четырех пресетов качеств уходило более часа.

Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз

Чтобы этого избежать и снизить время сборки, мы стали действовать по следующей схеме:

  • После импорта проекта мы генерим дополнительные Unity-проекты для каждого необходимого качества, где с помощью symlink линкуем папку Library для каждого сгенерированного проекта. Но будьте аккуратнее: внутри библиотеки есть динамические папки, которые создаются во время сборки. Например, может пересчитываться кэш шейдеров или папка с packages, что может привести к проблемам в параллельных сборках. Соответственно, линковать нужно только статический контент и саму базу.
  • В отдельных процессах запускаем одновременно несколько экземпляров Unity и в каждом собираем отдельное качество.
  • Результат от каждой сборки мы перемещаем в родительский проект.
Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз

Таким образом, мы смогли распараллелить время сборки бандлов в рамках одной ноды.

Подводя итоги: результаты нашей борьбы с гигантским временем сборки

  • Нам удалось вернуть показатели времени сборок на прежний уровень до выхода War Robots Remastered даже несмотря на то, что количество контента в проекте увеличилось в 2,5 раза. Для этого мы использовали Unity Accelerator. Также удалось убрать зависания импорта, минимизировав вызовы AssetDatabase.Refresh().

Нагляднее в цифрах:

Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз

На графике ниже можно подробнее посмотреть распределение времени сборки по датам:

Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз
Проблемы скорости сборки, или что делать, если время сборки игрового билда увеличивается в 10 раз
  • Мы перестали пересобирать бандлы — вместо этого теперь забираем их сразу готовые из ранее собранных билдов.

  • Распараллелили сборку бандлов разных качеств, запустив одновременно несколько экземпляров Unity и залинковав ассеты.

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

Александр Панов
Руководитель группы кросс-проектной разработки в Pixonic
101101
28 комментариев

Звучит как статья на habr 

13
Ответить

Звучит как статья от разработчика игр на сайте посвященном разработке игр.

38
Ответить

Но, к сожалению, в данном подходе присутствует человеческий фактор, когда программист должен сам решить, менялся у него контент или нет. Это накладывает существенные ограничения в виде ошибок сборки, когда может отсутствовать или измениться контент.Я чего-то может не понимаю, но почему вы при сборке ассетов не ведёте свой учёт версий? Мы в своё время для обычных Ассет Бандлов (не Addressables) такое делали. Рядом с бандлами у нас было свой json с версиями всех бандлов и по нему смотрели, что меняется.

6
Ответить

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

7
Ответить

Извините, похоже на инструкцию: Как устроить ад в проекте :-)

1
Ответить

Скорее наоборот: как его избежать! :)

2
Ответить

Комментарий недоступен

Ответить