ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Perfetto CPU调度事件全解析:从彩色方块到唤醒延迟排查

Perfetto CPU调度事件全解析:从彩色方块到唤醒延迟排查 打开Perfetto的那一瞬间绝大多数人的第一反应是这些花花绿绿的小方块到底是什么尤其是当你第一次打开一份带了完整ftrace信息的trace看到那一条条CPU轨道上密密麻麻的彩色切片再看到某个线程的轨道上突然冒出一段段颜色不同的条带旁边还飞着一些箭头——说实话我当年第一次看这个界面整整发呆了好几分钟完全不知道从哪里下手。后来我耐着性子把Perfetto官方文档里关于“CPU Scheduling events”的内容逐字啃了一遍又结合自己手里那些卡顿、启动慢、CPU跑不满的trace反复对照才算把这些彩色方块、状态色带、小箭头彻底看明白。这份文档解决的核心问题其实只有一个当CPU上没有你的线程在运行的时候这中间的每一毫秒到底发生了什么。这篇文章就是把我读文档的笔记和实际排查经验合并在一起按“事件本身是什么——怎么抓到——在UI里怎么认——怎么用SQL算”这条线完整过一遍。我说的这个思路不只是给做Android性能优化的人看的凡是跟Linux调度沾边的工作——服务器上排查延迟抖动、嵌入式设备上分析负载不均、游戏引擎里盯渲染线程的空转原因——这套方法全都通用因为底层的sched事件大家都是一样的。1. 为什么非看Scheduling events不可官方文档的定位拆解1.1 一条slice只能说明“发生了什么”调度事件才能回答“为什么”先明确一个基本概念。你在Perfetto里看到的那种横跨时间轴的色块官方叫slice它表示的是一段时间内某个对象占用了某个资源。最常见的slice就是CPU轨道上的那些小方块——每一条调度slice记录的是某个线程在某个CPU上从开始运行到被切换出去的这一段连续时间。只看slice本身你能得到的信息是“线程A在0到10毫秒内占用了CPU 0”。这回答的是“发生了什么”。但如果你的线程在5毫秒的时候被切出去了到15毫秒才重新回来这中间的10毫秒它在干什么在等什么是主动睡的还是被中断打断了还是被其他线程挤下去的只看调度slice是永远看不出来的。这时候就必须去看更底层的调度事件。官方文档专门把CPU Scheduling events拉出来单独成篇理由就在这slice是内核调度器动作的“结果”而调度事件是“过程”。排查问题如果只看结果很容易误判——你看到线程没跑CPU下意识以为它在摸鱼结果点开事件一看它早就被唤醒ready了等了好几个毫秒才被调度器真正放到CPU上跑这完全是另外一类问题。1.2 官方文档里“CPU scheduling events”到底讲了哪几件事那份文档看起来篇幅不大但把调度这条线的核心要素都点到了。我把它的要点用自己的话整理了一遍主要有这几块。第一部分是sched_switch事件。这是最早在内核里就有的tracepointtracepoint是内核预埋的跟踪点用ftrace就能抓每次CPU从一个线程切换到另一个线程的时候都会触发。它记录的不是“当前在跑谁”而是“刚才跑的是谁”“马上要跑的是谁”“被切出去的那个线程当时处在什么状态”。这几个字段尤其是prev_state决定了你分析中断、抢占、睡眠问题的所有线索来源。第二部分是sched_wakeup和sched_waking这一对唤醒事件。线程被别人唤醒的时候事件里会记录唤醒者是谁、被唤醒者是谁以及发生唤醒的那个瞬间的CPU编号和时间。这一对事件是定位“唤醒延迟”和“等待调度延迟”的关键——没有它们你根本没办法把从“线程可运行”到“线程真正上CPU”这两段状态切分开。第三部分是UI怎么呈现这些事件。调度slice变成了CPU轨道上的彩色方块线程状态变成了track上的色带唤醒事件变成了一条条从唤醒者指向被唤醒者的灰色箭头。文档告诉你这些元素叫什么名字、什么颜色对应什么含义但文档不会告诉你的是实际分析时最优路径是先在UI上肉眼定位可疑区间再用SQL把具体数值算出来两套手段缺一不可。最后一部分是官方给的一个SQL示例。Perfetto自带的trace_processor支持用SQL查询所有解析出来的事件数据官方用这个能力演示了怎么把唤醒事件和时间轴上的slice关联起来计算“从唤醒到上CPU花了多久”。这个思路极其有用下面实操里我会展开讲。2. 调度事件的完整数据链路与核心概念2.1 事件族图谱sched_switch/sched_waking/sched_wakeup 各管哪一段Linux内核的调度器每个时钟周期都在做选择但真正对应用层表现有影响的动作就那么几个。Perfetto里能看到的调度相关tracepoint按时间顺序串起来是这样的。先说sched_switch。一个CPU上发生上下文切换时触发事件名四个字段prev_comm和prev_pid是被切出的线程prev_state是被切出线程当时的状态next_comm和next_pid是被切入的线程。这个事件标志着“一个runqueue上的切换动作完成了”。我把它理解为CPU执行时间的接力棒正式交到了下一个线程手里上一棒运动员离场时的状态都被记录下来了。prev_state尤其关键它决定了上一棒是自愿交棒的比如Sleeping也就是主动睡眠还是被强行换下去的比如Running——说明它本来还在跑是被更高优先级的任务抢占了CPU。再看唤醒事件。这可能是新手最容易搞混的一对。sched_waking和sched_wakeup其实描述的是同一个事件的两个阶段waking发生在唤醒者试图唤醒目标线程的瞬间sched_wakeup则在目标线程被实际拉入runqueue后触发。为什么有两个历史上不同内核版本、不同架构实现有过差异Perfetto把两个都抓进来。你用的时候不必纠结选哪个只要知道这两个事件都能给你“谁在什么时间点了谁”的信息字段里带着target_pid而wakeup类事件记录唤醒者的pid和运行的CPU。值得一提的是sched_process_exit和task_newtask。前者在线程退出时记录后者在新线程被创建时记录。这两个事件单独看不值钱但配合sched_switch能做进程生命周期完整画像我曾经靠task_newtask的时间戳定位过一个“应用频繁创建线程导致CPU抖动”的问题比反复看启动日志靠谱多了。2.2 线程状态机Sleeping/Runnable/Running/Uninterruptible Sleep要理解Perfetto UI里那些色带必须先理解线程状态。很多排查新手栽跟头就是没搞懂为什么同一个线程的时间轴上有灰、有蓝、有绿、有橙凑在一起像一条彩色围巾。线程的基本状态流转就几个可运行Runnable也就是Ready指的是这个线程已经满足运行条件正在runqueue里排队等CPU运行Running指的是它正在某个核上实际执行睡眠Sleeping指的是它自愿挂起等某个条件满足比如等锁、等IO不可中断睡眠Uninterruptible Sleep这块比较特殊它通常意味着线程在做同步IO或等待驱动里特定的不可被打断的操作这种状态下线程不响应普通唤醒。这里面最容易被忽略的是Runnable和Running的区分。只看调度slice你只能看到Running的部分看不到Runnable的部分。但Runnable恰恰是“线程准备好跑了却轮不上CPU”的状态——这是调度延迟的直接来源。Perfetto官方文档里那张状态说明图我建议你多看几遍把每个状态对应的UI颜色和事件来源记清楚。实际分析的时候你一眼看到线程轨道上有一长段蓝色Runnable就要警觉这段时间的线程在想跑却跑不了这就是延迟。2.3 从事件到UIPerfetto的track怎么拼出来的从一堆原始tracepoint到屏幕上的track中间经历了解析和聚合两个阶段。Perfetto的trace_processor负责把ftrace格式的sched_switch事件解析成一张名叫sched_slice的SQL表这张表每一行就是一段调度slice字段包括utid线程唯一ID、CPU编号、开始时间ts、持续时长dur还有上一线程退出时的状态end_state。除了sched_slicetrace_processor还生成了一张更接近“线程神仙视角”的thread_state表这张表直接把每条线程的状态变化整理成一段一段颜色和状态字段直接对应上一条说的那几种状态。CPU轨道和线程轨道就是这么来的CPU轨道的每个方块来自sched_slice线程轨道的每个色带来自thread_state。两者底层数据来自同一批事件但视角不同。CPU轨道回答的是“每个核都被谁占了”线程轨道回答的是“这个线程自己的状态怎么变”。官方文档里强调这两套track目的就是让你通过交叉比对确认线程状态变了到底是它自己主动变还是被外力挤下去的。唤醒事件的呈现形式是箭头。UI上唤醒箭头张得非常直观——从唤醒者的slice边缘延伸出去指向被唤醒线程time轴上对应的时间点。箭头本身在SQL里并不单独成行它其实是把waking/wakeup事件按照时间对齐画出来的。看懂这个箭头你就补全了线程状态链上最后的一环它从Sleeping变成Runnable的这个瞬间是谁拉了一把。3. 实操从采集到拿到一份带sched事件的trace3.1 采集trace的两种方式UI Recorder和命令行先说最省事的直接打开Perfetto的网页版UIui.perfetto.dev左侧的“Record new trace”标签页就是图形化的配置器。这里有四个决定性选项直接关系到sched事件的质量。Recorder配置里找到“Ftrace events”一栏展开后勾上sched/sched_switch、sched/sched_waking、sched/sched_wakeup_new这几个。在“Additional buffering”里把In-memory buffer size拉大我建议至少用到默认值的两倍也就是64MB往上。再把Max duration设置到10秒以内第一次抓trace千万不要一上来就录60秒。命令行方式适合需要自动化、或者目标机器不方便打开网页的场景。Perfetto命令行工具官方release包里可以直接下载在Android设备上通常直接用系统自带的/system/bin/perfetto即可。命令行的配置文件是一个protobuf文本格式的config写起来很简单下面这份是我日常排查调度问题用的基础配置buffers { size_kb: 65536 fill_policy: RING_BUFFER } data_sources { config { name: linux.ftrace ftrace_config { ftrace_events: sched/sched_switch ftrace_events: sched/sched_waking ftrace_events: sched/sched_wakeup ftrace_events: sched/sched_wakeup_new ftrace_events: sched/sched_process_exit ftrace_events: sched/sched_process_free ftrace_events: task/task_newtask ftrace_events: task/task_rename buffer_size_kb: 8192 drain_period_ms: 250 } } } duration_ms: 10000配置里的意思从字段名就能猜个大概buffer总量64MB、只抓调度相关的tracepoint、每个CPU的ftrace ring buffer是8MB、每250毫秒drain一次、总共抓10秒。我故意没开sched_blocked_reason这种重型事件因为它会显著增加trace体积第一次排查不需要。另外如果设备上应用上下文切换太频繁10秒的trace可能到200MB以上这时先在配置里把duration_ms缩短到3到5秒或者只抓目标进程相关的线程组后面再逐步扩大。采集命令也很简单。命令行工具方式是这样的# 先把上面的配置存成 sched.cfg # 直接抓trace perfetto -c sched.cfg -o /data/local/tmp/trace.perfetto-trace # 在PC上等抓完再把文件pull回来 adb pull /data/local/tmp/trace.perfetto-trace另外Perfetto的UI Recorder页面其实会生成等效的命令UI上有一个“VIEW TRACE”和“ADB”按钮一键抓取后直接自动打开。UI方式抓Android设备trace时它会通过adb直接调用设备的perfetto不需要在PC上装任何东西对日常验证来说是最顺手的。3.2 抓取后第一件事确认sched事件真的在trace里这是一个特别容易翻车的环节。经常有人抓完trace兴冲冲打开发现整个文件只有process列表和一些binder调用CPU轨道上干干净净原因就是ftrace采样其实没生效事件根本没抓到。这个坑我不知道踩了多少次所以我把“确认事件”放在了非常靠前的位置。最简单的确认方法是在UI里看一眼有没有sched相关的数据。打开trace后左侧Vars选项卡里应该能看到一个叫“CPU Slices”的track组里面每个CPU一行如果这个组是空的那ftrace肯定没开对。更精确的方式是打开UI底部的“Query”面板或者用独立命令行工具跑一句SELECT count(1) as c, name FROM ftrace_event WHERE name GLOB sched* GROUP BY name ORDER BY c DESC;ftrace_event这张表如果查不到说明你的trace里根本没有原始ftrace表。那就回头检查采集配置确认data source那边不是只选了进程信息采集而没有选ftrace。还有一类情况是事件抓了但很少比如只抓到了几十条sched_switch——那通常说明你没有拿到root权限或没有授予相应的SELinux权限perfetto只能抓到部分进程的调度信息。Android的老版本或者限制比较严格的环境里普通shell权限可能不够这时候需要确认是不是要临时给perfetto提权或者用系统级抓取方式。3.3 下载与打开trace的常见坑说起Perfetto相关的下载问题很多人问的最多是“怎么下载”。这里一次性说清楚UI本身不用下直接打开https://ui.perfetto.dev 就能在线分析trace是完全本地解析的不用担心上传。命令行工具和trace_processor需要下载到官网的release页面或者项目GitHub releases页面都能拿到对应平台的压缩包。Android设备上的perfetto一般是系统预装的如果版本太老可以把PC上解压出来的perfetto push到设备里用但注意架构要匹配。打开trace最常见的坑就是文件太大。一个带sched事件、录了30秒的trace轻轻松松超过1GB如果在浏览器里打开UI可能要卡几十秒甚至直接无响应。处理方案我给两个。第一个是录的时候就控制规模只录问题复现的那几秒第二个是如果已经有一个巨大的trace可以用命令行工具做裁剪或转换成轻量级的SQL方式分析先查数据再在UI里只加载部分时间窗口。另外chrome浏览器在打开超大文件时内存占用比较恐怖建议用独立的trace_processor做预处理别硬交给前端。4. 在UI里怎么看才能一眼定位调度问题4.1 CPU Track先看谁在跑再看谁被赶走打开一个正常的trace最上面的区域就是一排CPU Track每个CPU一行跑在上面的线程用带线程名的彩色方块铺满时间轴。分析第一步永远是先找到目标线程然后顺着它出现在CPU轨道上的那一串片段确认它在关键时刻跑了哪几个核每个片段持续了多久。这有个实用的交互细节点击一个CPU slice时右侧会把对应sched事件详情面板拉出来能看到prev_comm、next_comm、prev_state这些字段。但高性能CPU上slice非常密集肉眼逐个点不现实。所以我的习惯是先把时间窗口缩小到可疑区间比如用户反馈卡住的那几秒再按进程名过滤。UI右上角的搜索框可以直接搜线程名搜到之后会直接把时间轴跳过去并高亮它的所有slice。看CPU Track要建立的第一个直觉是你的线程不在CPU上的时间跟你线程在CPU上被切走的时间一定要分开看。如果问题区间里目标线程压根没出现在任何CPU Track上说明它不在Running状态直接跳到线程状态Track去看如果目标线程在CPU上被切换走的prev_state是Running那就是被别的线程抢占了。4.2 线程状态Track五种颜色背后的调度声誉严格说Perfetto没有“携带五种颜色”的固定官方常量不同版本配色略有差异但整体遵循一套约定。我基于长时间看trace的经验整理一份对照表状态常见配色含义数据来源Running绿色/亮色正在CPU上执行sched_switch next字段RunnableReady蓝色/青色已就绪、在runqueue排队sched_wakeup到sched_switch之间的gapSleeping灰色主动睡眠、等待事件prev_state SInterruptibleUninterruptible Sleep深红/暗色不可中断的等待通常与内核态IO相关prev_state DPreempted被抢占橙色系被更高优先级任务打断prev_state R 且被切换Thread State Track也就是线程状态轨道是Perfetto非常标志性的一种视图。它把每条线程的时间轴按状态分段铺开一眼就能看出这个线程的生命节奏。我看线程状态轨道有个固定顺序先找绿色段确认什么时候真在干活再看蓝色段确认它在排队然后点开排队前的灰色段看它究竟睡到什么时候醒的。如果一段蓝色特别长说明调度器迟迟没把它放上CPU这时候点蓝色段起始点附近通常能看到一条来自某个线程的唤醒箭头——顺着箭头找到唤醒者是谁。这里要特别提醒不要只看“绿色多不多”就下结论说线程忙不忙。线程状态轨道里绿色长只说明“拿着CPU不放”不代表“干的事都是有效工作”。一个自旋等待锁的线程状态会长时间是绿色Running但它实际什么都没做。所以线程状态轨道必须跟唤醒箭头、锁竞争的事件一起看绿归绿未必高效。4.3 唤醒箭头从wait到run之间发生了什么唤醒箭头是Perfetto里最有信息量、也最容易被忽略的元素。它出现在两种场景一个线程从Sleeping状态醒来或者从一个CPU上被唤醒到另一个CPU上。箭头起点是唤醒者或者一个CPU slice终点是被唤醒线程的状态转换点。箭头上悬停可以看到唤醒事件的时间戳、唤醒者的PID和名称。沟通个经验看到目标线程由蓝转绿也就是从Runnable变成Running的那一瞬间一定倒推去看它前面是什么时候被唤醒的。如果唤醒到上CPU之间隔了很长问题大概率出在调度器侧——比如target CPU的runqueue里排了太多线程或者目标线程被绑核在某个特别忙的核上。如果唤醒事件本身出现得很晚问题则出在唤醒者侧——唤醒者自己忙不过来迟迟没发出唤醒。还有个小细节唤醒箭头不总是从线程slice直接出发的有些内核唤醒事件会从一个CPU Track出发因为唤醒者自己可能已经不存在了比如刚退出。这种情况下别慌把鼠标放到箭头上看事件的target pid再全局搜这个pid的线程状态一样能拼出完整的因果链。4.4 SQL三板斧最常用的三个调度查询UI肉眼看肉眼量化还得靠SQL。Perfetto的trace_processor主推的就是SQL查询能力在UI里打开Query面板就能执行。下面这三个查询是我每次分析调度问题几乎必跑的“三板斧”。第一板斧查目标线程在指定时间窗内的所有调度sliceSELECT ts/1e6 AS ts_ms, dur/1e6 AS dur_ms, cpu, end_state FROM sched_slice WHERE utid (SELECT utid FROM thread WHERE tid 1234) AND ts BETWEEN 5000000000 AND 6000000000 ORDER BY ts;这条查询回答的是这个线程在哪段时间、在哪个核上、跑了多久以及被切出去时是什么状态。end_state字段就是sched_switch里的prev_state这是判断“主动睡还是被抢占”的最直接依据。第二板斧统计整个trace里每个进程的总运行时间看谁占CPU份额SELECT process.name AS process, CAST(SUM(sched.dur) / 1e9 AS INT) AS total_running_s FROM sched_slice sched JOIN thread USING (utid) JOIN process USING (upid) GROUP BY upid ORDER BY total_running_s DESC LIMIT 20;这个查询的结果可以帮你快速判断一段时间内的CPU资源去向。如果目标应用自己没进前几名但它需要的核一直被别人占着那答案就很明显了。第三板斧计算从唤醒到真正上CPU的延迟。这一步稍微绕一点。我需要先用raw表或者instant类事件找到唤醒的时刻再和sched_slice里该线程第一次上CPU的时刻做差。具体表结构不同版本略有差异但整体思路是这样的-- 找到目标线程在某个窗口内的唤醒事件 SELECT ts/1e6 AS wake_ms, name AS wake_reason FROM ftrace_event WHERE name IN (sched_waking, sched_wakeup) AND string_value 目标线程名 AND ts BETWEEN 5000000000 AND 6000000000;然后把结果输出复制到下一个查询里跟sched_slice对齐。SQL的妙处在于你可以把这两步合并成一条嵌套查询不同版本的字段名会有差异所以我更推荐先在UI里点一下唤醒箭头看数值再回到SQL里验证。5. 高频排查场景实录从事件看原因5.1 主线程卡顿Runnable状态堆积时间太长我自己遇到最多的情况就是应用主线程卡顿。这种问题的trace表现往往很有辨识度主线程的thread state轨道上某一段灰色睡眠之后接了一段很长的蓝色Runnable然后才变绿Running。蓝色的那段看起来很短但换算成毫秒可能已经卡了好几百毫秒。看这种trace第一步是确定蓝段的起点。蓝段起点如果是sched_waking事件带进来的说明线程在等CPU如果起点本身就是一个进程创建点或者锁唤醒点那问题更可能在锁竞争上。有一回我排查一个列表滑动掉帧问题发现主线程每次蓝段都有几百毫秒查看唤醒者发现是渲染线程不断唤醒主线程但主线程所在的大核被另外一个高优先级音频线程占着——这就不只是调度配置问题了还涉及线程优先级和cpu affinity的联动。处理这类问题的思路是先在UI上圈出最长的蓝段再用SQL确认蓝段总时长占时间窗的比例然后看这段蓝段前后对应的是谁在占用目标CPU。如果CPU被占得死死的要考虑调整优先级、绑核或减少目标CPU上的其他线程如果CPU其实闲得很那问题可能是cgroup调度组里的限制这些就要结合sched_attribute之类的数据源往下查了。5.2 启动慢唤醒到第一次执行之间的“调度延迟”应用启动慢的trace最经典的就是主线程被唤醒得早、但真正开始RUN起来却晚了很多。这种场景下你看主线程的thread state轨道会看到一个点主线程第一次出现可运行状态的时间点往往在进程冷启动时更早——早得超出预期。但从这个点往下到第一次出现在CPU Track上中间隔了一大段。之前我排查过一个flutter应用启动卡顿发现主线程sched_wakeup在time100ms就发生了但第一次上CPU的时间是230ms这个130ms几乎全花在了两个大核的runqueue上。而且那几个大核一直被系统的binder线程池占着每个slice都只有几百微秒频繁切换导致主线程根本排不进去。后来用SQL统计了大核上所有线程的slice数量和平均时长确认“碎片化占用”之后把主线程绑定到小核上运行反而启动更快了——因为小核虽然单核性能弱但runqueue非常短。所以启动慢不要只盯“主线程跑得慢”要看它“等CPU等了多久”。调度slice只能告诉你它跑起来以后用了90ms唤醒事件才能告诉你它在跑到第一个slice之前还渡过了130ms的等待时间。排查时一定要把这两个数字分开看。5.3 核心空转但线程不跑affinity与负载均衡问题还有一种很迷惑的情况你发现某几个CPU轨道上空空如也几乎没线程在上面跑但目标线程却一直处于Runnable状态上不了CPU。这种“局部CPU空闲局部排队”的组合十有八九是绑核或者调度域配置问题。遇到这种情况先看目标线程的调度slice都落在哪些CPU上。如果它只在CPU 4和5上出现过那基本确定它的cpus allowed被限制了。接着看线程创建时候的task_newtask事件里有没有记录affinity信息再结合/proc/pid/status里的Cpus_allowed_list这个信息trace里不一定直接有但我通常会adb pull一份参考。如果只有一部分大核被限制还牵涉到EAS和以及不同核的capacity差异——总之这是调度域层面的问题不是简单的“CPU不够”。5.4 诡异的Uninterruptible SleepD状态线程怎么查线程进入不可中断睡眠Uninterruptible Sleep也就是D状态是最让人头疼的调度问题之一。它的可怕之处在于D状态下线程不响应普通唤醒信号看起来就是死了一样。Perfetto里排查D状态线程核心是找到线程轨道上那段深红色条带的起始点和持续时间。深红条带前的唤醒无关、抢占无关它通常是被某个内核操作卡住。D状态最常见的元凶是存储IO和驱动等待尤其在某些文件系统回写或内核模块里做同步等待时。遇到D状态第一件事是确认它持续多久——短促的D状态可能是正常IO超过几秒就要怀疑有死锁风险。第二件事是看在D状态前后线程有没有和特定的binder transaction关联以及对应进程的IO事件。不过这里要注意sched_switch事件里的prev_state D只是告诉你它进入了D状态要找到卡住的根因往往需要同时抓block层的IO事件或者用户态strace单靠sched事件是不够的。但调度事件至少能帮你快速圈定嫌疑线程和窗口避免大海捞针。6. 常见问题与避坑实录6.1 trace太大、时间太长怎么处理这个是新手的头号困扰。录了20秒sched事件出来一个600MB的文件UI打开就是来回转圈。我的建议分三层。第一层在采集侧先小规模试抓三秒确认问题和事件都在再把时长拉到目标窗口的1.5倍。第二层在UI侧打开超大trace时可以先等它解析完然后用左侧时间轴拖到目标区域一次只渲染一小段会流畅很多。第三层是终极方案直接把trace文件交给trace_processor命令行走SQL分析拿到结果再决定要不要在UI里看细节。trace_processor是纯本地的只要内存够就不会有网页端那种卡顿。还有一个技巧录trace的时候可以把无关进程过滤掉。Perfetto的命令行配置里可以指定只抓某个pid和它的子线程组比如通过target_buffer配置配合进程选择可以显著降低sched事件总量。像耗时分析这种场景把范围缩到目标包名下其他进程一概不抓trace体积能小一个数量级。6.2 时间戳对不上、窗口聚合怎么看跨设备分析比如同时抓了系统日志和Perfetto trace的时候最容易遇到时间戳基准不一致的问题。UI上会默认显示设备的单调时钟boottime但如果你用Android系统的logcat时间跟它对会发现有偏移。处理办法是在trace里找一条带有实时时钟标记的event比如ftrace事件里的print事件或者binder_transaction里带的时间用它来换算两个时钟源的偏移量。另一个坑是窗口聚合当你缩放时间轴UI上显示的是聚合后的数据某个slice在屏幕上可能只是几个像素这时候点它看到的时间范围和你心里想的实际窗口不一样。做精确分析时一定要先放大到单个slice可点选、可读字段的级别否则很容易拿聚合数据误判。我看过有人把整条线程的几百个slice聚合在一起得出了“这线程CPU占用率100%”的错误结论实际它只是切换频繁。6.3 别把sched slice直接当“CPU占用率”这是一个宣传出去会挨打的低级错误但真的很多人犯。预设一种情况你在某个线程轨道上看到它长时间绿色Running于是判断这个线程“非常忙”再配合进程管理器一看占用率还算正常两个数据打架了于是开始怀疑Perfetto不对。实际上线程轨道绿色Running反映的是它“占据CPU时间”的多少而进程管理器里的CPU占用率通常还要折算线程数、核数、采样窗口两者口径就不一样。正确的算法是把目标线程的所有sched slice的dur加起来除以时间窗总时长再除以它所在的CPU数量——如果它可以在多个核上并行跑的话。想得到线程占CPU的比例用这个公式想得到整机CPU占用率则要对所有线程的slice做一次sum再除以核数和时间窗时长的乘积。如果直接用sched slice的肉眼占比来代表系统负载做性能对比时会得出完全错误的结论。6.4 内核版本差异、tracepoint字段差异必须兼容最后一条避坑不要假设所有设备上sched事件字段都一样。老内核4.x之前的不少版本和Android vendor自己改了调度器fork出来的内核ftrace事件字段名称都可能有偏差。比如有的内核把sched_wakeup和sched_waking两个事件都开了有的只开一个有的内核sched_switch里prev_state字段是个位掩码解析方式不同。遇到trace打开后事件少、字段对不上先别急着认为是抓取失败看一眼前置环境的内核版本。还有一套实用的兼容牌在抓trace时同时开上process_stats或类似的进程生命周期事件比如task_newtask、task_rename、sched_process_exit这套东西在几乎所有内核上都是稳定的即便某个分支内核的调度事件字段变化了你也能靠进程生命周期事件搭出基本的时间轴不至于完全抓瞎。这也是官方文档里为什么建议task事件和sched事件一起抓——它们是互相兜底的两套数据源。我个人的经验是看调度事件这件事最怕的不是看不懂单个事件而是没有一套稳定的“阅读顺序”。你眼睛盯着满屏彩色条乱扫永远只能看到表面热闹。先把CPU轨道上的slice当成“结果”再顺着线程状态轨道找到Runnable的那段蓝色然后逆着唤醒箭头找唤醒者最后用SQL把时间间隔算出来——这套流程走熟练之后Perfetto官方文档里关于CPU Scheduling events那部分你基本可以略读一遍就能直接上手实战了。你手头要是正好有一份卡顿或者启动慢的trace别犹豫现在就去把“sched/sched_switch”和“sched/sched_waking”勾上按这个顺序把问题区间走一遍调度这件事的底基本就摸清楚了。
返回列表