☰
Perfetto性能分析提效实战:从40分钟到10分钟的trace定位方法论
2026/10/20 5:34:55 网站建设 项目流程

这段时间组里把性能分析这一套从头梳理了一遍,专项代号就是标题里的HQ。做之前大家手里都有Perfetto,但每次出问题,抓trace靠手快,分析基本靠肉眼在UI里拖进度条,结论靠截图丢群里。一条150MB的trace,光等UI解析就要十几秒,来回翻几遍,四十分钟就这么没了。这个专项的目标很朴素:把一次典型卡顿定位的时间,从四十分钟压到十分钟以内。文章里分享的都是这轮专项实际落地的思路、配置和教训,读完之后你可以直接抄作业,也顺便看看哪些坑不用再踩一遍。

1. 先聊清楚:Perfetto分析到底慢在哪

1.1 trace不是越多越好

我见过不少同学拿到问题第一步就是把Perfetto数据源全勾上,一把抓到几百MB甚至上GB。Perfetto UI打开大文件本身要解析,几GB的trace光加载索引就要等一二十秒,机器差点直接卡死。更麻烦的是数据源开多之后,关键线程的事件会淹没在噪音里,肉眼扫时间线的时候,焦虑感直接拉满,根本没办法聚焦。

trace的漫无目的,是分析效率的头号杀手。抓trace之前必须回答三个问题:我要复现什么现象?需要哪些系统信息才能解释这个现象?哪个事件时间范围最关键?这三个问题想清楚了再去开采集,文件大小、解析时间、分析难度都会肉眼可见地下降。Perfetto本身不是问题,问题是很多人把它当成了一个“全量录制器”,而不是一个“按需证据收集器”。

1.2 三种人三种不同的效率瓶颈

按工作场景分一下,组里跟trace打交道的人大概有三种,效率瓶颈完全不一样:

  • 应用开发同学:只关心自己App的主线程在干嘛、主线程和binder调用链有没有问题。他们往往不需要全系统的调度数据,但默认配置一把梭,抓回来的trace一半以上是无关的kernel事件。
  • 系统底座和驱动同学:关心调度、中断、CPU频率、锁竞争。他们的trace往往要开大量ftrace事件,文件动辄几百MB,分析时最怕的是UI卡顿和事件被截断。
  • 性能专项同学:要横向对比不同版本的启动耗时、卡顿率、功耗数据。他们的问题不是单条trace怎么分析,而是怎么把几十条trace的统计口径统一起来。

这三种需求,决定了我们后面所有的配置模板、脚本化SQL、metric沉淀都要分层设计。不要指望一套方案解决所有人的问题,提效专项的第一步其实是给不同角色定义不同的“默认路径”。

1.3 我们组提效前的真实基线

为了量化专项效果,我们先做了一个月统计:一个典型的卡顿问题,平均要抓3条trace,每条约200MB,从抓取到给出初步结论平均40分钟。其中真正“看分析”的时间大概只有10分钟,剩下30分钟花在:复现现场没抓到、文件太大UI打不开、反复翻时间线找那一帧、同一类问题每次都要重写一遍SQL。

我们给HQ定的目标就是两条:常规问题定位时间压到10分钟以内;重复的分析动作沉淀成脚本或者metric,不允许第二次出现“同一个坑写同一段SQL”。后面的所有工作都是围绕这两条展开的。

2. 采集配置决定分析效率:把trace抓对的四件事

2.1 buffer大小和时长的预算怎么算

Perfetto采集时数据先写共享内存缓冲,再由traced进程写盘。配置里每个buffer的size_kb如果太小,高负载场景会丢事件;如果太大,文件体积和导出时间都会膨胀。实际抓取前建议先按场景估算一下:

  • 只开linux.ftrace的sched相关事件,中低负载下大概每秒产生2~5MB数据;
  • 加上power/cpu_frequency、power/cpu_idle,大概每秒5~10MB;
  • 再把gfx、view、am、wm这些atrace category加上,每秒可能到15~30MB。

