デュアルプラットフォームのファンアウト · Gradle / CocoaPods / DerivedData の階層 · キューゲートと単一ノードへのフォールバック
小さなチームが複数の Mac Mesh ノードへ Flutter または React Native の Monorepo を載せるとき、キャッシュの踏み合い、キューの飢餓、リージョンをまたぐ依存取得でウォールタイムを失いがちです。本稿では 直列・デュアルノード・ファンアウト・プラットフォーム専用 の判定マトリクスを示し、Gradle・CocoaPods・DerivedData のキャッシュキーを読み取り専用の境界で分け、六段の再現可能ゲートと単一ノードへのフォールバックを添えます。Monorepo の影響範囲ビルドとリージョン横断の成果物ファンアウトとあわせて読むと整理しやすくなります。
Android と iOS を並列に回すことは、I/O をモデル化せずに 1 台に二本のパイプラインを載せることと同義ではありません。並列度の取り違えは、不安定さをコンパイラからスケジューラへ移します。次の兆候が出たら、並列を足す前にマトリクスへ立ち返ります。
Gradle と Xcode のディスク競合: 1 ノード上で ~/.gradle と DerivedData が同時に肥大化し、I/O 待ちがウォールタイムを支配します。
CocoaPods の解決と Android NDK のズレ: ロックファイルがキャッシュキーに入らないと、リモートでは成功するのに統合では失敗する偽陽性が出ます。
Metro とエミュレータが CPU を奪い合う: 対話的な RN 確認と無人の CI が同一コアを共有し、キュー深度は上がるのにスループットは下がります。
読み取り専用境界のないファンアウト: コンシューマが共有接頭辞を書き換え、成果物ポインタの入れ替えと競合します。
リージョン横断のキャッシュ取得に再試行予算がない: ネットワークの再試行が膨らみ、同時シートを飢餓させます。
各次元は マルチリージョンのリモート Mac プール 上のモバイル Monorepo を想定しており、ファンアウトを監査可能なフィールドに落とすことが目的です。リポジトリが重いネイティブモジュールも載せる場合は、先に 影響範囲ビルドのガイド のフルビルド短絡ルールに揃えます。
| 次元 | 直列を優先 | デュアル・ファンアウトを優先 | プラットフォーム別の専用ノードに分割 |
|---|---|---|---|
| 変更の形 | 単一プラットフォームのアセットや文言の微修正 | 両プラットフォームで依存やバイナリ面が同時に動く | Android はマルチ ABI 行列が必要で、iOS は Archive を並列に確保したい |
| キューの健全性 | 深度が安定し P95 のウォールタイムが予測できる | 待ち時間の合計がファンアウトの節約を上回る | 片側の Archive が他方の PR 検証を塞ぐ |
| キャッシュ方針 | ロックファイルとツールチェーン指紋をキーに含めた 1 系統の接頭辞 | プラットフォームごとに書き込み接頭辞を分け、コンシューマは読み取り専用 | 専用ノードは世代番号とリース ID を隔離して持つ |
| マルチリージョン | 単一リージョンで SLA を満たせる | プライマリが書き込み、サテライトは読み取り専用でミラー | サテライトはどちらの接頭辞も書き換えない |
| フォールバック | キュー過多やディスクアラートで直列へ合流 | 検証が連続して二度失敗したらファンアウトを凍結 | 大きな Xcode や AGP 移行中は単一プラットフォーム占有を強制 |
再現可能な投入物を固定してからデュアルプラットフォーム並列を議論します。順序を逆にすると、並列は並列の失敗に変わります。
ランナーが共有プールの信頼関係のもとで各ノードにログインできる前提とします。成果物が大洋をまたぐ場合は、成果物ファンアウトのガイドにあるマニフェストとリース項目も実装します。
五要素の投入バンドルを固定します: 短いコミットハッシュ、Podfile.lock / Gemfile.lock、Gradle ロック、xcodebuild -version、Android SDK と NDK のメジャーをパイプラインのヘッダに載せます。
書き込み接頭辞を分けます: IOS_CACHE_GEN と AND_CACHE_GEN を単調に保ち、プラットフォーム間で 1 つの書き込みディレクトリを共有しません。
ファンアウトを決めます: マトリクスのセルがデュアル・ファンアウトのときだけ並列ジョブグラフを組み、それ以外は直列のままにします。
プライマリが書き込み、コンシューマは読み取り専用です: tarball を書くのはプライマリだけとし、コンシューマは展開前にハッシュとサイズを検証します。
キュー予算です: ネットワークエラーには指数バックオフと上限を設け、上限を超えたらインシデントを起票してシートを解放します。
1 ノードへフォールバックします: ディスクまたはキューのしきい値を超えたら自動で直列に縮退し、事後分析用に FANOUT_DISABLED を付けます。
IOS_KEY="${CI_COMMIT_SHORT_SHA}:pods:${PODFILE_LOCK_SHA}:$(xcodebuild -version | shasum -a 256 | cut -c1-10)"
AND_KEY="${CI_COMMIT_SHORT_SHA}:gradle:${GRADLE_LOCK_SHA}:${ANDROID_SDK_MAJOR}"
export FASTLANE_SKIP_UPDATE_CHECK=1
echo "{\"ios\":\"$IOS_KEY\",\"android\":\"$AND_KEY\"}" > "${CI_PROJECT_DIR}/dual-cache.manifest"
注: キー項目はスタックに合わせた独自マニフェストへ差し替えて構いません。単調な世代 と プラットフォームごとの書き込み接頭辞 は維持し、コンシューマ側で依存グラフを再パースしないようにします。
下の数値は レビューとキャパシティの出発点 として扱い、実ビルドのヒストグラムとキュー指標へ置き換え、外部 SLA として固定しないでください。事後分析にはデュアルプラットフォームの P95、tarball 展開時間、シート保持時間をまとめて載せます。
FANOUT_DISABLED と人による確認を選びます。| チームの信号 | 初期の立て方 | 成果物ファンアウトとの関係 |
|---|---|---|
| コントリビュータが一桁 | 単一リージョンでデュアルプラットフォームは直列、ローカルキャッシュを併用 | ファンアウト需要は低く、キーも単純で足りる |
| マルチリージョンの PR | プライマリが両接頭辞を書き、サテライトは読み取り専用 | rsync やオブジェクトストレージのファンアウトと強く結びつく |
| 夜間の AI エージェントのバッチ | 重いコンパイル用ノードを対話プールから切り離す | 夜間ジョブが共有キー空間を壊すのを防ぐ |
警告: コンシューマが読み取り専用になる前にリージョン横断のファンアウトを広げると、半同期の問題が同時の偽陽性へ増幅します。
ノート PC で Android エミュレータと iOS の Archive を同時に回すと、スリープ、ロック画面、帯域の揺らぎで時間を失いがちです。純粋な SaaS のモバイルビルドファームは、ネイティブのデバッグや企業ネットワーク方針と衝突しやすくなります。デュアルプラットフォームの継続的デリバリー と 監査可能なノード役割 を 1 本のキャパシティとして語りたいチームは、自作スタックでは可観測性と権限境界の手当てに窮しやすくなります。
契約単位のノード、帯域の説明責任、リージョンの選択 が欲しいチームは、キュー方針が明示された発注可能なクラウド Mac でクリティカルなデュアルビルドを回すほうが速いことが多いです。クロスプラットフォームのデリバリーに夜間の AI エージェント負荷まで載せるなら、VpsMesh の Mac Mini クラウドレンタルがしばしば最適です。ノードはきれいに分かれ、リンクは監査しやすく、並列度はキュー深度と同じくらい測りやすくなります。
双方の脚でウォールタイムが伸びる、単一ノードの I/O やエミュレータと xcodebuild の競合が見える、キュー深度がしきい値を上回り続けるときはファンアウトを検討します。変更が単一プラットフォームに留まるときは直列のままにします。トリミングの考え方は Monorepo の影響範囲ビルドの記事 にまとまっています。
分離なしで書き込み可能な接頭辞を共有することは避けます。階層化したキーとディレクトリを用い、プライマリだけが書き込み、コンシューマは読み取り専用にします。リージョン横断のファンアウトは 成果物ファンアウトのマトリクス を読んでください。