Статья о том как починить инкрементальную компиляцию в 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:
Теперь нам надо компенсировать потерянный функционал и вручную добавить расположение всех хидеров проекта.
Никуда не уходим из настроек и руками прописываем пути до папок с заголовками в поле 'user header search paths':
В Swift проекте их должно быть мало, только ваши кастомные Obj-C вещи.
Опять делаем полный clean и пробуем завести проект. Если влетели в следующую ошибку
то значит вы просто забыли упомянуть путь до одного или нескольких заголовочных файлов. После их исправления все должно собраться без осложнений, так как кроме этого мы никаких изменений не вносили.
В качестве бонуса отметим, что инкрементальная компиляция работает наилучшим образом при выключенном whole-module-optimization(WMO) о котором мы говорили в прошлый раз. Это не значит, что для полномодульной оптимизациии решение не работает, просто без нее все проходит на несколько секунд быстрее. Пускай при этом сборки с чистого листа и тянутся целую вечность.
Здесь уже каждый сам для себя решает, что ему удобнее, быстрее и лучше подходит. Если вы решили отказаться от WMO, то достаточно убрать флаг SWIFT_WHOLE_MODULE_OPTIMIZATION из настроек проекта, каких-либо сложностей возникнуть точно не должно.
Результат: инкрементальную компиляцию мы успешно починили. Отныне должно тратиться не более нескольких секунд на сам факт сборки Swift, не считая codesign, линковки и различных build scripts(о них мы поговорим ниже). Внутри компании мы проверили этот метод на трех разных проектах, нескольких версия OSX и вообще множестве конфигурациях в целом, что в том числе относится и к Xcode. И вот уже продолжительное время рецидивов не наблюдаем.
Разгон Cocoapods и Carthage
Наверное, многие замечали, что кроме самой компиляции еще много времени уходит на разные 'shell scripts':
При использовании 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-х лет. В следующий раз, когда скрипт будет запущен, он сначала проверит наличие этого файла, и если файл есть, то операция автоматически завершится без лишних телодвижений.
Для уверенности, скриншот результата, который у нас должен получиться:
Важно! Если вы решите добавить или удалить зависимость из Carthage, то перед сборкой проекта обязательно нужно провести полный clean. Иначе скрипт так и будет считать, что он уже все давно установил. А вы будете в поте лица пытаться найти объяснение происходящему.
Кстати, найти функцию clean можно в меню по пути Product > Clean.
Если еще зажать 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 проекта перед запуском.
P.S. Ruby не мой родной язык, попридержите тапки.
Заключение
Таким образом, у нас получилось сократить время сборки проекта с 45-и секунд до 5-и.
Чуть не забыл пруфы. Время сборки до:
Скриншот взят из видео к прошлой статье.
Время сборки после:
На мой взгляд, крайне значимый результат. Подход с сокращением издержек должен обязательно стоять на вооружении каждой компании и разработчика.
Уверен, что среди вас найдутся люди с опытом оптимизации Xcode, готовые дополнить и поделиться мыслями. Да и вообще, хотелось бы в комментариях видеть вопросы, чтобы в следующих своих статьях упоминал вас, ваш опыт, опыт ваших коллег и освещал тематику, т.к. для меня это хобби, жизнь и работа.
Напоследок поделюсь утилитой, которой я пользовался для измерения времени компиляции методов: https://github.com/RobertGummesson/BuildTimeAnalyzer-for-Xcode
Показывает индивидуально каждую функцию и суммарное время затраченное на её сборку.
Комментарии (21)
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
Mehdzor
17.12.2016 13:29Идея хороша. Только надо отталкиваться не от количества строк, а от Cartfile.resolved. Иначе что делать в случаях, если количество зависимостей осталось такое же? Или просто изменилась версия?
RomanVolkov
17.12.2016 14:01Согласен, тут нужен более точный анализ того какие зависимости были и какие стали. Чтобы уже наверняка предупреждать о необходимости очистить проект. Как вариант внутрь Carthage-Installation-Flag копировать Cartfile.resolved, а потом уже сравнивать. Если есть отличия — кидать warning.
atimca
19.12.2016 15:30Очень хотелось бы узнать, как побороть застревание на моменте «compiling swift source files». Интернет говорит, что нужно для этого править код, но проект уже по идее целиком сбилжен.
Mehdzor
19.12.2016 15:31Уточните, что значит застревание? Зависает или долго выполняет просто шаг?
atimca
19.12.2016 16:12Застревает. Сейчас билд, после минимальных изменений занимает около минуты, хотя Build Time Analyzer показывает хорошие результаты. Билд без изменения кода занимает от 2 до 7 секунд, хотя раньше было секунд 40, до применения приемов из вашей статьи, за что отдельное спасибо.
Mehdzor
19.12.2016 17:51Рекомендую открыть лог сборки и посмотреть, что происходит в момент зависания на шаге Compile Swift.
Mehdzor
19.12.2016 17:52Еще, если минуту занимает после минимальных изменений, то проверьте, может быть включен SWIFT_WHOLE_MODULE_OPTIMIZATION на ряду убранным header maps.
atimca
19.12.2016 20:31Действительно был включен SWIFT_WHOLE_MODULE_OPTIMIZATION, спасибо! Хотя Xcode не подавал виду. После того как я воспользовался некоторым приемом по откату настроек Build Settings (выделил параметр Optimization Level и нажал backSpace) все залетало намного шустрее.
ad1Dima
Читая всю эту серию постов очень сильно задумываюсь, стоит ли начинать новый проект на свифте...
Mehdzor
Swift сам по себе хорош, но многие системные вещи вгоняют в печаль. Пока они не будут исправлены, делать что-то серьезное — себе дороже. Как вспомню переход на Swift 3, так дурно становится. Мы у себя в компании приняли решение новые проекты делать снова на Obj-C.
SpectarlDragon
Мне помогло решение с Cocoapods. Спасибо за статью :)
Mehdzor
Пожалуйста. А с инкрементальной компиляцией у вас не было проблем?
GYFK
У нас в проекте — никаких. Спасибо огромное :)