跨平台指标体系
# 跨平台性能模型与指标体系
📊 学习成本预估 | 难度:⭐⭐⭐⭐(4/5)| 阅读:约 25 分钟 | 实操:1 小时 🔗 前置阅读:卷零·01 | ➡️ 后续延伸:卷一·01, 卷一·03
本文定义专栏的统一指标语言。所有性能议题(启动、卡顿、内存、网络…)必须能在本文定义的指标体系中找到对应位置,否则视为"伪议题"。
# 目录介绍
# 01.为什么需要统一的指标体系
# 1.1 指标混乱的代价
设想以下真实对话:
- A:我们启动优化了 30%!
- B:你说的是冷启动还是温启动?
- A:……都算吧,平均值。
- B:哪一档机型?是 Time to First Frame 还是 Time to Interactive?
- A:……
指标定义不清的结果:
- 团队之间无法对齐 —— 同一个数字代表不同含义。
- 优化效果不可验证 —— 改动前后的"对比"不可比。
- 线上线下不一致 —— 实验室数据漂亮,线上灾难。
- 老板看的报表是错的 —— 决策基于错觉。
统一指标体系的目的:让"启动 1.2s"这种描述对任何团队、任何平台都有精确、可复现、可度量的含义。
# 1.2 一个好指标的五个特征
借鉴 Google SRE 与 Brendan Gregg 的实践:
| 特征 | 含义 | 反例 |
|---|---|---|
| 可度量 | 可由仪器自动采集 | "用户感觉很卡" |
| 可定义 | 有数学表达,无歧义 | "性能优秀" |
| 可对比 | 不同时刻 / 不同版本之间可比 | 单次抽样 |
| 可归因 | 异常时能定位到具体层 | "整体性能下降" |
| 可决策 | 有阈值能驱动行动 | "FPS 大概 50" |
# 02.三种指标视角
性能指标的世界有三个互补的视角,缺一不可。
# 2.1 USE 模型 — 资源视角
由 Brendan Gregg 提出,针对任意资源的健康度三元组:
| 维度 | 含义 | 典型指标 |
|---|---|---|
| Utilization 利用率 | 资源在被使用的时间占比 | CPU% / 内存使用率 / GPU 占用率 |
| Saturation 饱和度 | 排队 / 等待程度(超额需求) | Run Queue 长度 / Page Fault 频率 / IO Wait |
| Errors 错误数 | 资源相关错误事件 | OOM / IO Error / 网络重传 |
关键认知:
- 利用率高 ≠ 有问题(CPU 100% 可能是健康的,只要不饱和、不出错)。
- 饱和度才是性能瓶颈的真实信号。CPU 利用率 70% + Run Queue 持续 > 1,比 CPU 100% + Run Queue=0 更危险。
适用场景:CPU、内存、磁盘、网络、GPU、文件描述符、线程池…… 任何"有限资源"。
# 2.2 RED 模型 — 请求视角
由 Tom Wilkie 提出,针对任意请求型组件:
| 维度 | 含义 | 典型指标 |
|---|---|---|
| Rate 速率 | 单位时间请求数 | QPS / 启动次数 / 帧产出速率 |
| Errors 错误率 | 失败请求占比 | 网络错误率 / 渲染丢帧率 / 崩溃率 |
| Duration 时长 | 单次请求耗时分布 | 接口耗时 P95 / 帧时长 P99 |
适用场景:网络请求、页面加载、单帧渲染、单次启动、单次手势响应。
# 2.3 APDEX / Web Vitals — 用户感知视角
USE 看资源、RED 看请求,但用户不关心这些,用户关心感受。因此需要第三类指标:
# APDEX (Application Performance Index)
APDEX = (Satisfied + Tolerating/2) / Total
阈值 T 由产品定义:
≤ T → Satisfied
T < x ≤ 4T → Tolerating
> 4T → Frustrated
2
3
4
5
6
# Google Web Vitals(Web 端事实标准,可借鉴到客户端)
| 指标 | 含义 | 优秀阈值 | 物理依据 |
|---|---|---|---|
| LCP Largest Contentful Paint | 主要内容渲染 | < 2.5s | 用户感知"加载完成" |
| INP Interaction to Next Paint | 交互到下次绘制 | < 200ms | 操作反馈连续性 |
| CLS Cumulative Layout Shift | 布局抖动累积 | < 0.1 | 视觉稳定性 |
# 客户端等价指标(本专栏定义)
| 客户端指标 | Web Vitals 对应 | 含义 |
|---|---|---|
| TTFM Time to First Meaningful Frame | LCP | 启动到首屏可见有意义内容 |
| TTI Time to Interactive | INP/TTI | 启动到可响应用户操作 |
| Tap Latency 点击响应延迟 | INP | 用户触摸到下一帧绘制 |
| Frame Drop Ratio 丢帧率 | — | 单位时间内未按时绘制的帧占比 |
| Jank Score 卡顿评分 | — | 综合卡顿严重度(次数 × 时长) |
# 2.4 三视角组合使用
用户感知(APDEX/Vitals)
▲
│ "用户体验如何?"
│
请求时长/错误(RED)
▲
│ "哪类操作出问题?"
│
资源饱和/错误(USE)
▲
│ "为什么会出问题?"
│
系统层
2
3
4
5
6
7
8
9
10
11
12
13
自顶向下分析路径:
- 用户感知层(APDEX)告警 → 用户体验劣化。
- 下钻到 RED → 哪些请求慢 / 错?
- 下钻到 USE → 是哪个资源饱和 / 出错导致的?
反例:只看 USE 不看 APDEX —— "CPU 不高、内存不满,但用户骂街"。这种情况几乎一定是流水线阻塞(off-CPU stall),需要专门的方法(详见《05.归因方法论》)。
# 03.分布与百分位
# 3.1 为什么均值是性能领域的谎言
考虑两组延迟数据,单位 ms:
组 A: [50, 50, 50, 50, 50, 50, 50, 50, 50, 50] 均值 50, P99 50
组 B: [10, 10, 10, 10, 10, 10, 10, 10, 10, 410] 均值 50, P99 410
2
均值相同,但 B 的用户体验是灾难:每 10 次操作有 1 次卡顿 410ms。如果只看均值,你以为两个版本一样好。
为什么性能数据天然长尾?
- GC 偶发触发
- IO 偶发抖动
- 缓存偶发未命中
- 锁偶发竞争激烈
- 网络偶发抖动
→ 性能延迟分布几乎都是右偏的、长尾的,均值被尾部样本严重拉偏。
# 3.2 百分位的物理含义
| 百分位 | 物理含义 | 关注场景 |
|---|---|---|
| P50(中位数) | 一半用户的体验 | 普遍体验 |
| P90 | 10% 用户感受到的"差体验" | 优化的常规目标 |
| P95 | 5% 用户感受到的"明显差体验" | 商业级 SLO 常用 |
| P99 | 1% 用户的"灾难体验" | 大流量产品必看 |
| P99.9 / P99.99 | 万分之一概率的极端尾部 | 金融 / 关键系统 |
关键认知:
- DAU 1 亿的产品,P99 = 5s 意味着每天 100 万次"灾难体验"。
- 优化的目标不是降均值,而是压缩尾部。
# 3.3 直方图与 HDR 直方图
百分位需要从直方图计算。两种常见实现:
| 类型 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 等距直方图 | 简单 | 高百分位精度差 | 范围已知的指标 |
| HDR Histogram | 对数桶,全量程高精度 | 实现复杂 | 性能延迟(推荐) |
实践要点:
- 不要用"算 P99 = 取后 1% 平均" —— 这是错的。
- 不要在客户端聚合 P99 后上报 —— P99 不可线性合并。
- 上报直方图桶 + 计数,在服务端聚合后计算分位。
# 3.4 抖动与稳定性指标
均值低不代表稳定。以下指标度量"波动":
| 指标 | 含义 | 用途 |
|---|---|---|
| 标准差 σ | 波动幅度 | 整体稳定性 |
| 变异系数 CV = σ/μ | 相对波动 | 跨指标对比 |
| P99/P50 比率 | 长尾扩张倍数 | 长尾健康度 |
| Jitter | 相邻帧 / 相邻请求时长差 | 帧率稳定性 |
帧率领域有句话:"60FPS 的均值不如 50FPS 的稳定"。用户对抖动的感知比对速度更敏感。
# 04.跨平台核心指标矩阵
下面是本专栏的"指标字典",后续章节直接引用。
# 4.1 CPU 类指标
| 指标 | 视角 | 定义 | Android 采集 | iOS 采集 | Web 采集 | 嵌入式采集 |
|---|---|---|---|---|---|---|
| 进程 CPU 使用率 | USE-U | 进程占用 CPU 时间 / 总时间 | /proc/[pid]/stat | host_processor_info | Performance API(受限) | /proc/[pid]/stat |
| 主线程 on-CPU% | USE-U | 主线程在 CPU 上执行时间占比 | /proc/[pid]/task/[tid]/stat | thread_info | Long Tasks API | task_struct |
| Run Queue 长度 | USE-S | 等待 CPU 的线程数 | /proc/loadavg | host_load_info | 不可见 | /proc/loadavg |
| 上下文切换率 | USE-S | 每秒上下文切换次数 | /proc/[pid]/status | task_events_info | 不可见 | /proc/stat |
| IPC(每周期指令) | 效率 | 单位 CPU 周期完成的指令数 | simpleperf | Instruments CPU Counters | 不可见 | perf |
# 4.2 内存类指标
| 指标 | 视角 | 定义 | Android | iOS | Web | 嵌入式 |
|---|---|---|---|---|---|---|
| RSS 物理内存 | USE-U | 实际占用物理内存 | /proc/[pid]/status | task_vm_info | 不可见(隔离) | /proc/[pid]/status |
| PSS 比例集 | USE-U | 共享内存按比例分摊后 | /proc/[pid]/smaps | 无对应 | 不可见 | /proc/[pid]/smaps |
| Java/Heap 使用 | USE-U | VM 堆已用 | Runtime.totalMemory() | 无 | performance.memory | 无 |
| GC 频率 | USE-S | 单位时间 GC 次数 | ART trace | 无(ARC) | DevTools | — |
| GC 停顿时长 | USE-S | 单次 GC 暂停时长 | ART trace | 无 | PerformanceObserver(longtask) | — |
| OOM 次数 | USE-E | 进程因内存被杀次数 | lowmemorykiller | Jetsam | crash | OOM Killer |
| 内存抖动 | USE-S | 短时间内大量分配 / 释放 | Allocation Tracker | Allocations Instrument | DevTools | — |
# 4.3 渲染类指标
| 指标 | 视角 | 定义 |
|---|---|---|
| 平均帧率 FPS | RED-R | 单位时间渲染帧数 |
| 帧时长 P99 | RED-D | 99% 的帧能在多长时间内绘制完成 |
| 丢帧率 | RED-E | 未在 Vsync 内完成的帧占比 |
| 大卡顿率 | APDEX | 单帧 > 700ms 的次数 / 总帧数 |
| Jitter | 抖动 | 相邻帧时长方差 |
| GPU 占用 | USE-U | GPU 利用率 |
| Overdraw 倍数 | 效率 | 像素被重复绘制的次数 |
平台采集:
- Android:
Choreographer.FrameCallback+gfxinfo+ Perfetto - iOS:
CADisplayLink+MetricKit+os_signpost - Web:
requestAnimationFrame+PerformanceObserver(frame) - 嵌入式:显示控制器 IRQ + 帧缓冲交换计数
# 4.4 网络类指标
| 指标 | 视角 | 定义 |
|---|---|---|
| 请求成功率 | RED-E | 2xx + 3xx 占比 |
| 接口耗时 P95 | RED-D | 95% 请求完成时长 |
| DNS / Connect / TLS / TTFB / Total 分段耗时 | RED-D | 请求各阶段时长 |
| 重传率 | USE-S | TCP 重传比例 |
| 弱网占比 | 环境 | RTT > 阈值 / 丢包率 > 阈值的会话占比 |
# 4.5 启动类指标
启动是客户端最复杂的复合指标:
| 指标 | 起点 | 终点 | 物理含义 |
|---|---|---|---|
| Cold Launch Time | 进程创建 | 首帧 | 完整冷启动 |
| TTFF Time to First Frame | 进程创建 | 第一帧上屏 | 用户看到"动了" |
| TTFM Time to First Meaningful Frame | 进程创建 | 首屏内容可见 | 用户看到"有用" |
| TTI Time to Interactive | 进程创建 | 主线程空闲、可响应 | 用户能"操作" |
三个时刻递进:TTFF < TTFM < TTI。多数团队只关注 TTFM,但 TTI 才决定"用户能不能用"。
# 4.6 稳定性类指标
| 指标 | 定义 |
|---|---|
| Crash Rate | 崩溃用户数 / DAU |
| ANR Rate | ANR 用户数 / DAU |
| Foreground OOM Rate | 前台 OOM 数 / DAU |
| 异常退出率 | 非正常进程结束占比 |
# 05.指标的边界与陷阱
# 5.1 高 Goodhart's Law 风险指标
"When a measure becomes a target, it ceases to be a good measure." — Charles Goodhart
某些指标一旦被作为 KPI,团队会"针对指标"而非"针对体验"优化:
| 高风险指标 | 作弊方式 |
|---|---|
| 平均 FPS | 把卡顿挪出采集窗口 |
| 启动时长 | 把耗时初始化推迟到首帧之后(伪首帧) |
| 崩溃率 | 把 Crash 拦截后吞掉 |
| 内存均值 | 在采集时机主动 GC |
对策:
- 用多指标组合(FPS 必带 P99 + 大卡顿率)。
- 用用户感知指标兜底(如 NPS、留存)。
- 关键指标做采集口径文档化 + 评审。
# 5.2 平台口径差异
同名指标在不同平台口径不同,必须显式声明:
| 指标 | Android 口径 | iOS 口径 | 是否可直接对比 |
|---|---|---|---|
| 内存 | PSS(含共享分摊) | Footprint(不含 dirty 共享) | ❌ |
| FPS | Choreographer 回调统计 | CADisplayLink 回调统计 | ✅(语义相近) |
| 启动 | Application.onCreate→首帧 | main()→viewDidAppear | ❌ 起止点不同 |
| 崩溃 | 包含 Native + Java + ANR | 只含信号崩溃 | ❌ |
# 5.3 采样误差与置信区间
任何指标都是样本估计,应附带置信区间:
P95 = 1.2s ±0.05s (95% CI, n=10000)
经验法则:
- 估计 P50:样本量 ≥ 100
- 估计 P95:样本量 ≥ 1000
- 估计 P99:样本量 ≥ 10000
- 估计 P99.9:样本量 ≥ 100000
样本不足时,禁止给出高百分位结论。
# 06.从指标到 SLO
# 6.1 SLI / SLO / SLA 的关系
| 概念 | 含义 | 例子 |
|---|---|---|
| SLI Service Level Indicator | 实际度量值 | "本周冷启动 P95 = 2.1s" |
| SLO Service Level Objective | 内部目标 | "冷启动 P95 ≤ 2.0s 占比 ≥ 99%" |
| SLA Service Level Agreement | 对外承诺 | 客户端通常无 SLA |
# 6.2 错误预算
错误预算 = 1 - SLO
如 SLO 为 P95 ≤ 2.0s 占比 99%,
则允许 1% 的"违约时段"作为创新 / 上线的预算。
2
3
4
错误预算的两个用途:
- 预算未耗尽:可激进上线、可做有风险的优化实验。
- 预算耗尽:冻结新功能、专注治理。
# 6.3 端侧 SLO 的特殊性
服务端 SLO 主要看可用性,端侧 SLO 必须分维度切片:
| 切片维度 | 必要性 |
|---|---|
| 机型档位(高 / 中 / 低端) | 高端机 SLO 严格,低端机宽松 |
| 系统版本 | 老系统给宽松目标 |
| 网络类型(WiFi / 4G / 弱网) | 弱网下放宽 |
| 国家 / 地区 | 海外网络环境差异大 |
| 应用版本 | 防止新版本劣化老用户 |
端侧 SLO 的常见错误:用一个总均值定 SLO。结果是高端机被低端机数据掩盖、问题永远找不到。
# 07 行业 SLO 基准对照
本章汇集业界公开过的性能基准数据,作为团队制定 SLO 时的参考锚点。 数据来源:Google I/O / WWDC / 字节火山引擎 / 阿里云栖 / Meta 工程博客等公开材料。 使用注意:业界基准 ≠ 你的目标,你应该比所在行业平均高一档,比头部低一档(除非你就是头部)。
# 7.1 启动时间基准
| 类型 | 行业平均 | 优秀基准 | 头部目标 | 来源 |
|---|---|---|---|---|
| Android 冷启动(中端机) | 2.5-3.5s | ≤ 2.0s | ≤ 1.2s | Google Play Console 公开 |
| iOS 冷启动 | 1.5-2.5s | ≤ 1.5s | ≤ 0.8s | WWDC 'Optimize App Launch' |
| Web 首屏(FCP) | 2.5s | ≤ 1.8s | ≤ 1.0s | Google Web Vitals |
| 微信小程序冷启动 | 3-5s | ≤ 2.0s | ≤ 1.5s | 微信公开课 |
# 7.2 渲染与卡顿基准
| 指标 | 行业平均 | 优秀基准 | 头部目标 |
|---|---|---|---|
| FPS P95(60Hz) | ≥ 50 | ≥ 55 | ≥ 58 |
| FPS P99(60Hz) | ≥ 40 | ≥ 50 | ≥ 55 |
| 卡顿率(帧 > 100ms) | ≤ 2% | ≤ 0.5% | ≤ 0.1% |
| ANR 率 | ≤ 0.4% | ≤ 0.1% | ≤ 0.05% |
| 输入响应 P95 | ≤ 200ms | ≤ 100ms | ≤ 50ms |
# 7.3 内存与崩溃基准
| 指标 | 行业平均 | 优秀基准 | 头部目标 |
|---|---|---|---|
| 崩溃率 | ≤ 0.5% | ≤ 0.1% | ≤ 0.02% |
| OOM 占崩溃比 | 30-50% | < 20% | < 10% |
| Java 堆峰值(中端机) | 200-300MB | ≤ 200MB | ≤ 150MB |
| Native 堆 | < 200MB | < 150MB | < 100MB |
# 7.4 网络性能基准
| 指标 | 行业平均 | 优秀基准 | 头部目标 |
|---|---|---|---|
| API 成功率 | ≥ 98% | ≥ 99.5% | ≥ 99.9% |
| API 耗时 P95 | ≤ 2s | ≤ 1s | ≤ 500ms |
| DNS 解析 P95 | ≤ 200ms | ≤ 100ms | ≤ 50ms |
| 弱网(地铁)成功率 | ≥ 80% | ≥ 95% | ≥ 99% |
# 7.5 头部 App 公开过的真实数字
| App | 场景 | 数字 | 来源 |
|---|---|---|---|
| 抖音 | 启动到首条视频可播 | < 1s | 字节火山引擎案例 |
| 微信 | 冷启动 P50 | ~ 1.5s | 微信公开课 |
| 手淘 | 冷启动到首屏 | ~ 1.8s | 阿里云栖大会 |
| iOS 冷启动 | < 1.2s | Meta 工程博客 | |
| Notion | 离线优先架构下,启动 P95 | ~ 800ms | Notion Engineering 博客 |
# 7.6 反例:失败的优化案例
| 案例 | 教训 |
|---|---|
| Meta Lite 启动从 2.5s 优化到 0.8s | 包体积增加 30%,发达地区收益 < 落后地区 |
| 某电商 App 全量异步化启动 | 启动反升 250ms(异步切换开销),后续回滚 |
| Twitter "Predict and Preload" 实验 | 命中率仅 23%,整体功耗反而升高 |
核心启示:基准是参考,不是目标——你的业务、用户群、设备分布决定了你"应该"达到什么水平。
# 一句话总结
指标体系的价值,不是产生数字,而是产生"可决策的语言"。
当团队所有人在说"P95 帧时长"时,指的是同一件事,性能工程才能开始运转。