编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 性能优化实践

    • README
    • 公共方法论

      • 性能工程总论原理
      • 跨平台指标体系
      • 性能求证方法论
      • 数据采集与观测
      • 归因方法与火焰图
        • 01.归因的两条路径
          • 1.1 自顶向下:从体感到代码
          • 1.2 自底向上:从热点到调用方
          • 1.3 何时用哪种
        • 02.关键路径分析
          • 2.1 关键路径的定义
          • 2.2 寻找关键路径的方法
          • A. Trace 时间轴分析
          • B. 依赖图分析
          • C. 反推法
          • 2.3 Amdahl 定律与优化天花板
        • 03.on-CPU 与 off-CPU 分析
          • 3.1 on-CPU:忙的归因
          • 3.2 off-CPU:等的归因
          • 3.3 wakeup 与因果链
        • 04.火焰图(Flame Graph)
          • 4.1 火焰图的构造原理
          • 4.2 火焰图的解读方法
          • 五个看点
          • 关键问句
          • 4.3 火焰图的变种
          • 4.4 常见误读
          • 误读 A:把"宽"当成"慢的原因"
          • 误读 B:忽略 off-CPU
          • 误读 C:误以为高度代表耗时
          • 误读 D:被采样率欺骗
        • 05.归因决策树
          • 5.1 卡顿归因决策树
          • 5.2 内存增长归因决策树
          • 5.3 启动慢归因决策树
          • 5.4 网络慢归因决策树
        • 06.混淆变量识别
        • 07.归因案例演练
          • 案例:列表滚动严重掉帧(跨平台同构)
          • Step 1 - APDEX
          • Step 2 - RED
          • Step 3 - USE
          • Step 4 - on-CPU vs off-CPU
          • Step 5 - 火焰图
          • Step 6 - 跨平台同构归因
          • Step 7 - 平台特异
          • Step 8 - 跨平台通用治理 + 平台特化
          • Step 9 - 求证(详见《03.求证方法论》)
        • 一句话总结
      • 性能预算防劣化
      • 性能体系全景图
      • 性能优化误区集
    • 体系建设篇

    • 资源专项篇

    • 流水线专项

    • 业务专项篇

    • 交付防御篇

  • 程序编程原理

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

  • 专栏
  • 性能优化实践
  • 公共方法论
杨充
2026-05-27
目录

归因方法与火焰图

# 归因方法论与火焰图解读

📊 学习成本预估 | 难度:⭐⭐⭐⭐⭐(5/5)| 阅读:约 30 分钟 | 实操:3 小时 🔗 前置阅读:卷零·01-04 | ➡️ 后续延伸:卷二·01, 卷三·03

采集到数据,下一步是归因:从"知道慢"走到"知道为什么慢"。本文给出一套跨平台通用的归因路径与火焰图解读方法。

# 目录介绍

  • 01.归因的两条路径
    • 1.1 自顶向下:从体感到代码
    • 1.2 自底向上:从热点到调用方
    • 1.3 何时用哪种
  • 02.关键路径分析
    • 2.1 关键路径的定义
    • 2.2 寻找关键路径的方法
    • 2.3 Amdahl 定律与优化天花板
  • 03.on-CPU 与 off-CPU 分析
    • 3.1 on-CPU:忙的归因
    • 3.2 off-CPU:等的归因
    • 3.3 wakeup 与因果链
  • 04.火焰图(Flame Graph)
    • 4.1 火焰图的构造原理
    • 4.2 火焰图的解读方法
    • 4.3 火焰图的变种
    • 4.4 常见误读
  • 05.归因决策树
    • 5.1 卡顿归因决策树
    • 5.2 内存增长归因决策树
    • 5.3 启动慢归因决策树
    • 5.4 网络慢归因决策树
  • 06.混淆变量识别
  • 07.归因案例演练

# 01.归因的两条路径

任何性能问题的归因,都可走两条互补的路径。

# 1.1 自顶向下:从体感到代码

用户感知(卡 / 慢)
   │  APDEX / Web Vitals
   ▼
请求级(哪类操作慢)
   │  RED 模型
   ▼
资源级(哪种资源饱和)
   │  USE 模型
   ▼
线程 / 进程级(哪个线程在做什么)
   │  off-CPU / on-CPU 分析
   ▼
函数级(哪个函数在烧 CPU / 在等待)
   │  火焰图 / Profile
   ▼
代码行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

适用:从一个用户感知问题出发,尚未知道根因。

# 1.2 自底向上:从热点到调用方

函数 A 占 CPU 30%
   │
   ▼
被谁调用?(caller stack)
   │
   ▼
是否在关键路径?是否可消除?
1
2
3
4
5
6
7

适用:已经看到一个明显热点(如某 API 调用占 30% CPU),想确认它是否真的有害。

# 1.3 何时用哪种

