Vuncloud Блог
← К полевым заметкам

iOS CI тормозит? Почему xcodebuild на GitHub Actions «плавает»

Полевые заметки · Один PR: 6 мин, потом 18 · сначала cold/warm · Shadow 14 дней · warm P95 14:12→6:05 ·~9 мин чтения

Экран Mac с Xcode и терминалом — диагностика медленного iOS CI xcodebuild на GitHub Actions
TL;DR · Сначала разобраться, потом чинить
  • Из 187 PR86,6 % warm: в SLA смотрите warm, не смешивайте cold в P95
  • Смена зависимостей, сброс cache, другой scheme → cold; разброс 2–3× по wall clock — норма
  • Даже при warm: cache miss, несколько job'ов на одном диске, тесты + подпись в одном job — всё ещё «плавает»
  • Грубый порядок: очередь → разделить cold/warm → cache → concurrency → железо в конце (водопад времени)

Полные данные → Pillar · Benchmark

86,6%
Shadow-выборка: warm build
14:12→6:05
warm P95 (macos-latest → выдел. M4)
2–3×
типичный разрыв cold vs warm

1. CI как лотерея: тот же commit, другое время

Если вы искали iOS CI тормозит? Почему xcodebuild на GitHub Actions «плавает», эти картины знакомы:

  • Тот же commit, re-run — wall clock 2–3× разница
  • P95 на дашборде красный, ощущение команды: «merge обычно не так долго ждём»
  • pod install то 30 секунд, то «завис»
  • В пятницу после обеда merge не жмут — боятся медленного прогона

Не всегда не хватает CPU. В 14-дневном Shadow с командами чаще: цифры смешали, среда нестабильна — cache не держится, диск в contention. Смена чипа — в самом конце.

Эта полевая заметка про одно: почему сборки скачут. Job в очереди? → статья про очередь. Cache, купить vs аренда → cluster cache и ROI.

2. cold и warm: не в одну кучу

Классическая ловушка: все времена сборок в один P95. Редкие cold тянут хвост — данные кричат «каждый день медленно», merge в быту говорит иначе.

Определения · Shadow-сравнение
  • warm: зависимости те же, cache на месте, scheme тот же — в основном инкрементальная компиляция; это «обычный merge»
  • cold: lockfile изменился, cache сброшен, новый target/scheme, первый прогон на runner — resolve, pod install, массовая пересборка

SLA: warm P50/P95. cold — отдельная линия, не привязывать к ощущению merge.

2.1 Когда становится cold

На macos-latest cold чаще — workspace после job чистят, cache не «приживается»:

Триггер тип. добавка маркеры в логе
изменён Podfile.lock +3–8 мин pod install, Downloading dependencies
DerivedData miss / сброс +5–15 мин CompileSwift, полная пересборка .o
смена scheme / target +2–10 мин другой xcodebuild -scheme
изменился SPM resolve +1–5 мин Resolve Package Graph
minor-апгрейд Xcode (macos-latest) первая сборка +10–20 мин новый SDK / пересборка module cache

Частые Pod-апгрейды → больше cold. Не деградация железа — другая работа на этой неделе. Отчитывайте «неделю Pod» и «обычную dev-неделю» раздельно.

2.2 Почему warm всё равно нестабилен

Даже только warm: wall clock может гулять ~30 %. Часто:

  • cache miss: ключ без arm64, ветка не привязана, job'ы делят слот
  • много macOS job'ов в org → диск и сеть тормозят друг друга
  • то только main, то полные unit+UI — разная нагрузка
  • объём изменений: исходники Pod vs одна строка SwiftUI — разный compile
Разработчик смотрит логи CI iOS в GitHub Actions и разбирает разницу warm/cold у xcodebuild

3. Куда уходит время

Проставьте таймстемпы по step'ам workflow — wall clock делится на пять блоков. Распределение warm (зависит от проекта):

iOS CI wall clock 5 segments (warm · illustrative)
① checkout + env setup        ~0:30 – 1:30
② pod install / SPM resolve   ~0:30 – 2:00   (cold ↑↑)
③ xcodebuild compile+link      ~3:00 – 8:00   (объём изменений кода)
④ tests (simulator / unit)    ~1:00 – 6:00   (опционально, часто недооценено)
⑤ archive + codesign           ~1:00 – 4:00   (release pipeline)

warm P50 total typical: 6 – 14 min

На практике: длительность step'ов или time для pod install, xcodebuild build, xcodebuild test. Блок ② >5 мин → cold/cache; ③ скачет → DerivedData/concurrency; ④ всегда длинный → вынести тесты или nightly.