所以一个128MB的buffer,如果只抓sched事件,撑30到60秒没问题;如果全量开,十几秒就可能写满。这是一个非常粗略的估算,但足够帮你在采集前决定配置:

buffers { size_kb: 131072 fill_policy: RING_BUFFER }

RING_BUFFER模式下buffer写满后新的会覆盖旧的,适合持续抓取然后靠trigger截停;如果只想抓固定时间窗口,建议用DISCARD策略,写满就停止,保证这段trace前后完整。判断buffer够不够,最直接的方法是抓完看一眼UI顶栏有没有“lost events”提示,有的话说明事件确实丢了,下次要么加大buffer,要么砍数据源。

2.2 数据源按需选择:一张表说清楚

这是我在这轮专项里最想强调的一件事:数据源选对了,分析效率直接翻倍。我们组后来做了一套内部模板,核心就是按问题类型选数据源,不要贪多:

要分析的问题必开的数据源和事件可省掉的
应用卡顿/ANRsched/sched_switch、sched/sched_wakeup、gfx、view、input、am全量cpu_frequency、global log
App冷启动慢process生命周期相关、am、wm、gfx、viewCPU idle、irq事件
功耗/发热power/cpu_frequency、power/cpu_idle、linux.process_stats高频log、binder细粒度切割
IO/存储问题ext4或f2fs相关ftrace、block层事件gfx/view等UI事件
后台耗电power/wakeup、suspend相关、alarm相关屏幕渲染相关atrace

以应用卡顿为例,我最常用的配置是这样一个pbtx:

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_wakeup" ftrace_events: "power/cpu_frequency" atrace_categories: "gfx" atrace_categories: "view" atrace_categories: "input" atrace_categories: "am" } } } data_sources { config { name: "linux.process_stats" process_stats_config { scan_all_processes_on_start: true proc_stats_poll_ms: 100 } } } duration_ms: 15000

这种配置抓出来的trace通常只有30~80MB,UI解析基本秒开,主线程关键路径的信息一条不少。很多同学觉得“全开才安心”,其实全开之后你要花大量时间去排除无关事件,反而是最不安心的状态。

2.3 用trigger做定向截停,别等bug自己过去

有一种场景非常难受:问题不是稳定复现的,可能手机放那儿半小时才卡一次。如果一直全量抓,trace文件早爆了。Perfetto的trigger机制就是为这个场景设计的。思路是起一条长跑的后台采集,buffer用环形覆盖,配置里注册一个trigger:

triggers { mode: STOP_TRACING trigger_name: "hang_happened" stop_delay_ms: 3000 }

启动采集时不设duration_ms,而是让它待命:

adb shell perfetto -c /data/local/tmp/config.pbtx -o /data/misc/perfetto-traces/trace --wait-for-trigger

等到问题复现的瞬间,从另一个终端发trigger:

adb shell perfetto --trigger hang_happened

这样buffer里保留的正好是触发点前一段时间的高价值事件,后面的分析直接看触发点附近的窗口就行。我们组后来把所有“难复现的偶现问题”都改成这套流程,分析者不用再面对一条半个小时还全是噪音的大trace。这是采集端提效收益最明显的一个改动,强烈建议大家都试一次。

2.4 长trace分段采集的实战经验

如果确实需要观察几十秒甚至几分钟的长场景,我的经验是不要指望超大buffer硬扛。Perfetto的TrackEvent支持增量状态清理,但很多老接口和ftrace并不买账,实际效果不一定稳定。更靠谱的做法是:

  • 把场景拆成“预热段-操作段-观察段”,只对操作段做高细节采集;
  • 或者在操作点前后各留3~5秒缓冲,其余时间用低开销数据源维持一个粗略的时间线;
  • 导出时先看文件大小,超过300MB的trace先别急着扔进UI,在命令行里做一轮粗筛再决定要不要细看。

另外还有一个细节:抓完trace后先用adb shell perfetto --pull把文件拉到本地,在设备的/data分区空间紧张的情况下,大文件拉取很容易失败。尽量在抓取时把buffer和duration控制得克制一点,不要指望“先抓到再说”,采集时偷懒,分析时就要还债。

