Борьба за время сборки iOS-приложений

Habrahabr 2

Чуть больше месяца назад мы выпустили iOS-приложение «Тинькофф Инвестиции». Приложение полностью написано на языке Swift, но имеет некоторые Objective-C-зависимости. Продукт быстро начал обрастать новой функциональностью, а вместе с тем время сборки проекта существенно увеличивалось. Когда мы пришли к тому, что после clean или значительных правок проект собирался дольше шести минут, мы осознали, что перемены необходимы.

image
На просторах интернета было найдено много действенных и не очень способов ускорить время сборки проекта. Особенно нас интересовало время сборки debug-версии, потому что работать становилось всё сложнее. Ниже я расскажу о методах, которые мы опробовали в рамках решения задачи, и результатах, которых мы добились. Хочу отметить, что долгое время сборки может зависеть от разных факторов, поэтому и методы для каждого проекта используются разные.

1. Участки кода, сложные для компиляции.

Поскольку Swift еще молод, некоторые сладкие синтаксические конструкции могут вызывать непонимание у компилятора. Для оценки самых тяжелых для компиляции функций можно использовать встроенный в компилятор Swift-анализатор. Проще всего получить отчет, выполнив сборку проекта из консоли этой командой:
xcodebuild -workspace App.xcworkspace -scheme App clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep .[0-9]ms | grep -v ^0.[0-9]ms | sort -nr > functions_build_analysis.txt
где «App.xcworkspace» — название файла workspace вашего проекта, «App» — название схемы, по которой нужно сделать билд.

Мы передаем флаги "-Xfrontend -debug-time-function-bodies" для отладки процесса компиляции и учета времени на компиляцию каждой функции. С помощью grep мы выбираем строки, содержащие время компиляции, затем выводим отсортированный результат в файл functions_build_analysis.txt.

С помощью такого отчета мы нашли несколько тяжелых для компиляции функций, одна из которых собиралась 17 секунд, а другая — 6. Основная причина такого плачевного результатам — использование «Nil Coalescing» в конструкторе объекта. В коде были такие конструкции:

let object = Object(param1: param1Value ?? defaultParam1Value,
  param2: param2Value ?? defaultParam2Value)
Мы вынесли вычисление значения параметра в отдельную строку выше, и проблема была решена — время компиляции функций сократилось до 300 миллисекунд.

Это далеко не единственный сюрприз, который может преподнести вам компилятор Swift. Основные проблемы долгой сборки отдельных функций связаны с определением типов переменных. Чаще всего это связано с использованием операторов «??», «?:» в конструкторах объектов, словарей, массивов, а также при конкатенации строк и массивов. Вы можете прочитать статью с интересными наблюдениями по поводу ускорения времени сборки через рефакторинг кода.

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

2. Сборка только выбранной архитектуры для Debug-билдов.

В debug-сборке происходит компиляция проекта только для архитектуры устройства, выбранного для отладки. То есть, выбирая здесь Yes, мы теоретически ускоряем время сборки в два раза.

Поскольку в нашем проекте этот флаг уже был установлен в Yes, мы не добились выигрыша в этом пункте. Но ради эксперимента мы опробовали сборку с флагом, выставленным в No. Для этого пришлось повозиться с Pod’ами, потому что они тоже были готовы предоставить скомпилированный код только для активной архитектуры. Время сборки проекта в итоге составило 10 минут 21 секунду, что действительно почти в два раза больше, чем изначальное.

3. Whole Module Optimization.

У Swift-компилятора есть флаг под названием «-whole-module-optimization». Он отвечает за то, каким образом будут обрабатываться файлы во время компиляции: будут ли они компилироваться по одному или сразу собираться в модуль. В Xcode управлять этим флагом можно с помощью секции «Optimization Level», однако по умолчанию нам доступны только эти опции:

Использование Whole Module Optimization существенно уменьшает время компиляции debug-сборок. Но вместе с этим флагом нам добавляют флаг «-O», который включает SIL-оптимизатор, и возникает проблема — проект перестает поддаваться отладке. В консоли мы видим следующее:

App was compiled with optimization - stepping may behave oddly; variables may not be available.

Чтобы сохранить возможность сборки сразу целого модуля и отключить оптимизацию, можно добавить флаг «-Onone» в секции «Other Swift Flags». В итоге для отладки мы получаем сборку, максимально быстро собирающуюся за счет отключения всякого рода оптимизаций. В нашем проекте это дало поразительные результаты — скорость сборки debug увеличилась почти в 3 раза.

4. Precompiled Bridging Headers.

Есть еще один флаг компилятора, который помогает сократить время компиляции. Но работает он только для сборок без флага «-whole-module-optimization» и, скорее, может быть полезен для Release-сборок. Это флаг «-enable-bridging-pch».

Помогает он не во всех случаях, а только в проектах с Bridging-хедерами от Objective-C. Эффект заключается в том, что каждый раз во время сборки компилятор не перестраивает таблицу бриджинга Objective-C методов в Swift.

Для нашего проекта с выключенным флагом «-whole-module-optimization» и включенным «-enable-bridging-pch» выигрыш во времени составил около 15%.

Итоги

По итогам исследования для ускорения компиляции вашей Debug-сборки можно выделить два основных способа: оптимизация самого кода для компилятора и использование флага «whole-module-optimization». Нам удалось снизить чистое время сборки проекта (clean build) с 6 минут до 1 минуты 20 секунд, половину из которых занимает сборка сторонних зависимостей. Если у вас есть свой опыт борьбы с компилятором Swift, поделитесь в комментариях.

P.S.: железо, на котором проводились тесты: Mac mini (Late 2012) 2,3 GHz Intel Core i7 16 GB 1600 MHz DDR3 250 GB SSD