Если трижды подряд собрать один и тот же коммит на облачном Mac, первая сборка может пройти нормально, вторая — внезапно замедлиться, а третья — снова вернуться к обычной скорости. При этом процессор не загружен постоянно на 100 %, а загрузка данных по сети давно завершена. Такие колебания часто ошибочно принимают за нехватку ресурсов. На практике причиной может быть Spotlight, сканирующий DerivedData, редактор с рекурсивным наблюдением за всей рабочей областью или несколько заданий CI, одновременно записывающих данные в один каталог кеша. Главное в такой ситуации — не очищать сразу все кеши, а сохранить текущее состояние, измерить I/O и затем последовательно сузить область конкуренции.
Сначала создайте сопоставимую базовую линию сборки
Перед диагностикой зафиксируйте коммит, версию Xcode, цель сборки и пути кешей. Не объединяйте в одну выборку первичную загрузку зависимостей и последующие инкрементальные сборки. Также не запускайте сценарии очистки одновременно со сборкой.
set -o pipefail
WORKSPACE="$HOME/ci/work/app"
DERIVED="$HOME/ci/derived/app-main"
PACKAGES="$HOME/ci/packages/app"
mkdir -p "$DERIVED" "$PACKAGES"
cd "$WORKSPACE"
xcodebuild \
-resolvePackageDependencies \
-clonedSourcePackagesDirPath "$PACKAGES"
for run in 1 2 3; do
/usr/bin/time -lp xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath "$DERIVED" \
-clonedSourcePackagesDirPath "$PACKAGES" \
build 2>&1 | tee "$HOME/ci/build-$run.log"
done
Записывайте общее время выполнения, максимальный размер резидентной памяти, этап разрешения зависимостей в журнале и наличие других заданий в момент замедления. Если все три запуска медленные, обычно следует проверять проект или цепочку инструментов. Если выбиваются только отдельные запуски, вероятнее фоновая конкуренция за I/O.
| Наблюдение | Что проверить в первую очередь |
|---|---|
| Низкая загрузка CPU, но сборка останавливается | Ожидание файловой системы, блокировки каталогов |
Активны mds и mdworker |
Область индексирования Spotlight |
| После закрытия редактора скорость восстанавливается | Рекурсивное наблюдение за файлами |
| Проблема возникает только при параллельных заданиях | Общий каталог DerivedData или кеш пакетов |
Зафиксируйте I/O с помощью системных инструментов
Сначала проверьте состояние индексирования, а затем соберите данные о доступе к файлам во время аномально медленной сборки. Для fs_usage нужны права администратора. Поскольку команда выводит большой объем данных, сначала ограничьте набор процессов, а затем отфильтруйте результат по ключевым словам путей.
mdutil -s /
sudo fs_usage -w -f filesys mds mdworker_shared |
grep -E 'DerivedData|SourcePackages|/ci/work/'
В другом терминале проверьте связанные процессы:
ps -axo pid,ppid,%cpu,%mem,etime,command |
grep -E 'mds|mdworker|xcodebuild|swift-frontend|SourceKit' |
grep -v grep
Если mdworker лишь кратковременно читает недавно полученный исходный код, этого недостаточно, чтобы считать его первопричиной. Более весомое доказательство — непрерывное сканирование большого количества промежуточных файлов в период замедления, причем пути сканирования совпадают с путями, в которые записывает сборка.
Не выполняйте
rm -rf DerivedDataдо сбора диагностических данных. Очистка кеша может дополнительно замедлить следующую сборку и уничтожить сведения о том, какой процесс многократно обращался к конкретным файлам.
Ограничьте границы индексирования Spotlight
Не рекомендуется полностью отключать индексирование системного тома. Разработчикам может быть нужен системный поиск, а глобальное изменение скроет истинную проблему в организации каталогов. Надежнее вынести часто изменяемые и полностью восстанавливаемые кеши на отдельный том APFS и изменить настройки только для него.
Проверьте точку монтирования тома кеша и выполните:
mdutil -s /Volumes/CICache
sudo mdutil -i off /Volumes/CICache
mdutil -s /Volumes/CICache
Оставьте индексирование для исходного кода, документации и других материалов, которые должны находиться через поиск. На отдельный том перенесите DerivedData, кеш загрузки пакетов, вложения тестов и временные каталоги архивов. Чтобы впоследствии восстановить индексирование, выполните:
sudo mdutil -i on /Volumes/CICache
sudo mdutil -E /Volumes/CICache
Не помещайте материалы для подписи, долгосрочные артефакты и временные кеши в одну область очистки. Исключение из индекса устраняет только конкуренцию при сканировании и не заменяет контроль доступа и классификацию данных.
Настройте рекурсивные наблюдатели и общие кеши
Редакторы, генераторы кода и серверы разработки часто наблюдают за корневым каталогом репозитория. Если правила наблюдения охватывают .git, DerivedData, вложения тестов или кеш пакетов, каждая сборка может создавать десятки тысяч бесполезных событий.
Сузьте область наблюдения
Ограничьте наблюдение каталогами исходного кода и конфигурации, явно исключив:
.gitи временные файлы, создаваемые при получении рабочей копии;- DerivedData, архивы и каталоги результатов тестирования;
SourcePackagesи другие восстанавливаемые кеши зависимостей;- журналы, отчеты о покрытии и каталоги сгенерированного кода.
Сначала завершите работу подозрительного редактора или агента, а затем повторите тот же базовый тест. Если колебания исчезнут, возвращайте процессы по одному: так проще определить виновника, чем при одновременном изменении всех инструментов.
Выделите отдельные пути записи для параллельных заданий
Последовательные сборки могут повторно использовать кеш проекта. Однако в параллельном CI несколько заданий не должны записывать данные в один каталог DerivedData. Путь должен содержать как минимум идентификаторы репозитория и задания:
SAFE_REPO="${REPO_NAME//[^a-zA-Z0-9_-]/_}"
SAFE_JOB="${JOB_ID//[^a-zA-Z0-9_-]/_}"
export DERIVED_DATA="$HOME/ci/derived/$SAFE_REPO/$SAFE_JOB"
export PACKAGE_CACHE="$HOME/ci/packages/$SAFE_REPO"
mkdir -p "$DERIVED_DATA" "$PACKAGE_CACHE"
Кеш пакетов можно совместно использовать для чтения, но при обновлении зависимостей все равно возможна конкуренция за запись. В среде с высокой параллельностью зависимости можно сначала разрешить на контролируемом этапе, а затем запускать задания сборки со стабильным результатом разрешения.
Разделите очистку и регрессионную проверку
Сценарий очистки не должен выполняться одновременно с xcodebuild. Удаляйте устаревшие кеши по каталогам заданий, а не очищайте безусловно весь корневой каталог каждый день. Сначала выведите только список кандидатов:
DERIVED_ROOT="$HOME/ci/derived"
if ! pgrep -x xcodebuild >/dev/null; then
find "$DERIVED_ROOT" \
-mindepth 2 -maxdepth 2 \
-type d -mtime +7 -print
fi
Проверив уровень вложенности и срок хранения, вынесите удаление в отдельное задание. Для приемочного теста снова трижды запустите зафиксированную базовую сборку и одновременно наблюдайте за fs_usage. Успешный результат — не одна особенно быстрая сборка, а прекращение постоянного сканирования каталогов сборки индексирующими процессами, отсутствие общих путей записи у параллельных заданий и стабильное распределение времени между этапами последовательных сборок.
В завершение внесите в диагностическую запись версию Xcode, хеш коммита, путь DerivedData, путь кеша пакетов, список активных процессов наблюдения и состояние индексирования. При следующем замедлении можно будет сначала сопоставить различия в окружении, а не начинать очередные догадки с очистки кеша.
Часто задаваемые вопросы
Нужно ли отключать Spotlight на всём системном томе?
Нет, это не должно быть первым решением. Сначала подтвердите, что mds или mdworker постоянно обращается к каталогам сборки. После подтверждения меняйте индексирование только на отдельном томе кэша.
Можно ли параллельным задачам CI использовать общий DerivedData?
Для последовательных сборок одного проекта это допустимо. Параллельным задачам лучше выделять пути по репозиторию, ветке или идентификатору задания, чтобы исключить одновременную запись и конфликт блокировок.
Выберите облачный Mac для следующего конвейера разработки
Сравните три фиксированные конфигурации и четыре узла в Азии, а затем выберите срок аренды с учётом реальной нагрузки. Каждая аренда включает отдельную физическую машину, а не виртуальную.