3. Trace Processor:把SQL作为分析主战场的三个台阶

3.1 第一次启动trace_processor_shell

命令行工具trace_processor_shell大概是Perfetto这套工具链里提升效率最明显的一个,但奇怪的是很多人根本没打开过。它本质上是一个基于SQLite的trace查询引擎,把整个trace的所有事件都变成一张张可查询的表,完全没有GUI加载的延迟。

启动方式很简单:

trace_processor_shell /path/to/trace.perfetto-trace

进去之后先看有哪些表:

.tables

只想快速跑一条SQL的话,也可以直接在命令里带上查询:

trace_processor_shell /path/to/trace.perfetto-trace "SELECT count(*) FROM slice;"

第一次用的时候,建议先看两个东西:slice表的事件切面、thread_state表的线程状态切面。前者记录的是应用和系统打的各个可命名的事件区间,后者记录的是每个线程的Running、Runnable、Sleeping状态切换,这两个表基本能覆盖日常80%的分析诉求。

3.2 常用表结构和三张王牌查询

slice表的核心字段是ts、dur、name、category、utid、track_id;thread表有tid、name、utid、is_main;process表有pid、name、upid。把这三张表join起来,就是最常见的分析主干。

第一张王牌是查某个App主线程上所有耗时超过阈值的事件:

SELECT s.ts / 1e6 AS time_ms, s.dur / 1e6 AS dur_ms, s.name, s.category FROM slice s JOIN thread t ON s.utid = t.utid JOIN process p USING (upid) WHERE p.name = 'com.example.app' AND t.is_main = 1 AND s.dur > 1000000 ORDER BY s.dur DESC LIMIT 50;

这张查询直接回答“主线程的时间都花哪了”,比在UI里一条条拖时间线快一个数量级。

第二张王牌是看整个系统里最忙的时刻分布。把时间切成500ms的桶,统计每个桶里所有slice的总耗时:

SELECT CAST(ts / 500000000 AS INT) * 0.5 AS window_s, count() AS event_cnt, sum(dur) / 1e6 AS busy_ms FROM slice WHERE dur > 0 GROUP BY window_s ORDER BY busy_ms DESC LIMIT 20;

输出结果后,哪个500ms窗口最忙一清二楚,然后再回UI把时间跳过去细看,搜索范围从整条trace瞬间缩小到一段。

第三张王牌是看跨进程的binder调用分布:

SELECT p.name AS process_name, count() AS binder_cnt, sum(s.dur) / 1e6 AS total_ms, avg(s.dur) / 1e6 AS avg_ms FROM slice s JOIN thread t USING (utid) JOIN process p USING (upid) WHERE s.name GLOB '*binder*' GROUP BY p.name ORDER BY total_ms DESC;

这条SQL可以把系统里所有进程的binder通信量拉个排行,锁死哪个进程在跟谁频繁交互,基本一次搞定。

3.3 用标量指标代替“肉眼找规律”

我发现很多同学对SQL模式有一种误解,觉得必须要会写非常复杂的逻辑才有用。其实在日常定位中,最有价值的反而是几个简单的标量:总耗时、最大单次耗时、发生次数、跨进程调用量的Top N。

把这些问题变成SQL之后,分析就不再是“打开UI慢慢找”,而是“跑一遍查询、看排行、再决定要不要深挖”。以前我在UI里找一次卡顿对应的时间点,少说五分钟;现在用上面第二张查询直接算出来,10秒以内。遇到特殊案例需要细看的时候,再用UI工具定位,这就是“SQL粗筛+UI精看”的组合打法,也是HQ专项里最核心的方法论转变。

4. 自定义Metric:把重复劳动固化成一条命令

4.1 metric是什么,为什么值得搞

Perfetto的metric机制本质上就是“把SQL查询结构化、参数化、可复用”。一条分析SQL你这次写了,下个月又遇到类似问题,还得从头写一遍,这其实是隐性浪费。metric的价值就是把这些SQL固化成一个名字,下次运行一条命令就能出结果,团队里其他人也能直接复用。

