第4章:Trace核心概念——看懂时间线背后的“骨架”
欢迎来到核心概念区。说实话,Perfetto 的 UI 看起来很直观——花花绿绿的条条框框,拖拽缩放,好像谁都会看。但如果你只想停留在这个层面,那遇到复杂问题你肯定抓瞎。我个人觉得,搞懂 Trace、Track、Slice、Counter、Event、Packet 这几个基础概念,比背一百条 ADB 命令都值。它们是 Perfetto 的“语法”,不掌握语法,你读不懂系统到底在干什么。
4.1 Trace 和 Packet:大盒子与小积木
先说 Trace。Trace 是什么?说白了,就是你用 perfetto 命令抓到的一个 .perfetto-trace 文件。它是一个大盒子,装着你分析性能问题的所有原始证据。一次抓取会话,生成一个 Trace,这是最基本的单位。
那么,这个盒子里装的是什么?Packet。 Perfetto 序列化的最小单元是 Protobuf 数据包。Trace 文件本质上就是一长串按时间顺序排列的 Packet 流。你想想看,解析 Trace 时,Perfetto 就像读一本按时间记录的书,一页一页(一个 Packet 一个 Packet)地读取,重建出系统的完整状态。
我之前调试一个内存泄漏,就是靠扫描特定类型的 Packet。我记得当时的场景——我写了个小工具,在 Packet 流中过滤 ProcessTree 类型的消息,发现某个 Native 进程在反复创建匿名 mmap 却没有释放。如果我不是从 Packet 层面思考,根本不会想到这样去定位。
常见的 Packet 类型包括:
- FtraceEventBundle—— 囊括调度、中断、G2D 等内核事件。
- TrackDescriptor—— 声明一个 Track 的元数据(名字、关联进程/线程等)。
- ProcessTree—— 描述进程和线程的父子关系。
- CpuFreqPacket—— CPU 频率变化记录。
- ChromeMetadataPacket—— Chromium 相关数据。
4.2 Track:调度与执行的“传送带”
Track 是时间轴上的行。你可以把它想象成一条传送带,上面按顺序放着各种事件和 Slice。在 Perfetto UI 上,你会看到很多并排的行——每个进程有自己的 Track,每个线程有自己的 Track,每个 CPU 核心也有自己的 Track。
Track 的元数据来自 TrackDescriptor 数据包。它定义了 Track 的唯一 ID(UUID)、名称以及关联的进程/线程信息。为什么要区分 Track?说白了,是为了隔离上下文。进程 A 的 Slice 不会跑到进程 B 的 Track 里,这就排除了干扰。
# Track描述符的数据结构示意(Protobuf)
track_descriptor {
uuid: 1001
name: "com.myapp"
process: { pid: 1234 }
}
track_descriptor {
uuid: 1002
name: "my_thread"
process: { pid: 1234 }
thread: { tid: 1235 }
}
嗯,这里要注意:如果你在 UI 上看到某个 Track 突然消失了,别慌。那可能是它的创建者和销毁者通过 RemoveTrack 事件把轨道标记为失效。我在分析 Chrome 浏览器性能时,就遇到过进程频繁创建和销毁 Track 导致界面闪烁的情况。
4.3 Slice:有始有终的“执行片段”
Slice 是最常用的概念,也是你做性能分析的“主力兵”。它代表一个有开始时间、有结束时间的操作。在 UI 上,它显示为一个长条。比如 binder transaction、Choreographer.doFrame、dequeueBuffer,这些都是 Slice。
我经常跟新人说:“没有结束的 Slice,就像没有结账的购物车——肯定有问题。” 为什么呢?因为 Perfetto 需要靠成对的 begin 和 end 事件来生成那个长条。如果一个 Slice 只有开始没有结束,UI 上就会出现一个一直延伸到时间轴末尾的“无限长”片段。
Slice 可以嵌套。举个例子:
Choreographer.doFrame(父 Slice,耗时 30ms)- ├─
Input(子 Slice,耗时 5ms) - ├─
Animation(子 Slice,耗时 3ms) - ├─
Measure/Layout/Draw(子 Slice,耗时 22ms)
你想想看,如果 doFrame 占了 30ms,超过 16ms 的边界,我一眼就能看出是 Measure/Layout/Draw 的 22ms 拖了后腿。这就是嵌套 Slice 的力量,它直接把瓶颈暴露给你。
binder transaction 的 Slice 都正常结束,但某个自定义 Slice 老是“开而不合”,导致 UI 上出现奇怪的间隙。排查下来,原来是有个线程在执行 Slice 过程中被 pthread_kill 强制退出了。所以,如果你的 Trace 里有不完整的 Slice,请先用“是不是进程异常退出”这个假说来看问题。
4.4 Counter 和 Event:折线与印记
Counter(计数器) 是用于展示随时间变化的数值的。它没有“时长”概念,只有时间点和对应的数值。在 UI 上它画成折线图或面积图。常见的 Counter 有:CPU 频率(cpufreq)、内存占用(mem.rss)、帧率(fps)、GPU 负载。
我有一个习惯:分析性能瓶颈时,一定会把 Counter Track 和 Sched Track 对照着看。比如说,我看到某个 App 线程在干活,但 CPU 频率只有 600MHz,那说明它很可能被限制频率了。对照着 cpufreq Counter 和 sched_switch 事件,可以确认是系统触发了功耗限制还是温度过热导致的降频。
Event(事件) 是瞬时的“印记”。它就标记在某个时间点,不占据时长。Perfetto 中很多重要的 Event 来自 ftrace:
sched_waking—— 一个进程正在被唤醒(但还没拿到 CPU)。sched_switch—— CPU 上下文切换,显示下一个要运行的进程。irq_handler_entry/exit—— 中断处理开始和结束。tracing_mark_write—— 用户态埋下的 Marker 点。
我在分析卡顿时,一定会先看 waking 事件。为什么会这样?因为卡顿往往是因为线程 A 被阻塞,等待线程 B 释放资源。通过查看 waking 事件,我能发现线程 B 什么时候被唤醒,以及它什么时候真正拿到的 CPU。如果两者之间有较大延迟,说明 CPU 资源紧张。
4.5 核心概念关系一览
| 概念 | 本质 | 是否有持续时间 | 典型示例 |
|---|---|---|---|
| Trace | 容器 / 会话文件 | 是(抓取时长) | 一次点击“Record”生成的文件 |
| Packet | Protobuf 数据包 | 无(序列化单元) | FtraceEventBundle、TrackDescriptor |
| Track | 时间轴上的“传送带” | 是(贯穿始终) | CPU 0、线程 1234 对应的行 |
| Slice | 有始有终的执行片段 | 是(必含开始+结束) | binder transaction、doFrame |
| Event | 瞬时点印记 | 否 | sched_waking、sched_switch |
| Counter | 数值变化曲线 | 否(时间点+数值) | CPU 频率、内存占用量 |
这张表请你保存好。它揭示了 Perfetto 数据的核心逻辑:Trace 是容器,Packet 是积木,Track 是传送带。Slice、Event、Counter 就是附着在 Track 上的三类核心数据,分别对应“持续操作”“瞬时标记”和“数值变化”。
Slice 和 Counter 的关联查询。分析卡顿:首先去 FrameDisplay Track 找 doFrame Slice。如果它运行超过 16ms,马上查当时的 gpu Counter 和 cpu 频率 Counter。这两者一交叉,瓶颈到底在渲染管线的哪一段,基本就水落石出。