image
Статья о том как починить инкрементальную компиляцию в Xcode для Swift проектов и ускорить build phases для Cocoapods и Carthage, ничего не поломав.


Небольшой спойлер: на трех разных проектах получилось сократить время инкрементальной сборки в 9 раз!


Туториал несет сугубо практический характер с минимумом воды. Обязательно к прочтению для действующих iOS разработчиков.



Проблема


У нас в компании ведется несколько параллельных проектов на Swift и, практически, везде была сложность с последовательной сборкой. На каждый повторный 'Run' уходило от 30 до 50 секунд. Минуту пишется код, пол минуты ждешь пока соберется проект. Успеваешь сходить за чаем, покурить, посчитать налоги и иногда вздремнуть.


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


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


P.S. Картинка в шапке не означает, что мы 'вертели' Xcode. Это его якобы так ускоряем.


Инкрементальная компиляция


Если кто не знаком с термином — это способ сборки только изменившихся мест кода без тотальной рекомпиляции всего проекта. А еще это то, что нормально не работает в Xcode.


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


Судя по всему, мы не одни такие. О рецидивах пишут и на StackOverflow под ответом, а так же в исходной теме на форумах Apple.


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


Что вообще такое этот header map для которого мы ставим флаг по рекомендации Apple?
Немного веб-археологии по документации яблока:


Header maps (also known as “header maps”) are files Xcode uses to compile the locations of the headers used in a target.

Своими словами, header maps — это индексный файл Xcode с местоположениями хидеров проекта. Файл о всех хидерах, которые мы используем.


Так как я обещал не погружаться слишком глубоко в теорию, то вот отличная статья на эту тему. Легко читается, крайне рекомендую.


Главное, что нечто в этих header maps(или то, что их использует) провоцирует недобросовестную работу инкрементальной компиляции. Если мы нашли такую техническую опухоль, то давайте поступим без полумер и просто их отключим.


Заходим в build settings и убираем лишние детали выставляем существующий флаг USE_HEADER_MAPS в NO:


image


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


Никуда не уходим из настроек и руками прописываем пути до папок с заголовками в поле 'user header search paths':


image


В Swift проекте их должно быть мало, только ваши кастомные Obj-C вещи.


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


image


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


В качестве бонуса отметим, что инкрементальная компиляция работает наилучшим образом при выключенном whole-module-optimization(WMO) о котором мы говорили в прошлый раз. Это не значит, что для полномодульной оптимизациии решение не работает, просто без нее все проходит на несколько секунд быстрее. Пускай при этом сборки с чистого листа и тянутся целую вечность.
Здесь уже каждый сам для себя решает, что ему удобнее, быстрее и лучше подходит. Если вы решили отказаться от WMO, то достаточно убрать флаг SWIFT_WHOLE_MODULE_OPTIMIZATION из настроек проекта, каких-либо сложностей возникнуть точно не должно.


Результат: инкрементальную компиляцию мы успешно починили. Отныне должно тратиться не более нескольких секунд на сам факт сборки Swift, не считая codesign, линковки и различных build scripts(о них мы поговорим ниже). Внутри компании мы проверили этот метод на трех разных проектах, нескольких версия OSX и вообще множестве конфигурациях в целом, что в том числе относится и к Xcode. И вот уже продолжительное время рецидивов не наблюдаем.


Разгон Cocoapods и Carthage


Наверное, многие замечали, что кроме самой компиляции еще много времени уходит на разные 'shell scripts':


image


При использовании cocoapods и/или carthage их даже несколько. На их выполнение уходит от 3 до 10 секунд каждый раз в зависимости от скорости вашего диска, процессора и положения звезд на небе. Cocoapods прописывает себя туда автоматически, а для Carthage приходится это делать вручную.
Немного изучив контент, мы выяснили, что эти скрипты занимаются ничем другим как копированием своих ресурсов в сборочную директорию проекта. При этом они не заботятся о том были уже скопированы этих файлы или нет. Хотя, гораздо логичнее было бы проверять наличие нужных ресурсов перед их повторным копированием.


