エンジニアリング実践

Xcode SourceKitのインデックス停止とCPU負荷を診断する

Xcode SourceKitのインデックス停止とCPU負荷を診断する

大規模なXcodeワークスペースをリモートで開くと、目に見えるファンはなくても、ターミナル上のCPU使用率は上がり続けることがあります。コード補完は「インデックス作成中」のまま止まり、定義へ移動しても結果が返りません。このとき最も危険なのは、待つことではなく、開発ディレクトリ全体をいきなり削除することです。まず負荷の原因がビルドなのかSourceKitなのかを確認し、次に診断情報を保存して、最後に影響を受けたインデックスだけを再構築するのが正しい手順です。

正常な再構築と実際の停止を見分ける

SourceKitは、プロジェクトを初めて開いたとき、Xcodeのバージョンを切り替えたとき、依存関係のロックファイルを変更したとき、または多数のブランチを切り替えた後にインデックスを再構築します。短時間のCPU高負荷だけでは、障害とは判断できません。まず手動のビルドを停止し、ワークスペース内のファイルを変更せずに、プロセスのスナップショットを2〜3回続けて確認します。

date
ps -axo pid,ppid,%cpu,%mem,etime,command | grep -E 'SourceKit|sourcekitd|XCBBuildService|swift-frontend' | grep -v grep

特に確認すべき点は3つです。XCBBuildServiceまたはswift-frontendがまだコンパイルを続けているか、SourceKitプロセスの実行時間が増え続けているか、編集を止めた後も同じプロセス群にCPU負荷が長時間集中しているかです。ビルドプロセスが動作中なら、まずビルドの完了を待ち、コンパイル負荷をインデックス停止と誤認しないようにします。

停止の判定を1回のCPU使用率だけで行ってはいけません。ファイル集合が安定している、ビルドタスクがない、コード補完が長時間進まない、ログが同じモジュールを繰り返し示している、という4条件が同時にそろった場合にのみ、クリーンアップを検討します。

比較可能な診断情報を保存する

最初にXcodeを再起動してはいけません。プロセス一覧、ログ、コールスタックを保存する診断ディレクトリを作成しておけば、後から修復の効果を比較できます。

STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="$HOME/sourcekit-diagnostics/$STAMP"
mkdir -p "$OUT"
ps -axo pid,ppid,%cpu,%mem,etime,command > "$OUT/processes.txt"
log show --last 10m --style compact --predicate 'process CONTAINS[c] "SourceKit" OR process == "Xcode"' > "$OUT/sourcekit.log"
PID="$(pgrep -n -f 'SourceKit|sourcekitd')"
test -n "$PID" && sample "$PID" 10 -file "$OUT/sourcekit.sample.txt"

サンプリング中は、ブランチを切り替えたり、多数のファイルを一括変更したりしないでください。ログを確認するときは、繰り返し現れるモジュール名、読み取れないパス、プラグインの読み込み失敗、リクエストのキャンセルループを優先的に探します。コールスタックが同じSwiftファイルや同じ生成ファイルの解析処理に何度も到達している場合は、そのファイルがスクリプトによって継続的に書き換えられていないかを確認します。

生成ファイルによるループを除外する

よくある原因は、コードジェネレーターが実行のたびにタイムスタンプを書き換えることです。内容が変わっていなくても、インデクサーはそのファイルを再処理します。生成スクリプトでは、先に内容を比較し、変更がある場合だけアトミック置換で対象ファイルへ書き込むようにします。また、生成先ディレクトリがフォルダ参照とソースディレクトリの両方としてプロジェクトに追加されていないことも確認してください。

ワークスペースごとにDerivedDataを分離する

同じDerivedDataを共有すると、異なるブランチ、異なるXcodeバージョン、並列タスクが互いに影響します。クラウドMacでは、ワークスペースまたはCIタスクごとに明示的なパスを割り当てる方法がより安定します。

ROOT="$PWD"
DERIVED="$ROOT/.derived-data"
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -derivedDataPath "$DERIVED" \
  -showBuildSettings > "$ROOT/build-settings.txt"

