一项首页重构合并后,功能测试全部通过,用户却开始反馈首次打开变慢。启动性能最容易在日常开发中被忽略:新增数据库迁移、同步读取配置、初始化分析模块或加载大图,都可能把耗时悄悄塞进主线程。与其等人工体验发现问题,不如在固定的云端 Mac 上把冷启动测量做成合并门禁。
先定义可比较的启动场景
不要把“点开应用”当成完整测试定义。至少要固定构建配置、Simulator 机型、系统运行时、应用初始数据和启动参数。测试目标也应保持单一,例如只测从进程创建到首个可交互界面出现,不把登录请求或测试数据下载混入结果。
建议单独建立 Performance.xctestplan,只启用启动性能用例,并关闭随机执行。测试环境通过启动参数进入稳定页面:
final class LaunchPerformanceTests: XCTestCase {
override func setUp() {
continueAfterFailure = false
}
func testColdLaunch() {
let app = XCUIApplication()
app.launchArguments = ["-uiTesting", "-resetLocalState"]
let options = XCTMeasureOptions()
options.iterationCount = 8
measure(
metrics: [XCTApplicationLaunchMetric(waitUntilResponsive: true)],
options: options
) {
app.launch()
}
}
}
-resetLocalState 应由应用的测试入口处理,只清理可重建的本地状态。不要在正式代码路径中根据该参数跳过真正需要测量的初始化,否则得到的是“测试专用启动速度”。
固定云端 Mac 与 Simulator 状态
性能门禁需要独享物理机,而不是与未知任务争用 CPU 和磁盘的共享环境。每次执行前先确认当前 Xcode 路径、可用 Simulator 和磁盘余量:
set -euo pipefail
xcode-select -p
xcodebuild -version
xcrun simctl list devices available
df -h "$HOME"
在测试计划中选定一个已安装的 Simulator。不要让脚本自动挑选“第一个可用设备”,因为镜像变化会使历史数据失去可比性。门禁运行期间还应避免并行归档、依赖更新和大规模索引任务。
性能数据首先是环境数据,其次才是代码数据。无法说明运行节点、构建配置与 Simulator 状态的数字,不适合直接用于阻断合并。
首次建立基线时,应在同一提交上重复执行。若离散程度明显偏大,先排查后台任务、磁盘压力和测试数据重置逻辑,而不是立即放宽阈值。
用 xcodebuild 执行单一门禁
命令行任务只运行目标用例,并把结果包保存为流水线产物。设备名称需替换为测试计划实际固定的型号:
set -o pipefail
mkdir -p Artifacts
xcodebuild test \
-workspace App.xcworkspace \
-scheme App \
-testPlan Performance \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:AppUITests/LaunchPerformanceTests/testColdLaunch \
-resultBundlePath Artifacts/Launch.xcresult \
| tee Artifacts/launch-test.log
set -o pipefail 很关键,否则 xcodebuild 失败后,管道末端的 tee 仍可能让任务返回成功。Launch.xcresult 与文本日志都应保留:前者用于查看每次测量和测试附件,后者便于快速定位编译或启动阶段错误。
| 检查项 | 固定方式 | 变化后的处理 |
|---|---|---|
| Xcode | 记录 xcodebuild -version |
先重建候选基线 |
| Simulator | 固定机型与运行时 | 不与旧基线直接比较 |
| 构建配置 | 使用同一测试计划 | 拒绝临时参数覆盖 |
| 测试数据 | 启动参数执行确定性重置 | 检查重置是否完整 |
| 迭代次数 | 门禁统一使用 8 次 | 不按单次结果判定 |
建立基线而不是追逐单次数字
在 Xcode 的测试报告中为指定测试建立性能基线,并把生成的共享基线文件纳入版本控制。基线变更要像依赖锁文件一样接受评审:提交说明应写清变化来源、验证提交和预期影响,不能只写“更新性能数据”。
阈值需要区分提醒与阻断。可以把小幅波动留作趋势观察,把连续复跑仍超过容忍范围的变化设为失败。真正的判断依据不是某一次最快成绩,而是同一环境下多次迭代是否稳定偏离已审核基线。
以下情况不应直接更新基线:
- 测试节点同时运行了其他重任务;
- Simulator 型号或运行时发生变化;
- 依赖首次下载或索引尚未完成;
- 测试为了通过而跳过了初始化步骤;
- 只有一次运行出现异常,其余结果正常。
失败后保留现场并定位责任路径
门禁失败时,先在同一云端 Mac、同一提交上复跑一次。若结果可复现,再按启动阶段拆分:进程入口、依赖注入、数据库打开、首屏模型构建、资源解码和首帧渲染。可以在这些边界加入统一的 os_signpost,让后续分析看到耗时落在哪一段,而不是凭提交规模猜测。
排查顺序应从最近变更开始,但不要只盯业务代码。构建配置变化、调试诊断选项、资源体积增长和测试夹具也会改变启动路径。修复后保留失败与通过两份 xcresult,并在合并请求中记录对照提交。
一套可靠门禁的价值不在于给启动时间贴上绝对标签,而在于让同一场景持续可比。固定环境、版本化基线、保存证据,再把基线更新纳入评审,启动性能才会从主观感受变成可追踪的工程约束。
常见问题
启动性能测试应该运行多少次?
同一提交至少执行 5 次,门禁任务建议设置 8 次或更多迭代,并保持模拟器型号、系统运行时、构建配置和后台进程一致。
性能门禁失败后应立即放宽阈值吗?
不应立即调整。先在同一节点复跑,检查模拟器状态、日志和代码路径;只有确认产品行为发生了有意变化,才重新建立并评审基线。
用一台按项目启停的云端 Mac 复现工作流
从三档 Apple Silicon 配置中选择合适资源,按天、周、月或季租用。设备为独享物理机、非虚拟机,实际可用状态以控制台实时返回为准。