工程实践

云端 Mac 上排查 Xcode SourceKit 索引卡死与异常 CPU 占用

云端 Mac 上排查 Xcode SourceKit 索引卡死与异常 CPU 占用

远程打开一个大型 Xcode 工作区后,风扇不可见,但终端里的 CPU 曲线持续拉高;代码补全停在“正在索引”,跳转定义也没有结果。此时最危险的动作不是等待,而是直接删除整个开发目录。正确顺序应当是先确认负载来自编译还是 SourceKit,再保存现场,最后只重建受影响的索引。

先区分正常重建与真正卡死

SourceKit 在首次打开工程、切换 Xcode 版本、修改依赖锁文件或切换大量分支后会重建索引。短时间高 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