Что мы и сделаем.


Начнем с Carthage.


Вдохновением для этого решения послужил недооцененный ответ на StackOverflow, где приняли и покрыли лайками одноразовый костыль. Кстати, если вы вдруг решите воспользоваться одобренным решением, то поймаете дикие и горячие ошибки в Runtime стоит вам сделать полный clean.


Мы так поступать не будем, поэтому пойдем по правильному пути и видоизменим наш Copy Carthage Frameworks build phase:


FILE="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Carthage-Installation-Flag"
if [ -f "$FILE" ]; then
    exit 0
fi
touch "$FILE"
/usr/local/bin/carthage copy-frameworks

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


Для уверенности, скриншот результата, который у нас должен получиться:


image


Важно! Если вы решите добавить или удалить зависимость из Carthage, то перед сборкой проекта обязательно нужно провести полный clean. Иначе скрипт так и будет считать, что он уже все давно установил. А вы будете в поте лица пытаться найти объяснение происходящему.


Кстати, найти функцию clean можно в меню по пути Product > Clean.


image


Если еще зажать option(alt), то можно сделать особый clean, который к тому же изгоняет демонов из проекта удаляет различные локальные настройки, кеш и часто дающую сбой прочую ерунду.


Теперь Cocoapods

Для бобов принцип тот же самый, но так как скрипты генерируются автоматически, придется добавить немного магии в Podfile. Чтобы это сделать, докинем следующие строки в самый конец файла:


post_install do |installer|
  Dir.glob(installer.sandbox.target_support_files_root + "Pods-*/*.sh").each do |script|
    flag_name = File.basename(script, ".sh") + "-Installation-Flag"
    folder = "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
    file = File.join(folder, flag_name)
    content = File.read(script)
    content.gsub!(/set -e/, "set -e\nKG_FILE=\"#{file}\"\nif [ -f \"$KG_FILE\" ]; then exit 0; fi\nmkdir -p \"#{folder}\"\ntouch \"$KG_FILE\"")
    File.write(script, content)
  end
end

Принцип работы этого скрипта такой же: он добавляет в скрипты копирования ресурсов проверку на файл-флаг. При этом не меняет build phases, в отличие от Carthage, а сразу изменяет сам скрипт cocoapods.


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


Теперь можно сделать 'pod install', чтобы изменения вступили в силу. Как и в случае с Carthage, при редактировании Podfile, тоже нужен полный clean проекта перед запуском.


image
P.S. Ruby не мой родной язык, попридержите тапки.


Заключение


Таким образом, у нас получилось сократить время сборки проекта с 45-и секунд до 5-и.


Чуть не забыл пруфы. Время сборки до:


image


Скриншот взят из видео к прошлой статье.


Время сборки после:


image


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


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


Напоследок поделюсь утилитой, которой я пользовался для измерения времени компиляции методов: https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode
Показывает индивидуально каждую функцию и суммарное время затраченное на её сборку.