场景 推荐路径
用户报"卡顿" / 监控告警 自顶向下
Profile 出明显热点 自底向上
偶发问题、原因不明 自顶向下,配合 trace
复盘已知问题 自底向上更直接

专家的常态是两条路同时走:自顶向下定位"层",自底向上找到"点",在中间相遇。

# 02.关键路径分析

# 2.1 关键路径的定义

借用项目管理的概念。在性能上下文中:

关键路径(Critical Path):从"输入触发"到"用户感知完成",决定整体时长的最长依赖链。

示例(启动):

进程创建 ──▶ Application.onCreate ──▶ MainActivity.onCreate ──▶ 首屏 layout ──▶ 首屏 draw
                  │                          │
                  └─ 异步初始化(不在关键路径上)
1
2
3

只有关键路径上的耗时才影响用户感知;非关键路径的优化对体感无益。

# 2.2 寻找关键路径的方法

# A. Trace 时间轴分析

录制完整 trace(Perfetto / Instruments / DevTools),找到主线程上首尾相连的一系列任务,即为关键路径。

# B. 依赖图分析

把所有任务画成 DAG,节点为任务,边为依赖。关键路径 = DAG 上的最长路径。

# C. 反推法

从终点(首帧上屏)反向追问"它在等什么",直到追到起点。

# 2.3 Amdahl 定律与优化天花板

              1
S = ───────────────────────
      (1 - p) + p / s

S:整体加速比
p:可加速部分占比
s:该部分的加速比
1
2
3
4
5
6
7

例子:

  • 启动总耗时 1000ms,其中数据库初始化 400ms。
  • 把数据库初始化优化到 100ms(4× 加速)。
  • 整体提升 = 1000 / (600 + 100) = 1.43×(即从 1000ms 到 700ms)。

核心洞察:

  • 优化收益的天花板 = 该部分占比。占比 5% 的代码优化到 0,整体最多提升 5%。
  • 应优先优化占比最大的关键路径。
  • 当某段已被优化到极致,剩余部分的 p 增大,需要重新评估优先级。

# 03.on-CPU 与 off-CPU 分析

# 3.1 on-CPU:忙的归因

on-CPU:线程实际占用 CPU 时间。

采集:周期性采样调用栈(如 99Hz),统计各栈出现频率。

适用:定位"CPU 在烧什么"。

典型工具:

  • Linux/Android:perf / simpleperf
  • macOS/iOS:Instruments Time Profiler
  • Web:DevTools Performance / Profiler API
  • 嵌入式:perf / gprof

# 3.2 off-CPU:等的归因

off-CPU:线程因等待(IO / 锁 / 信号)而被调度出 CPU 的时间。

这是性能优化中最容易被忽视的部分:

  • 用户感觉卡,但 CPU 不高 → 大概率是 off-CPU 问题。
  • 主线程总耗时 100ms,其中 on-CPU 仅 30ms,剩余 70ms 全在等。

典型 off-CPU 来源:

来源 例子
磁盘 IO 读 SharedPreferences / 大文件
网络 IO 同步请求
锁等待 synchronized / mutex 竞争
跨线程同步 wait/notify、Future.get
跨进程通信 Binder / IPC
GC 等待 Stop-The-World

采集:

  • Android:atrace 配合 sched_switch tracepoint,或 eBPF
  • iOS:Instruments System Trace
  • Web:DevTools Performance(蓝色 idle / 黄色 task)

# 3.3 wakeup 与因果链

off-CPU 时间内,线程在等"被谁唤醒"。找到唤醒者,就找到了因果链。

线程 A:等锁 ───────── (off-CPU 50ms)─────── 拿到锁,继续
                                  ▲
                                  │ wakeup
线程 B:持有锁 ───── 释放锁 ──────┘
1
2
3
4

Linux 的 sched_wakeup tracepoint 可记录唤醒关系,是高级归因的核心。eBPF 工具如 offcputime 能直接产出 off-CPU 火焰图。

实战意义:当卡顿发生时,主线程 off-CPU 50ms,找到唤醒者是某个 Binder 线程在等远程进程返回,归因瞬间清晰。

# 04.火焰图(Flame Graph)

由 Brendan Gregg 发明,是性能归因最重要的可视化工具。

# 4.1 火焰图的构造原理

输入:大量调用栈样本(每个样本是从根到叶的栈帧序列 + 计数)。

构造:

  1. 把所有栈对齐根部、合并相同前缀。
  2. 横轴:合并后的"宽度"= 出现次数(耗时占比)。
  3. 纵轴:栈深度。
  4. 每一格 = 一个栈帧,宽度 = 该函数(含被调用的子函数)总耗时。

