工程實務

雲端 Mac 診斷 Xcode SourceKit 索引停滯與異常 CPU 佔用

雲端 Mac 診斷 Xcode SourceKit 索引停滯與異常 CPU 佔用

遠端開啟大型 Xcode 工作區後,雖然看不到風扇運轉,終端機裡的 CPU 曲線卻持續攀升;程式碼補全停在「正在建立索引」,跳至定義也沒有結果。此時最危險的做法不是繼續等待,而是直接刪除整個開發目錄。正確順序應是先確認負載來自編譯還是 SourceKit,再保留現場證據,最後只重建受影響的索引。

先區分正常重建與真正停滯

首次開啟專案、切換 Xcode 版本、修改相依套件鎖定檔,或在差異較大的分支間切換後,SourceKit 都會重建索引。短時間 CPU 佔用偏高並不代表發生故障。先停止主動建置,保持工作區檔案不變,連續觀察兩到三次程序快照:

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

重點觀察三件事:XCBBuildServiceswift-frontend 是否仍在編譯;SourceKit 程序的執行時間是否持續增加;停止編輯後,CPU 是否仍長時間集中在同一組程序。若建置程序仍在運作,應先讓建置完成,不能把編譯負載誤判成索引停滯。

判斷是否停滯不能只看單次 CPU 數值。只有在檔案集合已穩定、沒有建置工作、程式碼補全長時間毫無進展,且日誌反覆指向同一個模組時,才值得進行清理。

保留可供比較的現場證據

不要一開始就重新啟動 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,避免兩個建置程序同時寫入模組快取。

情境 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"

重新開啟專案後,保持分支與相依套件不變,等待第一次索引完成。若問題再次出現,再刪除目前專案的獨立 DerivedData,而不是清空 ~/Library/Developer。如果問題仍會重現,應回到證據層檢查:是否只有特定分支會觸發、是否有某個產生檔案持續變動,或是否有編譯器外掛與目前的 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