Поделиться с друзьями
-->

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


  1. ad1Dima
    16.12.2016 15:59

    Читая всю эту серию постов очень сильно задумываюсь, стоит ли начинать новый проект на свифте...


    1. Mehdzor
      16.12.2016 16:11

      Swift сам по себе хорош, но многие системные вещи вгоняют в печаль. Пока они не будут исправлены, делать что-то серьезное — себе дороже. Как вспомню переход на Swift 3, так дурно становится. Мы у себя в компании приняли решение новые проекты делать снова на Obj-C.


      1. SpectarlDragon
        16.12.2016 16:49

        Мне помогло решение с Cocoapods. Спасибо за статью :)


        1. Mehdzor
          16.12.2016 16:50

          Пожалуйста. А с инкрементальной компиляцией у вас не было проблем?


          1. GYFK
            16.12.2016 18:32

            У нас в проекте — никаких. Спасибо огромное :)


  1. chesno4eck
    16.12.2016 16:02

    Спасибо за статьи. Продолжайте в том же духе!


  1. RomanVolkov
    17.12.2016 13:28

    Спасибо за статью!
    Кстати, в случае с Carthage файл может содержать кол-во входных файлов ( переменная $SCRIPT_INPUT_FILE_COUNT ), а скрипт проверять кол-во текущих входных файлов и сохраненное значение после clean & build.

    Взял за основу ваш скрипт

    FILE="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Carthage-Installation-Flag"
    if [ -f "$FILE" ]; then
    echo "!!!! $FILE"
        while read -r line
        do
            if [ "$line" != "$SCRIPT_INPUT_FILE_COUNT" ]; then
                echo "warning: count of Carthage files has changed. Make Clean & Build"
            fi
    
        done < "$FILE"
    
    
        exit 0
    fi
    touch "$FILE"
    echo -e $SCRIPT_INPUT_FILE_COUNT >> "$FILE"
    
    /usr/local/bin/carthage copy-frameworks
    


    image


    1. Mehdzor
      17.12.2016 13:29

      Идея хороша. Только надо отталкиваться не от количества строк, а от Cartfile.resolved. Иначе что делать в случаях, если количество зависимостей осталось такое же? Или просто изменилась версия?


      1. RomanVolkov
        17.12.2016 14:01

        Согласен, тут нужен более точный анализ того какие зависимости были и какие стали. Чтобы уже наверняка предупреждать о необходимости очистить проект. Как вариант внутрь Carthage-Installation-Flag копировать Cartfile.resolved, а потом уже сравнивать. Если есть отличия — кидать warning.


        1. Mehdzor
          17.12.2016 15:32

          Если есть отличия, то выполнять copy-frameworks ;)


          1. RomanVolkov
            17.12.2016 16:43

            Точно! :D


  1. kolbin_aa
    17.12.2016 22:31

    Правильно ли я понимаю, в User Header Search Path нужно просто указать путь к директории/ям с ObjC?


    1. Mehdzor
      17.12.2016 22:31

      Да.


  1. atimca
    19.12.2016 15:30

    Очень хотелось бы узнать, как побороть застревание на моменте «compiling swift source files». Интернет говорит, что нужно для этого править код, но проект уже по идее целиком сбилжен.


    1. Mehdzor
      19.12.2016 15:31

      Уточните, что значит застревание? Зависает или долго выполняет просто шаг?


      1. atimca
        19.12.2016 16:12

        Застревает. Сейчас билд, после минимальных изменений занимает около минуты, хотя Build Time Analyzer показывает хорошие результаты. Билд без изменения кода занимает от 2 до 7 секунд, хотя раньше было секунд 40, до применения приемов из вашей статьи, за что отдельное спасибо.


        1. Mehdzor
          19.12.2016 17:51

          Рекомендую открыть лог сборки и посмотреть, что происходит в момент зависания на шаге Compile Swift.
          image


        1. Mehdzor
          19.12.2016 17:52

          Еще, если минуту занимает после минимальных изменений, то проверьте, может быть включен SWIFT_WHOLE_MODULE_OPTIMIZATION на ряду убранным header maps.


          1. atimca
            19.12.2016 20:31

            Действительно был включен SWIFT_WHOLE_MODULE_OPTIMIZATION, спасибо! Хотя Xcode не подавал виду. После того как я воспользовался некоторым приемом по откату настроек Build Settings (выделил параметр Optimization Level и нажал backSpace) все залетало намного шустрее.


            1. Mehdzor
              19.12.2016 20:43

              Здорово, что помогло.


  1. slxl
    22.12.2016 10:18
    -1

    Спасибо! Сэкономили мне пару часов жизни)