关键约定:

  • 宽度有意义:越宽 = 越耗时。
  • 横轴顺序无意义:通常按函数名字典序排列。
  • 颜色无意义:通常随机,便于区分(部分变种用颜色编码语言层)。
          [main]                       <- 根,宽度 = 100%
           │
   ┌───────┼─────────┐
[init][render][network]                <- 子函数,宽度反映耗时占比
   │      │
[gc][layout][raster]
       │       │
   [measure][gpu_submit]               <- 越往上越是底层 / 叶子函数
1
2
3
4
5
6
7
8

# 4.2 火焰图的解读方法

# 五个看点

看点 解读
宽柱顶端 该函数自身耗时大("平顶"),是优化重点
宽且分叉 该函数耗时由多个子函数贡献,需逐个看
窄但深 调用链很深但总耗时不高,关注是否有冗余调用
重复出现的栈 同一函数在多处出现,可能是热点工具方法
意外的栈 不应出现在该路径的函数(如 UI 线程出现 IO)

# 关键问句

1. 宽度最大的"平顶"是谁?  → 自身耗时最高的函数
2. 它是预期的吗?           → 业务必要 vs 可优化
3. 它能被消除 / 减少 / 异步化吗?
4. 它的 caller 在关键路径上吗?
1
2
3
4

# 4.3 火焰图的变种

类型 用途
on-CPU Flame Graph 默认,看 CPU 时间分布
off-CPU Flame Graph 看等待时间分布,定位 stall 问题
Differential / Diff 对比两个版本的火焰图,红色 = 退化,蓝色 = 改善
Memory Flame Graph 横轴 = 分配字节数,定位内存热点
Allocation Flame Graph 横轴 = 分配次数,定位频繁分配
Wakeup Flame Graph 显示唤醒因果,归因 off-CPU
Inverted Flame Graph (Icicle) 倒过来,从根看向叶

Differential Flame Graph 的威力:发布前后跑一次,红色部分一目了然 —— 回归排查神器。

# 4.4 常见误读

# 误读 A:把"宽"当成"慢的原因"

真相:宽 = 耗时占比高,但不一定是"问题"。

  • 一个 App 必然要做的事(如 layout)就是宽的,不代表它有问题。
  • 重点看:是否过宽(大于业务必要程度)。

# 误读 B:忽略 off-CPU

仅 on-CPU 火焰图看不到等待。卡顿很多源于 off-CPU。

# 误读 C:误以为高度代表耗时

高度只代表栈深度,不代表耗时。永远只看宽度。

# 误读 D:被采样率欺骗

低采样率下,火焰图可能漏掉短任务。结论需结合采样率与样本量。

# 05.归因决策树

下面给出 4 个最常见性能问题的决策树。遇到对应问题时,按树走一遍。

# 5.1 卡顿归因决策树

单帧 > 16.67ms 出现
   │
   ├── on-CPU 占主导?─── Yes ──► 火焰图找平顶
   │                            │
   │                            ├─ 业务计算 → 算法 / 异步化
   │                            ├─ 渲染计算 → 减少视图层级 / 减少重绘
   │                            ├─ 内存分配密集 → 对象池 / 复用
   │                            └─ 解码 / 压缩 → 异步 / 缓存
   │
   └── off-CPU 占主导?── Yes ──► off-CPU 火焰图 + wakeup 链
                                │
                                ├─ IO Wait → 异步 / 缓存
                                ├─ 锁等待 → 减少临界区 / 无锁数据结构
                                ├─ Binder/IPC → 批量 / 缓存远端结果
                                ├─ GC 停顿 → 减少分配 / 降低内存压力
                                └─ Page Fault → mmap 预热 / 减少冷数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 5.2 内存增长归因决策树

内存持续增长
   │
   ├── 增长是否随业务功能开关?──► 关闭 X 后不再增长 = X 是源头
   │
   ├── 是 Java/Heap 增长?─── Yes ──► Heap dump → 支配树
   │                                │
   │                                ├─ 单类对象数异常 → 泄漏
   │                                ├─ Bitmap 占大头 → 图片缓存策略
   │                                └─ String 占大头 → 字符串拼接 / 重复
   │
   ├── 是 Native 增长?──── Yes ──► malloc trace / hprof
   │                                │
   │                                ├─ 第三方 SO 库 → SO 内泄漏排查
   │                                └─ JNI 引用未释放 → GlobalRef 检查
   │
   └── 是匿名映射 (mmap)?  Yes ──► smaps 分析
                                  │
                                  └─ 大段匿名 mmap → 检查 NIO Buffer / 文件映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 5.3 启动慢归因决策树

冷启动 > 目标值
   │
   ├── 进程创建到 onCreate 慢?─► dex 加载 / SO 加载 / 类预热
   │
   ├── Application.onCreate 慢?─► 同步初始化项目 → 异步化 / 懒加载
   │
   ├── Activity 首帧前慢?─────► 主线程被阻塞?或视图层级过深?
   │
   └── 首帧到 TTI 慢?────────► 主线程被业务请求阻塞 → 关键路径分析