.derived-dataはリポジトリにコミットしないでください。並列タスクではさらに、.derived-data/job-42のようなタスク識別子を追加し、2つのビルドが同じモジュールキャッシュへ同時に書き込まないようにします。

シナリオ DerivedDataの方針 インデックスの方針
リモートでの対話型開発 ワークスペース専用の固定ディレクトリ 有効のまま使用
単発の検証ビルド タスクごとに専用ディレクトリ 必要に応じて有効化
ビルドとテストのみを行うCI タスクごとに専用ディレクトリ インデックス書き込みを無効化可能
Xcodeバージョンの切り替え 新しいバージョン用ディレクトリを使用 再生成

コード補完が不要なCIタスクでは、コマンドラインにCOMPILER_INDEX_STORE_ENABLE=NOを追加できます。ただし、共有するプロジェクト設定へ無条件に書き込まないでください。開発者が定義への移動、診断、コード補完を利用できなくなります。

影響範囲に応じて段階的に復旧する

クリーンアップを始める前にワークスペースを閉じ、xcodebuildswift-frontend、SourceKitの各プロセスが対象ディレクトリを使用していないことを確認します。最初の段階では、インデックスとモジュールキャッシュだけを削除します。

DERIVED="$PWD/.derived-data"
rm -rf "$DERIVED/Index.noindex"
rm -rf "$DERIVED/ModuleCache.noindex"

プロジェクトを開き直した後は、ブランチと依存関係を変更せず、最初のインデックス作成が完了するまで待ちます。問題が再発した場合は、~/Library/Developer全体を消去するのではなく、現在のプロジェクト専用DerivedDataを削除します。それでも再発する場合は、保存した診断情報に戻り、特定のブランチでのみ発生するのか、生成ファイルが継続的に変化しているのか、コンパイラプラグインが現在のXcodeと互換性を持たないのかを確認します。

「マシンを再起動したら直った」を結論にしてはいけません。再起動はプロセスを終了させるだけであり、原因がキャッシュの破損、入力ファイルの変動、ツールチェーンの差異のどれだったかは判別できません。

再現可能な検証チェックリストを作成する

修復後は、同じコミット、同じXcodeパス、同じDerivedData方針で再検証します。次の結果を記録することを推奨します。

  1. xcodebuild -versionxcode-select -pが想定したツールチェーンを指している。
  2. ワークスペースを開いた後にインデックス作成が完了し、コード補完と定義への移動が復旧している。
  3. 編集とビルドを停止した後、SourceKitのCPU使用率が低下する。
  4. 同じファイルまたはモジュールがログに繰り返し現れない。
  5. ワークスペースを閉じて開き直す操作を連続して行っても、問題が再発しない。
  6. CIと対話型開発で異なるDerivedDataディレクトリを使用している。

NowMini上のリモートセッションでも、診断ディレクトリをビルドログとともに保存してください。ただし、ソースコードの本文、鍵、マスキングされていない環境変数は収集しないようにします。最終的な目標はCPU使用率を即座にゼロにすることではなく、インデックスへの入力が安定していること、プロセスが進行できること、そしてクリーンアップの影響が現在のプロジェクトだけに限定されていることを証明することです。

よくある質問

SourceKitのCPU使用率が高ければインデックス停止ですか?

必ずしも停止ではありません。大規模プロジェクトの初回オープン、Xcodeの切り替え、依存関係の更新後は高負荷になります。同じモジュールの処理が長時間反復し、補完も進まない場合に異常を疑います。

DerivedDataをすべて削除する必要がありますか?

通常は不要です。Xcodeとビルドを停止し、プロジェクト専用パスのIndex.noindexとModuleCache.noindexだけを削除します。再発時に限り、そのプロジェクトのDerivedDataを作り直します。

CIビルドではインデックス生成を無効にできますか?

できます。対話操作を伴わないビルドやテストにはCOMPILER_INDEX_STORE_ENABLE=NOを設定できます。開発セッションではナビゲーションと補完のため有効に保ちます。

NowMini M4

独自の物理ノードで次のタスクを実行

M4、16GB RAM、256GB SSDを搭載したクラウドMacを、シンガポール、東京、ソウル、香港のノードからタスク期間に合わせて選択できます。

クラウドMacを今すぐレンタル