4. Тесты и подпись: скрытые тормоза

Много «xcodebuild медленный» на деле — тесты или подпись в той же сборке:

  • cold start симулятора: первый boot на CI — без прогрева каждый job заново
  • UI tests на порядок медленнее unit; в одном job с compile → некрасивый P95
  • сертификаты, Keychain, profiles — на hosted runner часто настраивают каждый job
  • archive/IPA — release pipeline, не PR-валидация
Короткий совет · PR и release отдельно

PR: build + лёгкие тесты, warm P95.
TestFlight/release: отдельный workflow и метрика. Вместе → «сколько ждём merge» никогда не сойдётся.

5. 14 дней Shadow

Двойной трек: macos-latest и выделенный Mac mini M4, по 187 PR, Xcode 16.2 / CocoaPods 1.15.2 выровнены. Методика: benchmark pillar.

класс выборка доля macos-latest P95 выдел. M4 P95
warm build 162 86,6% 14:12 6:05
cold build 25 13,4% 19:40 11:20
смешано (частая ошибка) 187 100% ~16:00+ ~7:30+

cold+warm в одном P95 → повседневный опыт завышен ~15–25 % → «срочно покупать железо». Лучше: merge = warm, недели Pod = cold отдельно.

На выделенном M4 warm всё равно быстрее cold — DerivedData/Pods с постоянным «домом» окупается. Подробнее: cluster cache.

6. Порядок: ясность → cache

После очереди (или если она мала) — не начинать с покупки:

  1. тегировать сборки: cold vs warm (Podfile.lock, cache hit)
  2. SLA только warm P95; cold — еженедельно или отдельная линия
  3. стратегия cache DerivedData, Pods, SPM — best practices cache (в подготовке)
  4. не перегружать macOS job'ы: self-hosted 1–2 параллельно; hosted — меньше общего workspace
  5. разделить PR-валидацию и release/signing
  6. cache не хватает → M4 / M4 Pro — чип и инженерные часы
workflow snippet · warm / cold tag (illustrative)
- name: Classify build type
  run: |
    if git diff --name-only HEAD~1 | grep -q Podfile.lock; then
      echo "BUILD_TYPE=cold" >> $GITHUB_ENV
    else
      echo "BUILD_TYPE=warm" >> $GITHUB_ENV
    fi

- name: Record wall clock
  run: |
    echo "build_type=${BUILD_TYPE}" >> metrics.csv
    echo "wall_sec=$(( $(date +%s) - START ))" >> metrics.csv

7. FAQ

2–3× на том же commit — нормально?

На macos-latest да: один раз полный cache hit, другой miss плюс очередь. Сначала сравните pod install и логи cache, не меняйте чип сразу.

Как правильно считать P95?

Ощущение merge: P95 warm, ~30 сэмплов за две недели. cold отдельно — вместе искажает закупки.

pod install всегда медленный — вина CocoaPods?

На hosted runner чаще: Pods без постоянного места, cache key без ветки/архитектуры. Self-hosted с фиксированным диском: warm часто <30 сек.

Mac mini M4 уберёт разброс?

warm P95 −57 %, σ −40 % — но без разделения cold/warm и дизайна cache пики останутся и на выделенной машине.

Очередь или сама сборка?

Начало лога: Waiting for a runner. Очередь в wall clock, не в минутах GitHub. Очередь ≈0, а медленно — сторона сборки этой заметки → cache.

Читать дальше?

YAML cache → cluster cache; купить vs аренда → ROI и 500 сборок/месяц.

8. Итог

«iOS CI медленный» на GitHub Actions редко сводится к CPU. Чаще накладывается:

  1. cold и warm в одном P95
  2. hosted job чистит диск → cache не держится
  3. тесты, подпись и PR-build в одном job

Сначала честный warm P95, потом cache → concurrency → железо. За 14 дней Shadow метрики + cache хватили, чтобы warm P95 ушёл с 14:12 до 6:05 — без поспешного решения о покупке.

Нужен постоянный «дом» для DerivedData?

Vuncloud Cloud Mac M4 Pro: диск данных 1 ТБ, actions-runner предустановлен. DerivedData / Pods остаются на диске — меньше «каждый run cold», как на hosted runner.

Тарифы Cloud Mac · FAQ подключения CI/CD

Полевые заметки · iOS CI

Сначала разделить быстрое и медленное, потом менять чип

cold/warm отдельно · cache первым · Shadow dual-run неделю

Читать pillar Benchmark
Ограниченное предложение Смотреть тарифы