一次本地 Instruments 观察显示“首屏大约一秒”,合并后的版本却在远程测试中明显变慢。问题通常不在工具,而在测量对象没有边界:网络等待、JSON 解析、数据库写入和 UI 提交被揉成一个总时长,机器状态稍有变化,结果就失去可比性。更可靠的做法是用 os_signpost 标记业务区间,再让云端 Mac 上的 xctrace 以固定场景重复采集。
先定义可重复的关键路径
不要先问“整个 App 有多快”,先挑一个起止点稳定的用户动作。例如进入订单页后,从本地缓存读取开始,到首批列表单元完成数据绑定结束。把它拆成三个区间:
| 区间 | 起点 | 终点 | 是否适合门禁 |
|---|---|---|---|
load_cache |
发起缓存读取 | 返回结构化数据 | 适合 |
decode_payload |
开始解码 | 生成领域对象 | 适合 |
render_first_batch |
提交首批模型 | 首批视图完成布局 | 适合 |
wait_remote_api |
发起外部请求 | 收到响应 | 仅作诊断 |
外部网络时间受线路和服务端状态影响,不宜直接阻断合并。门禁应优先覆盖进程内、输入固定、能够重复触发的工作。
一个区间只能回答一个问题。若同一标记同时包含网络、磁盘和渲染,超限后仍然无法判断该修哪里。
添加稳定且可关联的埋点
使用固定的 subsystem 和 category,区间名称也不要拼接用户数据。需要并发测量时,为每次业务动作创建独立 OSSignpostID,避免两个相邻任务配错起止点。
import os
enum PerfTrace {
static let log = OSLog(
subsystem: "com.example.mobile",
category: "CriticalPath"
)
static func begin(_ name: StaticString) -> OSSignpostID {
let id = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: id)
return id
}
static func end(_ name: StaticString, id: OSSignpostID) {
os_signpost(.end, log: log, name: name, signpostID: id)
}
}
调用端必须用 defer 收口,否则异常分支会留下只有开始、没有结束的区间:
let traceID = PerfTrace.begin("decode_payload")
defer { PerfTrace.end("decode_payload", id: traceID) }
let models = try decoder.decode([Item].self, from: fixtureData)
不要把令牌、文件内容或用户标识写入 signpost 文本。性能证据会进入构建产物,字段应保持低基数并可安全归档。
固定 xctrace 的运行条件
先在图形界面中确认模板名称和区间确实可见,再切换到命令行。测试应用应使用固定数据夹具、固定模拟器型号和相同构建配置。采集前终止旧进程,避免上一次运行残留状态。
set -euo pipefail
OUT="$PWD/perf-artifacts"
mkdir -p "$OUT"
xcrun xctrace record \
--template "Points of Interest" \
--time-limit 45s \
--output "$OUT/critical-path.trace" \
--launch -- "$APP_PATH" \
-PerfScenario first-batch \
-PerfFixture "$PWD/Fixtures/items.json"
xcrun xctrace export \
--input "$OUT/critical-path.trace" \
--toc > "$OUT/toc.xml"
xctrace export 的 XPath 和表结构可能随工具版本变化,因此解析脚本应先检查目标 schema 是否存在,再读取区间。不要把依赖列位置的 awk 当长期接口;优先解析导出的 XML,并在版本升级时保留一份最小 trace 做契约测试。
在 LemonVM 的远程环境中执行时,先记录 xcodebuild -version、系统版本、构建配置和测试夹具哈希。节点或工具链变化后,应重新建立基线,而不是把新旧环境的数字硬放在一起比较。
用多轮样本建立门槛
单次运行容易受到首次加载、后台索引和温度状态影响。建议先预热一次,再执行至少五轮有效采集。门禁比较中位数,同时给最慢样本设置宽松上限。
假设基线中位数为 420 ms,可以把告警线设为基线的 1.15 倍,即 483 ms;但不要把这个比例复制到所有区间。几十毫秒的小函数与数秒的数据准备过程,波动模型并不相同。
判定脚本至少区分三类结果:
- trace 无法生成:基础设施失败,不判性能回退。
- 目标区间缺失:场景或埋点失败,需要修复测试。
- 有效样本超过门槛:性能门禁失败,并归档证据。
基线文件应和代码一起评审,包含区间名、样本数、中位数、允许上限、工具链版本和夹具哈希。任何提高阈值的改动都应在提交说明中写明原因,避免门槛只升不降。
排除常见的假回退
首次运行慢而后续稳定,通常是缓存或动态链接准备造成的,应把冷启动和热路径分成两组。所有轮次都缓慢时,再检查代码变化。只有某一轮异常,则先看当时是否存在索引、备份、额外模拟器或并行构建。
还要检查以下项目:
- Release 与 Debug 数据不能混用;
- 不同模拟器系统版本分别维护基线;
- 每轮测试前恢复相同业务状态;
- 禁止性能任务和大型构建任务并发;
- trace、解析结果和标准错误输出一起归档;
- 解析不到区间时禁止默认写入
0 ms; - 超时退出后要终止被测应用与遗留采集进程。
如果测试依赖图形会话,任务开始前应增加一次可见性预检;若只测纯解析或存储层,则尽量使用独立测试目标,减少界面状态带来的噪声。
让失败结果可以直接行动
门禁输出不应只有“性能失败”。至少打印区间名、当前中位数、基线、变化比例、有效样本数和 trace 路径。例如:
FAIL render_first_batch
median_ms=512
baseline_ms=420
change=21.9%
samples=7
trace=perf-artifacts/run-06.trace
评审者看到结果后,应能直接下载对应 trace,定位到具体区间,并判断是业务代码回退还是测量环境异常。稳定的性能门禁不是追求每次数字完全相同,而是让相同输入、相同工具链和相同运行条件下的变化可解释、可复查。先从一条高价值路径开始,连续观察其波动,再逐步增加区间,比一次加入几十个不可靠指标更有效。
常见问题
os_signpost 性能门禁应该测平均值还是中位数?
优先使用多轮样本的中位数,并同时限制高分位或最大允许值。平均值容易被单次系统抖动拉偏,单次结果则不适合作为合并门禁。
xctrace 采集失败时应该直接判定性能回退吗?
不应该。采集失败、区间缺失和性能超限应使用不同退出码;前两者属于基础设施或场景问题,只有取得有效样本后才能判定性能回退。
所有 os_signpost 区间都适合进入持续集成吗?
不适合。只选择起止点稳定、业务语义明确且能被固定输入重复触发的关键路径;依赖人工操作或外部实时服务的区间应留作诊断指标。
按任务周期选择 LemonVM 云端 Mac
Lemon M4 与 Lemon M4 Pro 覆盖新加坡、东京、首尔、香港和美国西部,支持日、周、月、季计费。