1
2
3
4
5
6
7
8
9

# 5.4 网络慢归因决策树

请求耗时 P95 高
   │
   ├── DNS 时间长?──────► DNS 缓存 / HTTPDNS
   │
   ├── Connect 时间长?──► 长链接 / 连接复用
   │
   ├── TLS 时间长?─────► Session Resumption / TLS 1.3 / 0-RTT
   │
   ├── TTFB 长?───────► 服务端慢 / 弱网 / 队头阻塞 → HTTP/2、HTTP/3
   │
   └── Body 下载慢?───► 弱网 / Body 过大 → 压缩 / 分片 / CDN
1
2
3
4
5
6
7
8
9
10
11

# 06.混淆变量识别

归因最容易被混淆变量误导。识别清单:

混淆 表现 真因
时间序列偏差 改完代码后下午测,比上午快 设备温度差异(降频)
缓存效应 第二次跑都快 OS Page Cache、JIT 编译、磁盘 cache
测试数据偏差 测试集小 / 单一 真实分布不同
平台版本偏差 仅在某 OS 版本上慢 OS 自身问题,非业务问题
后台干扰 偶发慢 后台任务抢占资源
Profiler 自身影响 加上 profiler 后慢 5 倍 工具开销,不是真实情况

对策:

  • 至少做 2 台同型号、不同测试者的独立验证。
  • 切片分析(按时段 / 网络 / 系统版本)。
  • 区分 profile 模式与生产模式数据。

# 07.归因案例演练

# 案例:列表滚动严重掉帧(跨平台同构)

现象:

  • Android:单帧 P99 = 48ms(应 < 16ms),FPS 平均 42。
  • iOS:相同列表,单帧 P99 = 38ms。
  • Web:相同列表,单帧 P99 = 55ms。
  • 三端皆有问题,但程度不同。

自顶向下走一遍:

# Step 1 - APDEX

体感差,确实是性能问题,进入度量。

# Step 2 - RED

慢的是哪类操作?滚动单帧(duration)。错误率正常。

# Step 3 - USE

  • CPU 占用:Android 65%、iOS 50%、Web 70%。
  • IO Wait:低。
  • 内存正常。
  • → 不是资源饱和问题。

# Step 4 - on-CPU vs off-CPU

  • Android 主线程 on-CPU 70%,off-CPU 30%。
  • iOS 主线程 on-CPU 75%,off-CPU 25%。
  • Web 主线程 on-CPU 85%。

→ 主要是 on-CPU 问题。off-CPU 还有一段,需查。

# Step 5 - 火焰图

三端火焰图共同特征:

[ScrollView/ListView/UICollectionView/IntersectionObserver Scroll]
    │
    └── [bindViewHolder / cellForRow / render]
            │
            ├── [Bitmap.decode / UIImage init / Image decode]   ← 平顶 30%
            ├── [setText / NSAttributedString / innerHTML]      ← 平顶 20%
            └── [layout / autolayout / reflow]                  ← 平顶 25%
1
2
3
4
5
6
7

# Step 6 - 跨平台同构归因

层 三端共性根因
应用 列表项含大图,滚动时同步解码
应用 文本富文本布局复杂
运行时 滚动频繁触发 Layout,无缓存复用
系统 滚动期间需保持 60fps,主线程预算极紧

# Step 7 - 平台特异

平台 额外因素
Android RecyclerView ViewHolder 复用率低(不同 itemType 太多)
iOS 自动布局约束嵌套深,触发多次 layoutSubviews
Web 布局抖动(read/write 交替触发 reflow)

# Step 8 - 跨平台通用治理 + 平台特化

通用治理(适用三端):

  1. 图片预解码(IO 线程) + 内存缓存。
  2. 富文本渲染结果缓存。
  3. 视图复用率监控(要求复用率 > 90%)。

平台特化:

  • Android:itemType 收敛到 ≤ 3 种;启用 Prefetch。
  • iOS:用 frame 布局替代约束(极致场景);预排版。
  • Web:DOM 操作分批,避免 layout thrashing。

# Step 9 - 求证(详见《03.求证方法论》)

通用 + 特化方案上线后:

平台 P99 帧时长前 后 改善
Android 48ms 17ms -65%
iOS 38ms 14ms -63%
Web 55ms 20ms -64%

关键洞察:通用治理贡献了 70%+ 改善,平台特化贡献 30%。这印证了"原理跨端通用,差异只是实现"的核心命题。

# 一句话总结

归因不是猜,而是按路径走。
自顶向下定位"层",自底向上定位"点",在火焰图上相遇。

上次更新: 2026/06/07, 10:26:12
数据采集与观测
性能预算防劣化

← 数据采集与观测 性能预算防劣化→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式