- 187 件の PR のうち、約 86.6% が warm——日常 SLA は warm を見る。cold を P95 に混ぜない
- 依存更新・cache クリア・scheme 変更で「コールドスタート」——壁時計が 2–3 倍 ブレるのは普通
- warm だけでも、cache key のミス・複数 job のディスク争奪・テストと署名の同居で、まだ揺れる
- おおよその順序:キュー → cold/warm 分離 → cache → 並列制御 → 最後にハード(対照は ウォーターフォール拆解)
完全な対照データ → GitHub Actions 最適化ピラー · Benchmark
1. CI が抽選みたい:同一 commit で数倍
GitHub Actions で iOS CI を回していると、こんな画面、見覚えがあるはずです:
- 同じ commit を再実行すると、壁時計が 2–3 倍 変わる
- ダッシュボードの P95 は真っ赤なのに、体感は「普段 merge してもそんなに待たない」
pod installが 30 秒のときと、止まったように見えるときがある- 金曜午後は merge をためらう——CI がまた「遅い方」を引くのでは、と
必ずしも CPU が足りないわけではありません。チームと 14 日間 Shadow 二重軌道を走らせた経験では、もっと多いのは数字の混ぜ方と環境の不安定さ——cache が残らない、ディスクを奪い合う——体感が抽選になるパターンです。チップ換装はだいたい最後の話。
この記事はひとつだけ:ビルドがなぜ速くなったり遅くなったりするか。job がキューで runner を待っている? → キュー対策。cache の設計、買うか借りるかは cache 深掘り と ROI に任せます。
2. cold と warm:一つの鍋に混ぜない
よくある落とし穴:毎回のビルド時間を全部ひとつの P95 に入れること。たまに「コールドスタート」が混ざると、テールが一気に伸びる——データ上は「毎日遅い」、実際の merge 体験はそうでもない、というズレが生まれます。
- warm:依存そのまま、cache 残存、scheme 不変——だいたい増分コンパイル。「日常 merge」に近い
- cold:ロックファイル変更、cache クリア、target/scheme 変更、runner 上の初回——resolve・Pod 再インストール・広域リビルドが必要
日常 SLA は warm の P50/P95 で十分。cold は別ラインで。merge 体験と混ぜない。
2.1 いつ「コールドスタート」になるか
macos-latest では cold が多め——job 終了後 workspace が消え、cache が「定着」しにくいからです:
| トリガー | 典型的な追加時間 | ログでよく見るキーワード |
|---|---|---|
Podfile.lock 変更 |
+3–8 分 | pod install、Downloading dependencies |
| DerivedData 未ヒット / 削除 | +5–15 分 | CompileSwift、.o 全量再生成 |
| scheme / 新 target 切替 | +2–10 分 | 別の xcodebuild -scheme |
| SPM 依存グラフ変更 | +1–5 分 | Resolve Package Graph |
| Xcode マイナー版の受動アップグレード(macos-latest) | 初回ビルド +10–20 分 | 新 SDK / モジュール cache 再構築 |
依存をよく上げる週は cold が増える——マシンが劣化したわけではなく、その週やっている仕事が違うだけ。報告では「Pod を上げた週」と「通常開発週」を分けると、数字が通りやすくなります。
2.2 warm でもなぜ不安定か
warm だけでも、壁時計は上下に 3 割くらい揺れることがあります。よくある原因:
- cache ミス:key に
arm64がない、ブランチ未バインド、複数 job が同じスロットを奪い合う - 同一 org で macOS job が集中し、ディスクとネットワークを奪い合う
- main だけ compile の回と、unit + UI test 全量の回——そもそも仕事量が違う
- 変更面の差:Pod ソース触り vs SwiftUI プレビュー 1 行——compile 量が全然違う
3. 時間はどこで消えるか
workflow の各 step にタイムスタンプを付けると、壁時計はだいたい五つに割れます。warm 時の典型分布(プロジェクトで前後します):
① checkout + 環境準備 ~0:30 – 1:30 ② pod install / SPM resolve ~0:30 – 2:00 (cold 時 ↑↑) ③ xcodebuild compile+link ~3:00 – 8:00 (変更面で決まる) ④ テスト(simulator / unit) ~1:00 – 6:00 (任意、過小評価されがち) ⑤ archive + codesign ~1:00 – 4:00 (リリース pipeline) warm P50 のよくあるレンジ:6 – 14 分
実務では step 所要時間か time で pod install、xcodebuild build、xcodebuild test を分けて見る。② が 5 分超えがちなら cold と cache を疑う。③ が揺れるなら DerivedData と並列。④ が常に遅いならテストを切り出すか nightly 全量へ。
4. テストと署名:見えない足かせ
「xcodebuild が遅い」という報告の多くは、テストか署名が同じ build に乗っているケースです:
- Simulator コールドスタート:CI 上の初回起動は数分待つことも。ウォームアップなしだと job ごとにやり直し
- UI Test は unit の桁違いに遅い。compile と同じ job だと P95 が読めない
- 証明書・Keychain・Profile 取得——ホスト型 runner では job ごとに再セットアップが多い
- Archive・IPA はリリース pipeline の話。PR 検証の時間と混ぜない
PR:build + 軽量テスト、warm P95 を見る。
TestFlight / リリース:別 workflow、別指標。ひとつに混ぜると「普段 merge に何分?」が永遠に合わない。
5. 14 日 Shadow 実測
二重軌道対照:macos-latest と専有 Mac mini M4 で各 187 PR、Xcode 16.2・CocoaPods 1.15.2 を揃え。完全な手法 → ピラー Benchmark。
| 分類 | サンプル数 | 割合 | 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、依存更新週は cold を別期待。
専有 M4 でも warm は cold よりずっと速い——DerivedData と Pods に固定の「居場所」を与える価値が大きい。cache YAML の詳細は別記事(cache 深掘り)。
6. どう並べるか:見える化から cache へ
キュー問題を除外(または小さい)したあと、この順で。いきなりハードの話から入らない:
- ビルドにタグ:cold か warm か(Podfile.lock 変わった? cache ヒット?)
- 日常 SLA は warm P95 のみ。cold は週次または別ライン
- DerivedData・Pods・SPM の cache パスと key を固定 — cache ベストプラクティス(準備中)
- macOS job を詰め込みすぎない:self-hosted は 1–2 並列、ホスト型も workspace 共有を避ける
- PR 検証とリリース/signing を分離。テストで merge 指標を汚さない
- 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)
同一 commit の再実行で 2–3 倍差——普通?
macos-latest ではよくある話。一回 cache 全ヒット、次は miss + キュー——別物の build です。まず pod install と cache hit ログを比較。チップ換装はそのあと。
P95 はどう計算するのが正しい?
日常 merge の体感は warm の P95。2 週間で 30 サンプルくらいあると安定。cold は別集計か回数だけ——混ぜると調達判断がブレる。
pod install が毎回遅い——CocoaPods のせい?
ホスト型 runner では、Pods の居場所がなく、cache key がブランチ・アーキテクチャと未連動なことが多い。self-hosted で固定ディスクを指すと、warm 時 30 秒以内は珍しくない。
Mac mini M4 に替えればブレは消える?
対照では warm P95 が大きく下がる(−57%)、σ も −40% 程度。ただしcold/warm を分けず cache も未設計なら、専有機でもスパイクは残る。
遅いのはキュー? build 本体?
ログ冒頭の Waiting for a runner が何分かを見る。キューは壁時計に入るが GitHub 分課金には入らない。キューほぼゼロでまだ遅いなら build 側——この記事 → 次は cache。
続きを読むなら
cache YAML と key 設計 → cache 深掘り;買うか借りるか → ROI モデル と 月 500 ビルドの選び方。
8. まとめ
GitHub Actions の iOS CI が「遅い」とき、単純な CPU 不足は少ない。よく重なるのはこの三つ:
- cold と warm を混ぜて見て P95 が膨らんでいる
- ホスト型 job 終了でディスクが消え、cache が定着しない
- テスト・署名・PR build が 1 job に同居している
まず warm P95 をはっきりさせ、次に cache → 並列 → ハード。14 日 Shadow では、指標と cache だけで warm P95 を 14:12 から 6:05 まで下げられた——マシン購入を先に決める必要はない。
DerivedData に固定の「居場所」が欲しい?
Vuncloud Cloud Mac M4 Pro は 1TB データディスク付き、actions-runner プリインストール。DerivedData / Pods をディスクに残せるので、ホスト型 runner 特有の「毎回コールドスタート」が減ります。