metric通常由三部分组成:SQL计算文件、输出结构定义、注册信息。SQL负责从trace里算数据,输出结构告诉系统结果长什么样,注册信息就是让--run-metrics认识它。日常使用中,即使不做成正式的metric,只把常用SQL存成脚本文件,已经能省下大量重复劳动。

4.2 一个可落地的例子:主线程耗时分析

我们不追求一步到位搞成正式proto,先以“脚本化SQL”的方式落地。建一个main_thread_busy.sql文件,内容就是把前面那张主线程耗时查询加上明显的注释和参数说明,然后跑:

trace_processor_shell /path/to/trace.perfetto-trace \ "SELECT ... (上述主线程耗时的查询) ..."

如果嫌每次写SQL太费劲,可以把查询写到.sql文件里,然后通过命令传入:

trace_processor_shell --query-file main_thread_busy.sql /path/to/trace.perfetto-trace

输出重定向到CSV或者JSON之后,后续的整理、画图、入库对比都顺手了。我们组现在的做法是:每个专项目录下放一组*.sql文件,命名规则是问题类型_分析维度.sql,比如jank_main_thread.sql、binder_top.sql、window_busy.sql。谁遇到同类问题直接跑文件,不许重写。

4.3 进阶:把脚本升级成真正的metric

脚本用顺手之后,可以选几个稳定高频的分析升级成正式metric。最好的候选是那些输入输出都比较固定的分析,比如“主线程busy slice Top N”“卡顿窗口分布”。正式metric的写法在Perfetto源码的src/trace_processor/metrics目录下能找到大量现成例子,照猫画虎写一个自己的SQL文件,定义一个输出,然后跑:

trace_processor_shell --run-metrics my_metric_file /path/to/trace.perfetto-trace

这一步的收益在于,metric可以进Perfetto UI的“Analyze”面板,非命令行用户也能一键看到结果。我们组后来把启动耗时分析、主线程卡顿检测、binder Top N这三类升级成了metric,应用开发的同学拿到trace之后不用再求性能组写SQL,自己就能跑出结论。

4.4 沉淀metric库的节奏建议

不要一上来就追求大而全。我的建议是先满足“自己重复劳动最多”的那一类分析,跑通一条固化一条。固化的时候多写两行注释,说明这个metric适合什么场景、大概怎么解读输出,不然三个月后你自己都忘了当初这个指标是干嘛的。Perfetto升级版本之后,schema可能在细节上有调整,metric库要在每个版本升级后跑一遍含金量高的老trace验证结果,别等到用的时候才发现某列已经改名了。

5. UI读图提效套路:别急着拖进度条

5.1 拿到一条trace先从哪看起

即便有SQL在手,UI仍然是理解调度关系、锁竞争、跨进程调用的最佳工具。但打开UI之后第一件事不是拖进度条,而是先看整体时间线顶部的“概览条”,那一段压缩的柱状图能直接告诉你:哪个时间区间系统最忙,哪个时间点有长暂停。先找到异常的区间,再点进去,这叫“从宏观到微观”,而不是从头到尾一帧帧翻。

进入有意区间之后,把鼠标悬停在主线程状态条上,Perfetto UI会显示精确状态缩写:R是Running,S是Sleeping,D是Uninterruptible Sleep。看到主线程一大片S夹着一个D,基本可以判断是在等某种内核资源;看到Runnable状态堆积,则说明线程被调度器晾着。先学会读这些状态缩写,比盯着五颜六色的色块瞎猜要靠谱得多。

5.2 过滤、标记和线程排序的聚焦技巧

UI分析很怕“什么都看得见”,所以一定要学会做减法:

  • 搜索框支持正则:直接输process:com\.example\.app|thread:RenderThread这类表达式,把所有相关track高亮出来;
  • Pin住关键track:把主线程、RenderThread、binder相关track右键固定到顶部,其他track都可以隐藏;
  • M键打标记:在怀疑出问题的时间点按M打个标记,多段窗口之间来回对比时会方便很多;
  • 善用W/S/A/D快捷键:W放大、S缩小、A/D左右平移,鼠标拖半天才到目标区间的日子可以结束了。

