- Из 187 PR — 86,6 % warm: в SLA смотрите warm, не смешивайте cold в P95
- Смена зависимостей, сброс cache, другой scheme → cold; разброс 2–3× по wall clock — норма
- Даже при warm: cache miss, несколько job'ов на одном диске, тесты + подпись в одном job — всё ещё «плавает»
- Грубый порядок: очередь → разделить cold/warm → cache → concurrency → железо в конце (водопад времени)
Полные данные → Pillar · Benchmark
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 в быту говорит иначе.
- 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
3. Куда уходит время
Проставьте таймстемпы по step'ам workflow — wall clock делится на пять блоков. Распределение warm (зависит от проекта):
① 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: 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
После очереди (или если она мала) — не начинать с покупки:
- тегировать сборки: cold vs warm (
Podfile.lock, cache hit) - SLA только warm P95; cold — еженедельно или отдельная линия
- стратегия cache DerivedData, Pods, SPM — best practices cache (в подготовке)
- не перегружать macOS job'ы: self-hosted 1–2 параллельно; hosted — меньше общего workspace
- разделить PR-валидацию и release/signing
- cache не хватает → M4 / M4 Pro — чип и инженерные часы
- 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. Чаще накладывается:
- cold и warm в одном P95
- hosted job чистит диск → cache не держится
- тесты, подпись и 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.