tuist/argus を使って iOS アプリのビルドのボトルネックを見つける
このエントリーは with Advent Calendar 2025 の10日目になります。
普段は with の iOS アプリを開発している kymmt です。
このエントリーは、たまたまツイッターに流れてきた tuist/argus というツールを使って iOS アプリのビルド時におけるボトルネックを見つけてみよう!です。
あらかじめ書いておくと、見つけるまでで改善には至っておりません 🥺
背景
iOS アプリ開発においてよく課題に出やすいのがビルド時間です。
そこそこの規模のアプリならフルビルドに10分以上かかったり、差分ビルドでも場合によっては2分以上かかることは珍しくありません。
1行の変更を確かめるのに2分以上待つのは大変厳しいものがあります。
その解決になりそうなものとして、 Apple が Xcode Previews を出しました。
これを使うことでいちいちアプリをビルドして Simulator を見なくても、すぐに変更を確認できます。
しかし、この Xcode Previews もそこそこ頻繁に全体のビルドが走ります。
結果として、やはりビルド時間が iOS アプリ開発においてボトルネックになりやすいです。
しかし、 iOS アプリのビルド時間を改善するには普段の開発では使わない知識が求められ、難易度が高いです。
こういう知識が求められる場合は ChatGPT や Claude などの AI が強いですが、 xcodebuild のログを食わせようにもノイズが多すぎたり必要な情報が欠けてたりと使いものになりません。
そこで、 tuist/argus が役に立つ可能性があります。
tuist/argus とは?
tuist/argus は swift-build をカスタマイズしたものであり、 普通に xcodebuild しただけでは取得できない情報を取得できたり AI に食わせやすいように情報を整理してくれるツールです。
swift-build は Apple が 2025/2 に OSS として発表したビルドシステムであり、これまでは別々のビルドシステムを使っていた SwiftPM や Xcode(xcodebuild) を1つに統合したものです。
Xcode(xcodebuild) は内部で swift-buid を使っており、なんと環境変数によって差し替えることができます。
これを利用し、 Xcode の swift-build を tuist/argus に置き換えてビルド時の情報を収集することで、 AI に食わせやすい形に整理することができます。
tuist/argus の使い方
では実際に使ってみましょう!
tuist/argus は mise でインストールすることができます。
mise が入ってない人はインストールしましょう。
$ curl https://mise.run | sh
$ mise doctor # 問題があれば解決する
あとはコマンドを叩けば argus をインストールできます。
$ mise use -g github:tuist/argus
なお mise を使いたくない!って人は GitHub Releases から自分でバイナリを落としてパスを通すことでも使うことができそうです。
あとはお好みの AI Agent に使い方を教えてあげましょう。
私の場合はよく Claude Code を使うので、 tuist/argus の README.md を参考にしつつ以下のスラッシュコマンドを用意しました。
({} の部分はプロジェクト固有の値です。自分のプロジェクトで使う場合はプロジェクトに適した値にしてください)
.claude/commands/analyze-build.md
---
description: Analyze {プロジェクト名} Build
---
Check for build bottlenecks {プロジェクト名}.
Ouptut in Japanese to build-analysis.md
When running Xcode builds, use Argus to capture and analyze build data:
1. Run builds with Argus and a session ID for correlation:
```bash
SWB_BUILD_TRACE_ID=$(uuidgen) XCBBUILDSERVICE_PATH=$(which argus) SWB_BUILD_TRACE_ID=$SWB_BUILD_TRACE_ID xcodebuild build -scheme "{スキーマ名}" -workspace '{ワークスペースのパス}' -destination 'generic/platform=iOS'
```
2. Query build results using the session ID:
argus trace summary --build $SWB_BUILD_TRACE_ID
argus trace errors --build $SWB_BUILD_TRACE_ID
argus trace slowest-targets --build $SWB_BUILD_TRACE_ID --limit 5
argus trace bottlenecks --build $SWB_BUILD_TRACE_ID
Or use "latest" to query the most recent build:
argus trace summary --build latest
3. Use --json flag for programmatic access:
argus trace summary --build $SWB_BUILD_TRACE_ID --json
4. Run `argus trace --help` to discover all available commands.
プロジェクトのルートディレクトリで起動した Claude Code で /analyze-build を実行し 、build-analysis.md が出力されれば成功です!
なお1度 AI に実行させる前に
SWB_BUILD_TRACE_ID=$(uuidgen)
XCBBUILDSERVICE_PATH=$(which argus) SWB_BUILD_TRACE_ID=$BUILD_TRACE_ID xcodebuild build -scheme "{スキーマ名}" -workspace '{ワークスペースのパス}' -destination 'generic/platform=iOS'
部分を手元で実行してみることをおすすめします。
tuist/argus は swift-build を改造しているため、実行環境がシビアです。私は普段から mise を使っており、なぜかインストールしていた swift@5.9 のせいで失敗しました(アンインストールすることで解決)。
もしかしたら Xcode のバージョンでも失敗するかもしれないので、もし簡単に実行できなさそうでしたら諦めましょう 😇
Xcode16.4 で成功することは確認しています。
実際に使ってみた
ということで準備ができたら使ってみましょう!
やり方は簡単、使い方で用意したスラッシュコマンドを叩くだけです。
ということで実際に with iOS アプリのリポジトリで叩いてみました。
条件としては、~/Library/Developer/Xcode/DerivedData を吹っ飛ばした「フルビルド版」と、フルビルドした後にもう1度実行した「差分ビルド版」で検証しています。
(アプリに直接関係ありそうな値はマスクしています。公開しても問題なさそうですが 確認取るのがめんどかった )
フルビルド版
with_ios ビルド分析レポート
分析日時: 2025-12-01
スキーム: {スキーム名}
ビルドID: 1E631FB3-C44B-4218-B3D4-1427438CDE51
ビルドサマリー
| 項目 | 値 |
|---|---|
| ステータス | 成功 |
| 総ビルド時間 | {総ビルド時間} |
| ターゲット数 | {ターゲット数} |
| タスク数 | {タスク数} |
| エラー数 | 0 |
| 警告数 | {警告数} |
| キャッシュヒット率 | 0.0%(0/{タスク数}) |
最も遅いターゲット(Top 15)
| 順位 | ターゲット名 | パッケージ | 時間 | タスク数 |
|---|---|---|---|---|
| 1 | {メインターゲット} | {プロジェクト名} | {ビルド時間}s | {タスク数} |
| 2 | {依存ライブラリ1} | {依存ライブラリ1} | 374.00s | 25 |
| 3 | {依存ライブラリ1-1} | {依存ライブラリ1-1} | 369.00s | 23 |
| 4 | {依存ライブラリ2} | {依存ライブラリ2} | 363.00s | 23 |
| 5 | {依存ライブラリ3} | {依存ライブラリ3} | 359.00s | 23 |
| 6 | {依存ライブラリ3-1} | {依存ライブラリ3-1} | 358.00s | 23 |
| 7 | SwiftCompilerPlugin | swift-syntax | 354.00s | 40 |
| 8 | SwiftCompilerPluginMessageHandling | swift-syntax | 353.00s | 40 |
| 9 | SwiftSyntaxMacroExpansion | swift-syntax | 349.00s | 40 |
| 10 | SwiftSyntaxMacros | swift-syntax | 347.00s | 40 |
| 11 | SwiftSyntaxBuilder | swift-syntax | 346.00s | 40 |
| 12 | SwiftParserDiagnostics | swift-syntax | 341.00s | 40 |
| 13 | SwiftOperators | swift-syntax | 335.00s | 40 |
| 14 | SwiftParser | swift-syntax | 333.00s | 40 |
| 15 | SwiftDiagnostics | swift-syntax | 306.00s | 40 |
最も遅いタスク(Top 5)
| 順位 | タスク | 時間 | メモリ使用量 |
|---|---|---|---|
| 1 | SwiftSyntax コンパイル(70ファイル, arm64) | 288.00s | 3,536MB |
| 2 | SwiftSyntax コンパイル(70ファイル, x86_64) | 287.00s | 3,424MB |
| 3 | {依存ライブラリ4} コンパイル(283ファイル, arm64) | 95.00s | 1,007MB |
| 4 | アセットカタログのコンパイル | {時間}s | {容量}MB |
| 5 | アプリ本体コンパイル({ファイル数}ファイル) | {時間}s | {容量}MB |
並列化ボトルネック
以下のターゲットがビルドの並列実行を妨げています:
1. SwiftSyntax 依存チェーン(最大のボトルネック)
| ターゲット | ブロック時間 | ブロックしたターゲット数 |
|---|---|---|
| SwiftSyntax | 304.00s | 12ターゲット |
| SwiftParser | 333.00s | 9ターゲット |
| SwiftDiagnostics | 306.00s | 9ターゲット |
| SwiftBasicFormat | 306.00s | 8ターゲット |
| SwiftParserDiagnostics | 341.00s | 7ターゲット |
| SwiftSyntaxBuilder | 346.00s | 6ターゲット |
| SwiftSyntaxMacros | 347.00s | 5ターゲット |
| SwiftOperators | 335.00s | 4ターゲット |
2. Firebase 依存チェーン
| ターゲット | ブロック時間 | ブロックしたターゲット数 |
|---|---|---|
| FirebaseCore | 119.00s | 19ターゲット |
3. アプリ内部モジュール
| ターゲット | ブロック時間 | ブロックしたターゲット数 |
|---|---|---|
| {内部モジュール} | {時間}s | {ターゲット数}ターゲット |
クリティカルパス
ビルドの最長実行パス(合計: 2,744秒):
_SwiftSyntaxCShims (17s)
→ SwiftSyntax (304s)
→ SwiftParser (333s)
→ SwiftParserDiagnostics (341s)
→ SwiftSyntaxBuilder (346s)
→ SwiftSyntaxMacros (347s)
→ SwiftSyntaxMacroExpansion (349s)
→ SwiftCompilerPluginMessageHandling (353s)
→ SwiftCompilerPlugin (354s)
改善提案
優先度: 高
1. SwiftSyntax/Swift Macros のビルド時間削減
- 問題: SwiftSyntax 関連のビルドがクリティカルパスの大部分を占め、合計 2,744秒
- 原因: Swift Macros(Verge, StateStruct)が SwiftSyntax に依存
-
対策案:
- プリビルドされた SwiftSyntax バイナリの使用を検討
- マクロの使用を必要最小限に抑える
- CI環境でのSPMキャッシュ活用
2. キャッシュヒット率の改善
- 問題: キャッシュヒット率が 0%
-
対策案:
- Derived Data のキャッシュを有効活用
- CI環境でのビルドキャッシュ設定
- 増分ビルドの活用
優先度: 中
3. {依存ライブラリ1} パッケージの最適化
- 問題: {依存ライブラリ1-1} が合計 743秒
-
対策案:
- 使用していない機能のインポート削減
- 必要に応じて軽量な代替ライブラリの検討
まとめ
- 総ビルド時間: {総ビルド時間}秒
- 主要ボトルネック: SwiftSyntax 依存チェーン(Swift Macros使用による)
- 最大改善ポイント: SwiftSyntax ビルドの最適化とキャッシュ活用
ビルド時間の約50%以上がSwiftSyntax関連のコンパイルに費やされています。Swift Macrosの恩恵とビルド時間のトレードオフを検討し、CIでのキャッシュ戦略を強化することで大幅な改善が期待できます。
差分ビルド版
{アプリ} ビルドパフォーマンス分析
ビルド概要
| 項目 | 値 |
|---|---|
| ビルドID | EA689800-0136-443E-93BB-6408BC4E92B7 |
| ステータス | 成功 |
| 総ビルド時間 | {総ビルド時間}秒 |
| ターゲット数 | {ターゲット数} |
| タスク数 | {タスク数} |
| エラー数 | 0 |
| 警告数 | {警告数} |
| キャッシュヒット率 | 71.3%({ヒットした数}/{タスク数}) |
クリティカルパス分析
ビルドの最長経路(クリティカルパス)は以下の通りです:
{モジュール1} → {モジュール2} → {モジュール3} → {モジュール4} → {アプリ}
クリティカルパス合計時間: {ビルド時間}秒
これはビルドの理論的な最小時間を示しています。並列化の限界がこのパスによって決まります。
最も遅いターゲット Top 10
| 順位 | ターゲット | 所要時間 | タスク数 |
|---|---|---|---|
| 1 | {アプリ名} (メインアプリ) | {ビルド時間}秒 | {タスク数} |
| 2 | {モジュール5} | {ビルド時間}秒 | {タスク数} |
| 3 | {モジュール6} | {ビルド時間}秒 | {タスク数} |
| 4 | {モジュール4} | {ビルド時間}秒 | {タスク数} |
| 5 | {モジュール7} | {ビルド時間}秒 | {タスク数} |
| 6 | {モジュール2} | {ビルド時間}秒 | {タスク数} |
| 7 | {モジュール8} | {ビルド時間}秒 | {タスク数} |
| 8 | {モジュール3} | {ビルド時間}秒 | {タスク数} |
| 9 | {モジュール1} | {ビルド時間}秒 | {タスク数} |
| 10 | {ライブラリ} | {ビルド時間}秒 | {タスク数} |
並列化ボトルネック
以下のターゲットが他のターゲットのビルドをブロックしています:
重大なボトルネック
| ターゲット | 所要時間 | ブロックしているターゲット数 | ブロック対象 |
|---|---|---|---|
| {モジュール1} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール2} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール6} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール3} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール5} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール4} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール7} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
| {モジュール8} | {ビルド時間}秒 | {ブロック数} | {モジュールたち} |
分析ポイント
-
{モジュール1}モジュールが最も多くのターゲットをブロックしています({モジュール数}ターゲット)。このモジュールのビルド最適化が最も効果的です。
-
{モジュール2}モジュールも{ターゲット数}つのターゲットをブロックしており、改善の優先度が高いです。
-
フレームワーク間の依存関係チェーンが長いため、並列化の恩恵を受けにくい構造になっています。
最も遅いコンパイルタスク
バッチコンパイル(複数ファイル同時コンパイル)
| 順位 | タスク | 所要時間 | メモリ使用量 |
|---|---|---|---|
| 1 | Swiftバッチコンパイル({ファイル数}ファイル) | {ビルド時間}秒 | {使用量} MB |
| 2 | Swiftモジュールエミット(メインアプリ) | {ビルド時間}秒 | {使用量} MB |
| 3 | Swiftバッチコンパイル({ファイル数}ファイル) | {ビルド時間}秒 | {使用量} MB |
単一ファイルコンパイル(上位のみ)
以下のファイルが単独で43秒かかっています(バッチの一部):
- {ファイル1}.swift
- {ファイル2}.swift
- {ファイル3}.swift
- {ファイル4}.swift
- {ファイル5}.swift
- {ファイル6}.swift
- R.generated.swift
R.swiftスクリプト
- R.swift generate resources for framework {モジュール1}: 21秒(114.7 MB)
改善提案
優先度: 高
-
{モジュール1}モジュールの分割検討
- {ターゲット数}つのターゲットをブロックしているため、依存関係を見直し、必要に応じてモジュールを分割
- ビルド時間: {ビルド時間}秒 → 目標: {約30%減}秒以下
-
R.swiftの最適化
- {生成時間}秒かかっているR.swift生成を最適化
- 不要なリソース参照の削除
- 増分ビルドの活用確認
-
Swiftモジュールエミットの改善
- {メインアプリ}のモジュールエミットに{エミット時間}秒・1GB以上のメモリを使用
- メインアプリターゲットのファイル数削減(現在{タスク数}タスク)
- 機能ごとのフレームワーク分割を検討
優先度: 中
-
{モジュール2}モジュールの最適化
- {ターゲット数}ターゲットをブロック
- インターフェースの簡素化で再ビルドを削減
優先度: 低
-
キャッシュヒット率の向上
- 現在71.3%のヒット率をさらに向上
- 頻繁に変更されるファイルの依存関係を見直し
-
フレームワーク依存関係の最適化
- クリティカルパス: {モジュール1} → {モジュール2} → {モジュール3} → {モジュール4} → {アプリ}
- この依存チェーンを短縮できるか検討
まとめ
{アプリ}のビルドは{ビルド時間}秒で完了していますが、以下の改善により{20%}-{30%}秒程度の短縮が期待できます:
- {モジュール1}/{モジュール2}モジュールの最適化 - 並列化ボトルネックの解消
- R.swift生成の高速化 - {ビルド時間}秒の削減可能性
- メインアプリターゲットの分割 - {ビルド時間}秒のビルド時間を分散
キャッシュヒット率71.3%は良好ですが、クリーンビルドや新機能開発時にはボトルネックが顕著になるため、上記の改善を段階的に実施することを推奨します。
感想
with iOS アプリではモジュールの分割を行っていますが、これによりクリティカルパスが発生してモジュール分割の旨みが減るというのは目から鱗でした。
またライブラリやモジュールごとにビルド時間が出ることで、意外なところで時間がかかっていることがわかったのもよかったです。
今後の展望
tuist/argus を試したのはつい最近なのでまだ実際に改善に移れていないですが、この結果を見て次のようなアクションを思いつきました。
- Xcode26 にアップグレードして prebuild な SwiftSyntax を試すことでフルビルド時間が削減できないか検証する
- モジュールの依存関係の見直し
- クリティカルパスが長くなっているところを短くできないか検討する
- R.swift の見直し
- 地味に色々時間がかかっている
- String Catalogs などを使えないか検討する
- モジュール1 の最適化
- モジュール1 はいろんなターゲットから依存されている
- モジュール1 が遅いといろんなターゲットが遅くなるので不要なファイルがないかなどの最適化を行う
まとめ
tuist/argus を使ってビルドのボトルネックを探してみました。
以前まではビルドログなどを睨めっこして探していましたが今後は AI が代わりに見つけて改善を提案してくれる、そんな未来が見えました。
もしビルド時間が増えているが原因がわからない!という方がいれば、一度試してみるといいかもしれません。
Discussion