如果你的trace已经用SQL粗筛过,可以直接把SQL算出来的异常时间点填到UI的时间框里跳转,整个流程就变成了“SQL告诉你什么时候出了问题,UI告诉你为什么出错”。

5.3 UI的SQL查询面板和双向跳转

Perfetto UI自带一个SQL查询面板,很多人不知道。它跟trace_processor_shell是同一个查询引擎,所以在浏览器里也能直接跑查询。这个面板最有用的地方是结果集和UI时间线联动:点结果里某一行,UI会自动跳转到对应的时间位置和track,反过来在UI里选中一个slice,也能在面板里看到它的id和ts等信息,然后基于这个id继续查上下游事件。

这套双向跳转非常顺手,比如你在UI里看到一个可疑的长slice,不需要手动记时间戳,直接点它拿到slice.id,然后跑一条:

SELECT name, ts, dur / 1e6 AS dur_ms FROM slice WHERE track_id = (SELECT track_id FROM slice WHERE id = <刚才拿到的id>) AND dur > 0

就能快速找到同一个track上附近的同类事件,判断这个长slice是偶发还是周期性的。配合SQL使用的UI,才是完整的分析形态,只靠鼠标翻页真的太浪费时间。

6. 团队协作里的trace提效:从个人技巧到集体规范

6.1 trace文件的命名、归档和现场记录

个人技巧再强,不变成团队规范,效率还是起不来。我们组在HQ专项里定了一条死规矩:trace文件不允许裸放。

命名格式统一成问题简述_设备代号_系统版本_日期_序号,旁边一定放一个同名.md文件或者直接在trace描述里写清楚:复现步骤、期望行为、实际现象、关键时间点(大概几点几分出现)、涉及的核心线程。没有现场记录的trace,三个月之后再看跟废铁没什么区别,你根本想不起来当时在查什么。

导出归档之前,还要在Perfetto UI里把关键区间标注好,利用UI里自带的时间选择器标记选中一段异常区间,再导出trace。这样后续打开的人第一眼就能看到你标记的范围,不用全体重新翻一遍。

6.2 建立基线trace和指标对比

做性能分析最怕没有参照物。一个人说“这版本启动变慢了”,你得有数据告诉他慢在哪、慢了多少。我们的做法是维护一套“标准场景基线trace”:固定设备、固定系统版本、固定操作序列,每个版本发布前抓一套,并把基线指标通过SQL/metric全部跑出来存成JSON。

对比的时候,新trace和基线trace跑同一套查询,输出两个JSON,然后逐项diff。这样谁改的代码导致某段主线程耗时上涨,通常一目了然。这个流程跑顺之后,性能回归问题基本不再需要人工投入大量时间去“感觉慢没慢”,直接让数据说话。

6.3 我们踩过的坑和最终收益

这轮专项踩的坑也不少,列几个典型的:

  • target_buffer没配全:多个data source同时写buffer的时候,如果某个source没指定目标buffer,可能大家都挤在默认buffer里,事件互相挤压丢失。每个data source都明确写target_buffer,不要偷懒。
  • log数据源喧宾夺主:android.log全量开的时候,日志事件会大量占用buffer空间,把sched事件挤掉。抓sched分析时,log数据源能不开就不开,或者只开LID_EVENTS。
  • permission导致缺数据:有些ftrace事件需要特定权限,不是所有设备都抓得到。抓完先检查事件是否齐全,别分析半天发现cpu_frequency那段根本没抓到。
  • 版本不匹配:Perfetto升级后,旧trace文件用新版本工具打开一般没问题,但反过来旧工具解析新trace就可能报错或者缺列,所以工具链要尽量保持最新。

做完这些工作之后,我们的常规定位时间基本稳定在10分钟以内,偶现问题靠trigger流程也能在半小时内拿出第一版结论,最重要的是,性能组的同学从“陪别人看trace”变成“给团队提供分析工具和metric”,这大概是HQ专项里我自己最满意的一个变化。剩下的路,基本都是把新场景继续沉淀成脚本和metric的细活了。以后再有新的踩坑经历,再来更新。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询