Производительность сборки может быть проанализирована в самом Xcode. Такой анализ позволит понять узкие места, внести изменения и ускорить рабочий процесс, сэкономив много времени в течение дня для всех разработчиков, работающих над проектом. Медленная сборка часто отвлекает нас, поскольку возникают такие отвлекающие факторы, как социальные сети и Slack.
Изучив производительность сборки и потратив некоторое время на ее улучшение там, где это возможно, вы увидите, что можно добиться прогресса с помощью нескольких небольших шагов. С помощью нескольких оптимизаций я ускорил время инкрементальной сборки WeTransfer в 4 раза. Давайте рассмотрим имеющиеся на сегодняшний день возможности.
Измерение скорости сборки с помощью Xcode Build Timeline
Прежде чем приступить к оптимизации, необходимо измерить базовую производительность проекта. В процессе внедрения улучшений можно будет постоянно сравнивать базовые показатели.
Начните с запуска чистой сборки, используя CMD ⌘ + SHIFT ⇧ + K или выбрав в меню пункт Product ➔ Clean Build Folder…. Папка сборки содержит кэшированные файлы сборки, которые ускоряют инкрементные сборки, но это приводит к получению некорректного начального значения для оптимизации производительности. Очень важно сравнивать сборки, используя ту же среду, которую вы создадите, очистив папку сборки.
Вы можете начать новую сборку после того, как Xcode завершит очистку. После завершения сборки выберите свою сборку в Report Navigator и обязательно откройте помощник, чтобы посмотреть временную шкалу сборки:
Ассистент показывает, на что было потрачено все время во время чистой сборки. В приведенном выше примере вы видите оптимизированную версию проекта WeTransfer с оптимизированным распараллеливанием. Последнее означает, что ядра моего MacBook используются оптимально, поскольку каждая строка постоянно заполнена процессами. Я рекомендую вам посмотреть сессию WWDC 2022 “Демистификация распараллеливания в сборках Xcode”, чтобы узнать все об этой концепции.
Мы будем использовать эту временную шкалу сборки в качестве отправной точки для исследования производительности. Вы можете найти потенциальные улучшения, увеличив масштаб определенных блоков, которые выполняются последовательно. Например, на приведенной выше временной шкале показан следующий изолированно выполняемый скрипт:
После клика на конкретном блоке можно получить дополнительную информацию о выполняемом процессе. В моем случае я выяснил, какой скрипт мы рассматриваем, и пришел к выводу, что дальнейшая оптимизация для чистых сборок невозможна. Более подробно об этих конкретных оптимизациях я расскажу позже.
Анализ производительности с помощью Build Timing Summary
Build Timing Summary — это еще один базовый показатель, который можно использовать при оптимизации производительности сборки. Он собирает проект один раз и суммируется время, затраченное на каждую категорию.
Действие может быть вызвано в меню продукта с помощью команды Product ➔ Perform Action ➔ Build with Timing Summary или с помощью xcodebuild -showBuildTimingSummary.
После выполнения этого действия вы увидите, что Xcode начинает сборку вашего проекта на выбранном целевом устройстве или симуляторе.
После завершения сборки перейдите в навигатор отчетов и выберите последнюю сборку. Выберите «Recent» и прокрутите страницу вниз, пока не увидите Build Timing Summary.
Это может стать еще одной отличной отправной точкой для изучения того, где следует улучшить проект. В большинстве случаев больше всего времени уходит на компиляцию файлов Swift, для чего следует сосредоточиться на оптимизациях распараллеливания. Также я рекомендую посмотреть на время выполнения скриптов, чтобы понять, есть ли возможности для улучшения.
Обратите внимание, что здесь я выполнил чистую сборку, что отличается от инкрементной сборки. Стоит выполнить действие напрямую еще раз, что приведет к получению другого отчета Build Timing Summary. Большую часть времени вы будете выполнять инкрементные сборки, поэтому стоит найти медленные участки, которые влияют как на чистые, так и на инкрементные сборки.
Приведенный выше пример представляет собой инкрементную сборку после завершения оптимизации приложения WeTransfer. Мы почти не запускаем никаких скриптов и завершаем сборку за приемлемые 5 секунд.
Как может длительность компиляции исходных текстов Swift быть больше, чем общее время сборки?
Прежде чем мы перейдем к оптимизации, я хочу обратить внимание на то, что компиляция исходных текстов Swift занимает больше времени, чем общее время сборки. Приведенная выше сборка заняла 71 секунду, в то время как компиляция исходных текстов Swift заняла 352 секунды.
Я обратился за разъяснениями к Рику Балларду из команды Xcode Build System, и он дал несколько замечательных пояснений о том, как работает система:
Да, многие команды, особенно компиляция, могут выполняться параллельно друг с другом, поэтому многоядерные машины завершат сборку гораздо быстрее, чем время, затраченное на выполнение каждой из команд.
Оптимизация фаз сборки
Оптимизация фаз сборки — отличный способ ускорить сборку в Xcode. Некоторые из наших сценариев запуска могут быть не нужны для отладочных сборок и должны быть настроены только для сборки релизов.
Во время написания этой статьи я пытался улучшить время сборки приложения Collect by WeTransfer, которое я создал во время своей работы. Я обнаружил, что большую часть времени мы тратим на выполнение SwiftLint. Для нашей основной цели его выполнение занимало 10 секунд при каждом инкрементальном билде.
Небольшое улучшение мы нашли в добавлении параметра —quiet, но это дало нам лишь менее секунды на сборку. Хоть немного помогло, поэтому мы решили оставить этот параметр. Реальным значительным улучшением стала фильтрация файлов, которые не были изменены. Поскольку мы запускаем SwiftLint во многих наших подмодулях, мы легко получили ~15 секунд улучшения на сборку, включая все цели.
Код, связанный с SwiftLint, довольно специфичен для проектов, использующих этот инструмент. Если вы хотите увидеть наше окончательное решение, я рекомендую вам ознакомиться с этим запросом.
Запускайте фазу сборки только в случае необходимости
Если у вас есть сценарий, который вы хотите запускать только для отладки или релиза, вы можете включить проверку конфигурации:
В данном случае мы запускаем скрипт SwiftLint только для отладочных сборок. Аналогичным образом можно поступить, установив флажок для сборок «Release», если требуется запускать сценарий только для релизных сборок.
Проверка типов функций и выражений
Чтобы сузить круг причин медленной сборки, можно включить swift-флаги для получения дополнительной информации. Эти флаги были доступны еще в Xcode 10, но они по-прежнему очень полезны.
Компилятор может предупредить об отдельных выражениях, проверка типа которых занимает много времени, с помощью двух frontend флагов:
- -Xfrontend -warn-long-function-bodies=<limit>
- -Xfrontend -warn-long-expression-type-checking=<limit>.
Значение <limit> может быть заменено на количество миллисекунд, в течение которых выражение должно пройти проверку типа, чтобы было выдано предупреждение.
Чтобы включить эти предупреждения, перейдите в Build Settings ➔ Swift Compiler — Custom Flags ➔ Other Swift Flags:
При использовании этой настройки Xcode будет выдавать предупреждение для любой функции, проверка типа которой заняла более 100 мс. Это может указать на методы, которые замедляют время сборки. Разделение таких методов, а также добавление явных типов может привести к повышению производительности билда.
Приведенный выше метод приводит к медленной проверке типов, что плохо сказывается на производительности всей сборки. В данном случае причиной медленной проверки типа является неправильная работа с перечислением. Добавив NSFetchedResultsChangeType перед .delete и .insert, мы устранили предупреждение:
Настройки сборки для повышения производительности
Ускорение сборки Xcode за счет изменения нескольких настроек было распространенным приемом, позволяющим быстро набрать несколько секунд на инкрементной сборке. В настоящее время в Xcode большинство этих настроек установлены по умолчанию, поэтому их практически не нужно изменять. Однако может оказаться, что вы поддерживаете старый проект, в котором эти настройки все еще необходимо установить или они перезаписаны неверными значениями. Поэтому здесь приведен краткий обзор рекомендуемых на сегодняшний день настроек.
COMPILATION MODE
- Debug: Incremental
- Release: Whole Module
OPTIMIZATION LEVEL
- Debug: No Optimization [-O0]
- Release: Fastest, Smallest [-Os]
BUILD ACTIVE ARCHITECTURE ONLY
- Debug: Yes
- Release: No
DEBUG INFORMATION FORMAT (DWARF)
- Debug – Any iOS Simulator SDK: DWARF
- Release – Any iOS SDK : DWARF with DSYM File
Включение Eager Linking
Вы можете включить ускоренное связывание для своего проекта, чтобы проверить, не приведет ли это к улучшению времени сборки. Если эта функция включена, система сборки будет генерировать так называемый TBD-файл для фреймворков и динамических библиотек, предназначенных только для Swift, чтобы разблокировать связывание зависимых целей до того, как их зависимость закончит связывание. Подробнее о линковке можно узнать, посмотрев сессию WWDC 2022 “Линкуйте быстро: Улучшение времени сборки и запуска”.
Параллельный запуск фаз сценария сборки
Параллельный запуск фаз сценария сборки потенциально может привести к улучшению времени сборки. Я рекомендую сочетать его с User Script Sandboxing для запрета необъявленных input/output зависимостей. Только сценарии с указанными входами и выходами, а также сценарии, настроенные на выполнение на основе анализа зависимостей, будут пытаться выполняться параллельно.
Плагины Swift Package Build
Плагины сборки Swift-пакетов оказывают значительное влияние на время. В приложении WeTransfer я обнаружил, что плагин предварительной сборки приводит к аннулированию кэша и увеличению времени при инкрементных сборках. У нас была возможность перейти на обычный плагин сборки, что привело к уменьшению количества недействительных записей в кэше и ускорению сборки инкрементальных сборок.
Важно также знать, что запуск плагина сборки пакетов всегда занимает около секунды:
В приведенном выше примере не было файлов, которые нужно было бы вычищать, и плагин сборки не возвращал никаких команд сборки. Я ожидал, что влияние на общее время сборки будет близко к нулю, но здесь требуется выполнить определенный минимум работы.
Заключение
Время от времени полезно пересматривать время сборки в Xcode. Вы можете извлечь пользу из каждой выигранной секунды, и помните, что потерянное время накапливается: секунда на каждую сборку — это минута на каждые 60 сборок. Улучшения могут быть достигнуты за счет настроек проекта, фаз сборок и проверки типов в коде.
Спасибо!