
做Android和Linux性能分析的人Perfetto应该都不陌生。CPU Scheduling events是每次打开trace文件时信息量最大、也最容易看懵的一块满屏的彩色条、忽长忽短的色块、各种缩写状态乍一看像心电图细看又不知道到底能说明什么问题。但一旦学会从调度事件里读出谁在哪个CPU上跑、什么时候被换下去、换下去之后是继续排队还是去睡觉、又是被谁唤醒的很多卡顿和性能问题就能直接定位到线程级不需要靠猜。这篇内容就是围绕Perfetto官方文档里CPU Scheduling events部分展开的我会先讲清楚调度事件本身记录了什么再给一套从下载、录制配置、UI实操到问题定位的完整流程。适合刚接触Perfetto、想系统看调度数据的同学也适合已经在用但只停留在看一眼彩色条阶段的工程师。1. CPU Scheduling events先搞清楚内核到底记录了什么1.1 调度事件不是采样是每一次切换的完整账本提到性能分析很多人先想到的是perf采样、火焰图。CPU调度事件和采样有本质区别采样是每隔一段时间抽一次样两次采样之间发生了什么只能靠猜调度事件是内核在每个线程切换发生的瞬间主动记录下来的一次切换记录一条准确率接近百分之百。Perfetto里的CPU Scheduling events核心数据来自内核ftrace的调度tracepoint。这里有三类事件是基础几乎每次排查都会用到整理成表格更直观事件名触发时机回答的问题sched_switch线程从CPU上被换下来另一个线程被换上去谁被换下了换下去时是什么状态哪个线程接盘了CPUsched_wakeup一个睡眠中的线程被唤醒准备进入可运行队列谁醒了被谁唤醒的目标CPU是哪个sched_wakeup_new一个新创建的线程第一次被唤醒新线程从哪里来第一次登录CPU的路径是什么这三类事件合起来覆盖了线程生命周期里最重要的一段从睡眠到被唤醒、从排队到拿到CPU、从运行到让出CPU。Perfetto的UI之所以能把调度过程画成一条条带颜色的横条是因为它把连续的sched_switch事件在时间轴上拼了起来每一段连续占用CPU的时间就是一个调度片slice。理解这点很重要后面所有的操作都是围绕slice展开的。1.2 sched_switch事件里藏着的细节sched_switch事件本身携带的信息量很大。一次完整的sched_switch通常包含prev_comm上一个线程名、prev_pid、prev_prio上一个线程的优先级、prev_state上一个线程被切换出去时的状态以及next_comm、next_pid、next_prio下一个线程的信息。这里最值得关注的是prev_state。这个字段决定了线程被换下去之后去哪值为0表示线程仍然处于可运行状态只是暂时让出了CPU后续还会回来排队值为S表示线程是自愿睡眠一般是等锁、等IO或者主动sleep值为D表示不可中断睡眠典型场景是等待内核IO完成磁盘卡住时经常会看到大片的D状态。Perfetto官方文档里在讲调度事件时特别强调了要区分让出CPU后仍在排队和让出CPU后去睡觉这两种情况。前者说明CPU资源紧张后者说明线程在等待某个东西。后面的排查套路基本都从这里起跳。另外再说一个容易忽略的点sched_wakeup事件里通常会带target_cpu字段。这个字段能告诉你线程被唤醒时内核想让它去哪个CPU上跑但实际最终跑到哪个CPU要等sched_switch真正发生才知道。从wakeup到switch之间如果隔了很长时间说明线程醒是被醒了但排队等CPU等了很久那问题大概率出在调度延迟上。2. 准备阶段Perfetto下载、录制配置与数据打开2.1 其实大部分时候你只需要一个网页先说结论如果只是打开别人发来的trace文件分析根本不需要下载任何工具。Perfetto官方提供了在线UI直接用浏览器打开ui.perfetto.dev把trace文件拖进页面就能开始分析解析和渲染都在本地完成trace数据不会上传。那什么时候需要下载Perfetto工具链呢两种情况一是需要自己录制trace二是需要对trace做命令行级的分析。这时候才需要下载perfetto的预编译包通常从GitHub的Perfetto官方Release页面拿对应平台的压缩包解压后目录里有几个常用可执行文件perfetto负责录制trace_processor负责解析和SQL查询tracebox是Android上的录制工具。下载和解压没有特别讲究解压后直接命令行调用即可。如果你同时有adb环境也可以直接用设备自带的perfetto。Android 10之后的系统镜像里普遍内置了perfetto用adb shell which perfetto检查一下就能确认。2.2 录制配置把调度事件完整抓下来的最小方案自己录制时配置是关键。Perfetto支持通过一个文本格式的pbtx配置文件指定要采集的数据源。下面是抓取CPU调度事件的最低配置我已经在一台Pixel和一台Linux服务器上验证过直接可以用buffers { size_kb: 131072 } data_sources { config { name: linux.ftrace ftrace_config { ftrace_events: sched_switch ftrace_events: sched_wakeup ftrace_events: sched_wakeup_new ftrace_events: sched_process_exit ftrace_events: power_cpu_frequency } } } duration_ms: 15000简单解释一下各个配置的作用buffers.size_kb设成了128MB调度事件在高频场景下产生速度很快buffer太小会导致事件丢失录完后发现有大量的空白断层多半是buffer不够ftrace_events里只列了5类事件sched_switch和sched_wakeup系列是核心sched_process_exit负责标记线程退出power_cpu_frequency是为了同时拿到CPU频率曲线排查降频问题时必须有它duration_ms设15秒一般够复现一次卡顿操作了太长的录制会让buffer持续滚动把早期的关键数据挤掉。录制时Android设备上可以这样操作adb push config.pbtx /data/local/tmp/ adb shell perfetto --txt -c /data/local/tmp/config.pbtx -o /data/local/tmp/trace.perfetto-trace adb pull /data/local/tmp/trace.perfetto-trace .Linux服务器上则需要在root权限下执行sudo perfetto -c config.pbtx -o trace.perfetto-trace录制完成后把生成的.perfetto-trace文件拖进ui.perfetto.dev就能看到完整的时间轴。注意Android设备上如果没有root权限部分事件可能受限建议优先在可root的设备或者userdebug版系统上录制。2.3 打开trace后先认识界面布局Perfetto UI打开一个trace后默认会显示两条大类的轨道区域上面是CPU轨道记录每个物理CPU核心上的活动下面是进程轨道按进程分组进程里再展开各个线程。每个线程轨道上那些横向的彩色条就是调度slice。上方CPU轨道通常还分两块一块是CPU slice轨道显示每个核心上被哪个线程占用一块是频率轨道显示该核心的实时频率。这两个轨道配合起来能直接看出CPU是在满频跑还是在降频摸鱼。界面底部有一个统计区域当你在时间轴上拖拽选中一段范围后它会显示选中范围内所有slice的统计信息包括总时长、slice总数、各状态占比。这是后面做量化分析的主要入口。3. UI实操把调度事件读成一段因果故事3.1 在轨道里快速定位目标线程一个复杂trace打开后线程轨道动辄上百条直接找目标线程不现实。Perfetto UI右上角有一个搜索框输入线程名或进程名的关键词就能快速过滤和定位。更好用的方式是右键某个进程名在弹出来的菜单里选择Pin把这个进程固定到视图顶部固定区域这样上下滚动其他轨道时它始终可见对照分析时非常省事。我自己的习惯是先pin住目标进程的main线程再pin住CPU0轨道然后从时间轴上定位到疑似卡顿区间。这样需要关注的轨道最多也就两三条比全量打开清晰得多。3.2 点击一个slice右侧面板会给出所有关键字段在某个线程轨道上点击任意一条slice右侧详情面板会显示该slice的完整信息。这里要重点看的字段有字段含义使用场景tsslice起始时间戳确认这条slice在时间轴上的精确位置durslice持续时间判断这次CPU占用是长是短cpu运行在哪个CPU核心结合CPU轨道判断绑核/迁移情况end_state线程切出时的最终状态区分让出CPU后是继续排队还是去睡觉priority线程优先级排查高优先级线程抢占、RT线程等问题wakeup ts本次被唤醒的时间点如果存在计算唤醒到真正上CPU之间的延迟实际分析时ts和dur是最常被盯着的。比如你要判断一次触摸卡顿发生在哪个阶段就把时间轴缩放到卡顿点附近找到主线程对应的slice看它的dur是否异常长或者ts和期望的时刻是否对不上。3.3 Runnable和Running最容易看错的两个状态刚接触Perfetto的人最容易犯的错是把Runnable误当成Running。虽然两个状态下线程都在准备干活但本质区别很大Running线程真正占用CPU正在执行指令在UI里显示为比较实的深绿色条Runnable线程处于可运行状态但还没获得CPU正在排队等待在UI里通常显示为浅绿色窄条经常紧贴在Running slice的左侧。想象一下排队打饭Running是那个正在窗口打饭的人Runnable是排在他后面的下一位。下一位已经准备好刷卡了但窗口还没空出来他只能等着。系统里如果很多线程长时间处于Runnable说明CPU资源供不应求或者有高优先级线程在频繁抢占。在UI里这两种状态可以这样区分选中一段时间范围看底部统计区域里Runnable和Running的占比。如果Runnable占比显著超过Running优先怀疑CPU资源争抢。另外仔细看主线程轨道时经常能看到由Running切换到另一个线程时中间夹着一块很短的浅绿色那就是主线程在排队。排队越频繁、越宽调度延迟越明显。3.4 用wakeup链追踪到底是谁在叫我一条完整的调度链通常是这样的线程在Sleeping状态下等待某个事件某时刻另一个线程触发了唤醒操作内核发出sched_wakeup事件目标线程进入可运行队列最终在某个CPU上切换执行。这里最关键的连接点是wakeup ts和slice的起始ts之间的间隔。实操方法是点击目标线程的某条Running slice在右侧详情面板找到wakeup ts字段。如果wakeup ts和slice起始ts相差很小说明线程被唤醒后几乎立刻拿到了CPU调度延迟很低问题不在调度如果wakeup ts远早于slice起始ts比如差了十几毫秒甚至还多那说明线程虽然醒了但一直在队列里排队这时要去看当时的CPU负载情况看看是哪个线程占着核心不走。反过来的情况也值得注意如果一条slice结束后线程进入Sleeping并且之后很长一段时间都没有新的wakeup信息说明唤醒动作本身就没有及时发生。这时候要去查是谁应该负责唤醒却迟迟没做通常是等锁、等某个回调或者定时器没到点。沿着这个思路不断往上追最终一定能追到一个具体线程的具体动作。4. 从调度事件到性能结论几类高发问题的识别套路4.1 主线程长时间RunnableCPU资源被抢占先说一个非常典型的卡顿形态。你打开trace找到出问题的进程主线程在卡顿时间段内主线程几乎没有长条的Running slice取而代之的是大量紧挨着的浅绿色Runnable块偶尔有短暂的Running闪过但很快又被切走。这种形态说明主线程在抢CPU过程中处于下风。原因通常有两个方向系统总CPU负载过高所有核心都被塞满主线程排不上有更高优先级的线程在持续占用CPU尤其是实时优先级RT线程它们的抢占能力远高于普通线程。排查时先把时间轴缩放到Runnable密集区找到此时占着CPU的线程是谁。如果是RT线程看它的priority字段和运行时长再确认它是否有持续高频运行的必要。很多音频处理框架里的RT线程在异常情况下会疯狂占用CPU把主线程挤到排队。这个问题光看业务代码不好发现但调度图上一目了然。4.2 大面积Uninterruptible SleepIO才是元凶另一种常见的异常形态是线程轨道上出现成片的红色长条。在Perfetto里Uninterruptible Sleep不可中断睡眠通常用红色标识可以直接理解为线程在等一个必须等完的内核IO操作。这种状态最常见的来源是磁盘和网络IO。进程发起了read或write请求内核把请求提交给块设备后线程进入D状态等待IO完成。如果设备响应慢线程就一直挂着表现为红色长条。定位思路也比较直接找到大段红色块所在时间段向上看该线程的调用栈或者看同时段其他线程的IO相关事件再结合trace里是否采集了block事件的ftrace数据来判断具体是哪个文件、哪个设备。这里有个实操提醒基础配置里没有含block类事件如果怀疑IO问题记得回来把block_block_rq_issue和block_block_rq_complete加进ftrace_events重新录制不然只有D状态没有IO事件只能猜是IO问题但看不到完整证据链。4.3 唤醒延迟异常定时器与锁的另一面有些卡顿既不是CPU满载也不是IO阻塞而是线程被唤醒得太晚。这种问题从主线程窗口看卡顿段内没有明显的Runnable堆积反而是一段较长的Sleeping直到临界点才突然出现一条Running slice。这时候要去对比wakeup ts和slice起始ts。如果wakeup本身发生得很晚说明唤醒源有问题。最常见的唤醒源有三类定时器、锁、跨线程消息。Perfetto trace里如果同时采集了sched_process_hang或者其他同步事件可以直接看是哪个线程持有锁时间过长如果没有这些事件就回到主线程的wakeup pid字段挨个检查唤醒者线程当时在干什么。我踩过不少次这个坑一开始总以为是主线程慢后来发现是某个后台线程持有锁太长时间主线程一直在锁上睡眠。只看主线程轨道永远只能看到一行Sleeping必须沿着wakeup链找到那个磨磨蹭蹭的持锁线程问题才能破。4.4 用选区统计把感觉变成数字有时候视觉上觉得某段时间不对劲但说不清到底异常在哪。这时可以拖拽选中一个可疑时间范围看底部统计面板选中区域内slice总数、总时长、每个状态的时间占比都会列出来。我的做法是先选一个正常时间段做基准再选一个卡顿时间段做对比看两组数据的差异。比如正常段Runnable占比只有2%卡顿段暴增到30%那是调度延迟问题再比如正常段主线程Running slice平均长度是5ms卡顿段变成了0.5ms那是频繁切换问题。有了数字做支撑和开发同学对齐时能少很多无谓的争论。5. 进阶用Trace Processor SQL把调度数据变成可查的表格5.1 SQL面板在哪能干什么Perfetto内置的Trace Processor系统化地把trace里的所有事件解析成了SQL表UI左下角或者右下角有一个QuerySQL面板在里面可以直接写SQL查询。这是把调度事件从看图升级到统计分析的关键入口。常用的两张表是sched_sliceUI中通常简称为sched和thread_state。sched表里每一行是一条调度slice字段有ts、dur、cpu、utid、end_state等thread_state表则更细把每个线程每一瞬间的状态变化都记录了下来state字段直接标注了是Running、Runnable还是Sleeping。5.2 三个实用查询模板第一个查询统计某进程所有线程在各状态上花了多长时间SELECT thread.name AS thread_name, thread_state.state, SUM(thread_state.dur) / 1000000 AS total_ms FROM thread_state JOIN thread USING (utid) WHERE thread.upid ( SELECT upid FROM process WHERE name com.example.app ) GROUP BY thread.name, thread_state.state ORDER BY total_ms DESC;这个查询能把一个进程的各个线程状态分布拉成一张表一眼看出哪个线程大部分时间在Running、哪个线程一直在Sleeping、哪个线程Runnable占比异常高。第二个查询找出指定时间段内Runnable时间最长的线程SELECT thread.name AS thread_name, cpu, COUNT(*) AS runnable_pieces, SUM(dur) / 1000000 AS runnable_ms FROM thread_state JOIN thread USING (utid) WHERE state GLOB R* AND ts BETWEEN 500000000 AND 600000000 GROUP BY thread_name, cpu ORDER BY runnable_ms DESC LIMIT 20;state GLOB R*的意思是匹配R和R两种Runnable状态。执行完这个查询当前时间范围内所有排队等待CPU的线程就都浮出水面了。第三个查询查找某线程连续运行超过10毫秒的sliceSELECT ts, dur, cpu, end_state FROM sched WHERE utid ( SELECT utid FROM thread WHERE tid 12345 ) AND dur 10000000 ORDER BY dur DESC;这个查询常用于找长耗时slice。如果主线程在卡顿点附近出现了超长slice说明是单次执行时间过长如果没有超长slice但状态统计显示总运行时间很高那问题就是碎片化运行导致的整体延迟。5.3 用SQL算调度延迟wakeup到switch的间隔前面提到wakeup ts和slice起始ts的时间差也可以直接用SQL算。把sched_wakeup事件和sched_slice按utid和时间点关联就能批量计算这些延迟SELECT w.utid, thread.name AS thread_name, (MIN(s.ts - w.ts)) / 1000000 AS min_wakeup_to_switch_ms, (AVG(s.ts - w.ts)) / 1000000 AS avg_wakeup_to_switch_ms FROM instant w JOIN thread ON w.utid thread.utid JOIN sched_slice s ON w.utid s.utid WHERE w.name GLOB sched_wakeup* AND s.ts w.ts GROUP BY w.utid, thread.name ORDER BY avg_wakeup_to_switch_ms DESC LIMIT 20;这个查询跑完哪些线程平均要等很久才能从唤醒状态切换到Running直接被排序暴露出来。不过注意不同Perfetto版本的instant表结构可能会有些差异如果跑不对优先查看该版本文档里Trace Processor的Schema定义调整表名和字段名即可。6. 常见问题与避坑速查6.1 trace里看不到任何slice全是空白优先检查录制配置。ftrace_events漏了sched_switch或者buffer设得太小导致事件被丢弃都会造成空白。另外如果录制时系统正处于深度睡眠状态比如手机灭屏大部分核都idle从调度视角看自然一片安静。先做一个唤醒屏幕再操作的动作看看slice是否出现。6.2 时间轴上出现明显的时间断层相邻两个slice之间的时间戳跳变过大通常是trace buffer溢出丢事件了。解决办法是加大buffers.size_kb比如从64MB调到256MB或者缩短duration_ms。排查高负载场景时宁可录短一点也要保证录制完整性。如果一直丢失还要确认是不是录制工具本身在高负载下CPU抢占严重考虑先限制录制系统上的其他负载。6.3 state字段的含义在不同版本里略有差异Perfetto版本更新迭代很快thread_state表里的state取值在不同版本之间可能有细微差别比如R和R的分界、U和D的区分方式。遇到不确定的取值直接去Trace Processor文档里查当前版本Schema的枚举值定义不要凭经验硬套。6.4 主线程明明在跑但UI上看不到长条有时候主线程的slice很多但都是碎片化的极短条看起来像虚线。这种情况说明主线程在频繁让出CPU通常是遇到了锁竞争或者被高优先级线程打断。视觉上很容易误判成主线程没干活这时候结合SUM(dur)按时间范围统计一下主线程总运行时间和期望值对比差距就是被抢占或者等锁的损失。再补充两个实用小技巧。第一个Perfetto UI中选中一个slice后按F快捷键可以直接聚焦放大到该slice双击也能达到类似效果比手动缩放精准得多。第二个右键轨道选择Filter可以快速隐藏无关轨道分析时只保留目标进程和CPU轨道能大幅降低信息噪音。我个人在实际操作中最深的体会是看CPU Scheduling events一定要带着问题去看不要漫无目的地扫。每次打开trace先问自己三个问题——目标线程在哪段时间不在Running那段空档里它是处于Runnable还是Sleeping如果Sleeping最后是谁、在什么时间把它唤醒的把这三个问题的答案串起来基本上所有调度相关的性能问题都能落到一个具体的线程和一个具体的时刻上。Perfetto官方文档其实已经把底层机制讲得很清楚了剩下的功夫就是多上手看真实trace看得多了那些彩色条和状态字段自然而然就有直觉了。