Чуть больше месяца назад мы выпустили 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
Поделиться с друзьями
-->

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


  1. gramotnii
    19.06.2017 11:43
    -6

    Jenkins подключили и время на сборку билда перестало играть роль.
    нажал кнопку и пишешь код дальше


    1. ToyoApps
      19.06.2017 11:50
      +5

      Jenkins не решает проблемы сборки приложения на компьютере разработчика (если вы про CI говорите). Прежде чем послать pull-request у нас считается хорошей практикой потестировать написанный код собственными руками в наиболее типичных кейсах реализованной функциональности, для чего и необходимо собрать приложение «у себя».


      1. gramotnii
        19.06.2017 12:00
        -5

        либо я чего-то не понял либо… пишешь код, запускаешь на симуляторе/девайсе, тестишь, заливаешь на гит, запускаешь jenkins, тестировщики ставят себе билды

        Но за статью спасибо, полезная инфа!


        1. mayorovp
          19.06.2017 12:09
          +2

          Вы забыли важный шаг. Тот самый, который тут ускоряют.


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


          1. gramotnii
            19.06.2017 12:25
            -5

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


            1. Agranatmark
              19.06.2017 22:27

              рекомендую посмотреть wwdc 2017 про swift 4. Одно из нововедений, которое будет внедрено в xcode 9, инкрементальная сборка. Суть проблемы, если сейчас вы изменили 1 файл, то у вас весь проект билдится заново. Особенно это доставляет, если вы разрабатывается на компе с hdd.


    1. dmitry_dvm
      19.06.2017 12:00

      Дженкинс обычно собирает при коммите в основную репу. Как он поможет при постоянных дебажных запусках?


      1. Aazamandius
        19.06.2017 15:23
        -1

        Дженкинс собирает с той ветки с который вы ему скажете. Другое дело заливать коммит банально не проверив собирается ли он вообще по моему крайне глупо.
        Я не думаю что товарищ gramotnii пишет код никогда не запуская приложение на своем устройстве


  1. Njall
    19.06.2017 16:33
    -1

    Great!


  1. SarkazmMan
    19.06.2017 18:22

    Вопрос по первому пункту — а почему используете nil coalescing, а не дефолтные параметры?


    1. ToyoApps
      19.06.2017 18:27

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


  1. Mehdzor
    20.06.2017 01:35

    Не хвастовства ради, но уже было и гораздо подробнее освящено.
    https://habrahabr.ru/post/317650/
    https://habrahabr.ru/post/317298/
    https://habrahabr.ru/post/316986/


    1. ToyoApps
      20.06.2017 09:00

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

      Но спасибо за дополнение, было интересно почитать.