Vuncloud ブログ
← フィールドノートへ戻る

iOS CIが遅い?GitHub Actionsでxcodebuildがブレる理由

フィールドノート · 同じPRが6分の日と18分の日 · cold/warmを先に分ける · 14日Shadow実測 · warm P95 14:12→6:05 ·約9分で読めます

Xcodeとターミナルが表示されたMac画面、GitHub Actions iOS CI xcodebuildの遅延を調査
TL;DR · まず見える化、それから手を入れる
  • 187 件の PR のうち、約 86.6% が warm——日常 SLA は warm を見る。cold を P95 に混ぜない
  • 依存更新・cache クリア・scheme 変更で「コールドスタート」——壁時計が 2–3 倍 ブレるのは普通
  • warm だけでも、cache key のミス・複数 job のディスク争奪・テストと署名の同居で、まだ揺れる
  • おおよその順序:キュー → cold/warm 分離 → cache → 並列制御 → 最後にハード(対照は ウォーターフォール拆解

完全な対照データ → GitHub Actions 最適化ピラー · Benchmark

86.6%
Shadow サンプルが warm build
14:12→6:05
warm P95(macos-latest → 専有 M4)
2–3×
cold vs warm の典型的な壁時計差

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 体験はそうでもない、というズレが生まれます。

分け方 · Shadow 対照で使った定義
  • 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 量が全然違う
開発者が GitHub Actions の iOS CI ビルドログを見ながら xcodebuild の warm と cold の時間差を分析している

3. 時間はどこで消えるか

workflow の各 step にタイムスタンプを付けると、壁時計はだいたい五つに割れます。warm 時の典型分布(プロジェクトで前後します):

iOS CI 壁時計五段(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 所要時間か timepod installxcodebuild buildxcodebuild 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 とリリースは別カウント

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 へ

キュー問題を除外(または小さい)したあと、この順で。いきなりハードの話から入らない:

  1. ビルドにタグ:cold か warm か(Podfile.lock 変わった? cache ヒット?)
  2. 日常 SLA は warm P95 のみ。cold は週次または別ライン
  3. DerivedData・Pods・SPM の cache パスと key を固定 — cache ベストプラクティス(準備中)
  4. macOS job を詰め込みすぎない:self-hosted は 1–2 並列、ホスト型も workspace 共有を避ける
  5. PR 検証とリリース/signing を分離。テストで merge 指標を汚さない
  6. cache までやっても足りないとき初めて M4 / M4 Pro — シリコンと工数
workflow 片段 · warm / cold タグ付け(示意)
- 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 不足は少ない。よく重なるのはこの三つ:

  1. cold と warm を混ぜて見て P95 が膨らんでいる
  2. ホスト型 job 終了でディスクが消え、cache が定着しない
  3. テスト・署名・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 特有の「毎回コールドスタート」が減ります。

Cloud Mac プランを見る · CI/CD 接続 FAQ

フィールドノート · iOS CI

速さと遅さを見極めてからチップを変える

cold/warm分離 · キャッシュ優先 · Shadowデュアルラン1週間

ピラーBenchmarkを読む
期間限定 プランを見る