- 187 次 PR 里,八成多是 warm——日常该盯的是 warm,别把 cold 混进 P95
- 改依赖、清缓存、换 scheme 会「冷启动」,墙钟差 2–3 倍 很正常
- 就算都是 warm,cache 没配好、多 job 抢磁盘、测试和签名搅在一起,仍会忽快忽慢
- 大致顺序:排队 → 分清 cold/warm → 缓存 → 控并发 → 最后才谈换机器(对照见 瀑布拆解)
完整对照数据 → GitHub Actions 优化主轴 · Benchmark
1. CI 像抽奖:同 commit,差几倍
在 GitHub Actions 上跑 iOS CI,下面这些画面应该不陌生:
- 同一个 commit 重跑一遍,墙钟能差 2–3 倍
- 看板 P95 红得吓人,大家体感却是「平时 merge 也没等那么久」
pod install有时半分钟,有时像卡住了- 周五下午不敢点 merge——怕 CI 又「抽中」慢的那档
未必是机器算力不够。我们跟团队做过 14 天 Shadow 双轨,更常见的真相是:数字算混了,环境也不稳——cache 没留住、磁盘被抢,体感就像抽奖。换芯片往往排在很后面。
这篇只聊一件事:构建为什么忽快忽慢。job 卡在队列里等 runner?看 排队怎么破。缓存怎么配、买机租机划不划算,留给 cache 专题 和 ROI 那篇。
2. cold 和 warm:别混在一锅粥里
有个坑特别常见:把每次构建的耗时全倒进一个 P95。偶尔来一趟「冷启动」,尾巴就被拉很长——看数据觉得「天天都很慢」,其实日常 merge 没那回事。
- warm:依赖没动、缓存还在、scheme 没换——多半是增量编译,代表「日常 merge 长什么样」
- cold:锁文件变了、缓存清了、换 target 或 runner 上第一次跑——得重新 resolve、装 Pod、大面积重编
日常 SLA 盯 warm 的 P50/P95 就够了;cold 另画一条线,别和 merge 体验绑在一起。
2.1 什么情况会「冷启动」
在 macos-latest 上 cold 更常见——job 跑完 workspace 往往就清掉了,缓存很难「住下来」:
| 触发条件 | 典型耗时增量 | 日志里常见关键字 |
|---|---|---|
Podfile.lock 变更 |
+3–8 分钟 | pod install、Downloading dependencies |
| DerivedData 未命中 / 被清 | +5–15 分钟 | CompileSwift、全量 .o 重建 |
| 切换 scheme / 新 target | +2–10 分钟 | 不同 xcodebuild -scheme |
| SPM 依赖解析变更 | +1–5 分钟 | Resolve Package Graph |
| Xcode 小版本被动升级(macos-latest) | 首次构建 +10–20 分钟 | 新 SDK / 模块缓存重建 |
依赖升得勤,cold 自然多——不是机器退化,是这周干的活不一样。汇报时把「升 Pod 那周」和「平常开发周」分开,数字才说得通。
2.2 warm 为啥还是不稳
就算都是 warm,墙钟仍可能上下飘个三成左右,常见是这些:
- cache 没命中:key 少写了
arm64、没绑分支,或多个 job 抢同一个槽 - 同一 org 里 macOS job 扎堆,磁盘和网络互相拖
- 这次只编 main,下次全量 unit + UI test——工作量本就不一样
- 改动面不同:动 Pod 源码和改个 SwiftUI 预览,compile 量差很远
3. 时间到底耗在哪儿
给 workflow 各步打个时间戳,墙钟大致能拆成五块。下面是 warm 时比较常见的分布(项目不同会有出入):
① checkout + 环境准备 ~0:30 – 1:30 ② pod install / SPM resolve ~0:30 – 2:00 (cold 时 ↑↑) ③ xcodebuild compile+link ~3:00 – 8:00 (代码变更面决定) ④ 测试(simulator / unit) ~1:00 – 6:00 (可选,常被低估) ⑤ archive + codesign ~1:00 – 4:00 (发版流水线) 合计 warm P50 常见区间:6 – 14 分钟
实用做法:用 step 耗时或 time 分别看 pod install、xcodebuild build、xcodebuild test。② 经常五分钟往上——先想 cold 和 cache;③ 忽长忽短——查 DerivedData 和并发;④ 总是拖后腿——测试拆出去,或放 nightly 全量。
4. 测试和签名:隐形拖后腿
不少「xcodebuild 好慢」的反馈,拆开看是测试或签名被算进同一次 build:
- Simulator 冷启动:CI 上第一次拉起可能要等多几分钟,没预热就每 job 重来一遍
- UI Test 比 unit 慢一个量级,和 compile 塞一个 job 里,P95 很难看
- 证书、Keychain、Profile 下载——托管 runner 上常常每次 job 重新配一遍
- Archive、打 IPA 是发版流水线的事,别和 PR 验证的耗时搅在一起
PR:build + 轻量测试,盯 warm P95。
TestFlight / 发版:单独 workflow、单独指标。混在一处,「平时 merge 要等多久」永远对不上号。
5. 14 天 Shadow 实测
下面是一组双轨对照:macos-latest 和独享 Mac mini M4 各跑 187 次 PR,Xcode 16.2、CocoaPods 1.15.2 对齐。完整做法见 主轴 Benchmark。
| 分类 | 样本数 | 占比 | macos-latest P95 | 独享 M4 P95 |
|---|---|---|---|---|
| warm build | 162 | 86.6% | 14:12 | 6:05 |
| cold build | 25 | 13.4% | 19:40 | 11:20 |
| 混在一起算(容易误判) | 187 | 100% | ~16:00+ | ~7:30+ |
如果把 cold 和 warm 搅在一起报 P95,日常体验会被高估大概 15–25%,容易得出「得马上换机器」的结论。更稳的做法:merge 体验看 warm,升依赖那几周单独预期 cold。
独享 M4 上 warm 仍比 cold 快一截,说明让 DerivedData、Pods 有固定「家」很值——详见 CocoaPods / SPM / DerivedData 缓存优化。
6. 怎么排:从看清到 cache
排队问题排除(或已经不大)之后,可以按这个顺序来,别一上来就谈买机器:
- 给构建打标签:cold 还是 warm(Podfile.lock 变没变、cache 中没中)
- 日常 SLA 只盯 warm P95;cold 周报或单独一条线
- 把 DerivedData、Pods、SPM 的缓存策略定下来 — CocoaPods / SPM / DerivedData 缓存优化
- macOS job 别挤太满:self-hosted 一台 1–2 个并发就够;托管侧也尽量别多 job 共 workspace
- PR 验证和发版/signing 拆开,别让测试污染 merge 指标
- cache 都到位了还不达标,再聊 M4 / M4 Pro — 见 芯片与工时
- name: Classify build type
run: |
if git diff --name-only HEAD~1 | grep -q Podfile.lock; then
echo "BUILD_TYPE=cold" >> $GITHUB_ENV
else
echo "BUILD_TYPE=warm" >> $GITHUB_ENV
fi
- name: Record wall clock
run: |
echo "build_type=${BUILD_TYPE}" >> metrics.csv
echo "wall_sec=$(( $(date +%s) - START ))" >> metrics.csv
7. 常见问题(FAQ)
同 commit 重跑差 2–3 倍,正常吗?
在 macos-latest 上挺常见:一回 cache 全中,一回 miss 叠上排队,就像两档不同的 build。先看两次 pod install 和 cache hit 日志,别急着换芯片。
P95 到底怎么算才对?
日常 merge 的体验,用 warm 算 P95 就行,两周攒够三十次样本比较稳。cold 另算或只记次数——混在一起,采购决策容易偏。
pod install 每次都很慢,是 CocoaPods 的锅吗?
托管 runner 上更常是 Pods 没地方「住」、cache key 没绑分支和架构。self-hosted 指到固定大盘,warm 时半分钟以内很常见。
换 Mac mini M4 能消除波动吗?
对照里 warm P95 能降一大截(−57%),波动也收敛(σ −40%),但分不清 cold/warm、cache 没设计好,独享机照样会有尖刺。
慢是排队还是 build 本身?
看日志开头 Waiting for a runner 等了多久。排队算墙钟、不算 GitHub 分钟费。queue 几乎为零还慢,就是这篇聊的构建侧 → 接着看 cache。
还想往下读?
缓存 YAML 和 key 怎么设计 → cache 专题;买还是租、划不划算 → ROI 模型 和 月构建 500 次怎么选。
8. 收个尾
GitHub Actions 上 iOS CI 「慢」,很少是单纯 CPU 不够。更常是这几件事叠在一起:
- cold 和 warm 混在一起看,P95 被抬高了
- 托管 job 跑完就清盘,缓存留不住
- 测试、签名和 PR build 搅在一个 job 里
先把 warm 的 P95 算清楚,再 cache → 并发 → 硬件。14 天 Shadow 里,只做对指标和缓存,warm P95 就能从 14:12 拉到 6:05——不必先拍板买机器。
想让 DerivedData 有个固定「家」?
Vuncloud Cloud Mac M4 Pro 带 1TB 数据盘、预装 actions-runner,DerivedData / Pods 可以一直留在盘上,托管 runner 那种「每次冷启动」会少很多。