Engineering guide

在云端 Mac 上定位 Swift 编译瓶颈

在云端 Mac 上定位 Swift 编译瓶颈

一个 Swift 模块从几十秒逐渐编译到数分钟时,先不要立即重写代码或增加并发。远程环境中的依赖解析、构建脚本、链接和类型检查都会计入 Xcode 的总时间。有效的排查方式是固定云端 Mac 的工具链与输入,取得阶段级证据,再把问题缩小到具体源文件和表达式。

先建立可重复的测量基线

选择一个没有其他构建任务运行的时段,固定代码提交、Scheme、Configuration、SDK 和目标架构。记录 xcodebuild -version、当前提交以及实际执行命令。不要拿一次干净构建与一次增量构建直接比较,两者回答的是不同问题。

mkdir -p "$HOME/build-audit"
xcodebuild -version > "$HOME/build-audit/toolchain.txt"
git rev-parse HEAD > "$HOME/build-audit/commit.txt"

set -o pipefail
/usr/bin/time -l xcodebuild \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -showBuildTimingSummary \
  build \
  2>&1 | tee "$HOME/build-audit/baseline.log"

先连续执行三次普通增量构建,确认耗时波动是否稳定。需要测量干净构建时,应明确执行 clean 或使用独立的 DerivedData 路径,并让优化前后的清理方式完全一致。

单次最快结果不能作为结论。应比较同一条件下多次运行的中位数,同时保留最慢一次的日志,用来检查偶发脚本、网络请求或资源竞争。

从阶段摘要判断调查方向

Build Timing Summary 会显示编译、链接、资源处理和脚本等阶段。先找占比最高且重复出现的项目,不要只盯着最后一行总耗时。

现象 优先检查 常见误判
SwiftCompile 长时间占首位 类型检查、单文件体积、批量编译行为 误以为链接慢
Run Script 每次都执行 输入输出文件、脚本内部扫描范围 只增加机器并发
依赖解析耗时不稳定 锁文件、仓库访问、解析是否重复 当成 Swift 编译问题
Link 阶段突出 链接输入、调试符号、重复库 重写业务表达式

还要检查脚本是否声明输入与输出。没有依赖边界的脚本可能在每次增量构建中运行,掩盖源码优化的效果。脚本中若包含下载操作,应把网络等待与本地计算分别计时。

定位缓慢的函数与表达式

确认瓶颈位于 Swift 编译后,再临时加入前端诊断参数。可在专用诊断 Configuration 的 OTHER_SWIFT_FLAGS 中设置,避免污染团队日常配置。

-Xfrontend -debug-time-function-bodies
-Xfrontend -debug-time-expression-type-checking

重新执行同一条构建命令,将标准错误一并保存。日志通常会给出耗时、文件位置和函数或表达式。不同 Xcode 工具链的输出格式与阈值可能变化,因此解析脚本不应假定列数永久固定。

优先处理什么

先按耗时排序,再聚合到文件。一个重复编译数百次的中等热点,可能比单个极慢函数更值得处理。常见高成本代码包括过长的泛型链、嵌套闭包、包含大量分支的单个表达式,以及让编译器同时推断多个中间类型的集合转换。

修改时每次只处理一类问题,例如拆分表达式、为中间值补充明确类型,或把大型函数分成边界清晰的小函数。不要同时改编译设置和源码,否则无法判断收益来自哪里。

用最小改动验证原因

假设某段代码把过滤、映射、字典构造和可选值处理串在一个表达式中,可以先给中间结果命名并标注类型。目标不是追求更短的源码,而是减少类型检查器一次需要求解的约束。

let validItems: [Item] = items.filter { $0.isValid }
let identifiers: [String] = validItems.map(\.identifier)
let result: [String: Item] = Dictionary(
    uniqueKeysWithValues: zip(identifiers, validItems)
)

完成修改后,用相同提交基线之外仅包含该改动的分支重新测量。至少核对三项:热点日志中的该表达式是否下降、SwiftCompile 阶段是否下降、总构建时间是否在多次运行中稳定改善。若只有总时间变化而热点不变,应继续检查缓存或后台任务,而不是把结果归因于源码调整。

把诊断变成可维护的验收流程

诊断参数不适合长期加入所有构建。更稳妥的做法是建立独立的性能检查任务,按需运行并归档工具链、提交、命令和摘要。日志可能包含本地路径、仓库结构或环境变量展开结果,上传前要先脱敏。

建议为每次调查保存以下内容:

  1. Xcode 与 Swift 版本。
  2. Git 提交和构建配置。
  3. 干净构建或增量构建的明确标记。
  4. 三次以上运行的原始耗时。
  5. 阶段摘要与主要源码热点。
  6. 修改前后的唯一差异和回退方式。

如果结果在云端 Mac 上波动明显,先确认是否有并行构建、索引任务或遗留脚本运行,再复测。MiniRent 提供的是独享物理节点,但同一设备内由用户启动的任务仍会竞争 CPU、内存和磁盘。可靠结论来自受控输入、可重复命令和保留完整证据,而不是一次看起来更快的构建。

常见问题

为什么不能只比较两次 Xcode 总构建时间?

总时间会同时受到依赖解析、缓存状态、脚本和链接影响。应先固定提交、Xcode 版本与构建参数,再按阶段和具体 Swift 源码热点比较。

Swift 编译诊断参数适合长期保留在项目里吗?

不建议。它们主要用于短期取证,可能产生大量日志并随工具链变化。完成定位后应移除参数,仅在性能回归任务中按需启用。

Dedicated cloud Mac

在独享物理节点上运行构建任务

按天、周、月或季选择 MiniRents M4 与 MiniRents M4 Pro,并按团队和代码仓库位置选择节点。设备为独享物理机、非虚拟机。

选择机型并订购