news 2026/10/1 1:05:01

Android 10 Perfetto 命令行抓 trace 与 SQL 分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 10 Perfetto 命令行抓 trace 与 SQL 分析

1. Android 10 之后,抓 trace 的入口为什么变成了 Perfetto

1.1 还在敲 systrace 的人,大概率已经拿不到完整数据了

如果你手上的设备是 Android 10 或者更高版本,还在用systrace那条老路,会发现两个很尴尬的事情:一是脚本内部其实已经在调perfetto了,二是输出的 HTML 经常缺胳膊少腿,尤其是应用侧的gfx、view这些 category,抓出来只有系统侧的时间轴。这不是命令写错了,而是整条链路的底层实现换了。

从 Android 9 开始,Google 就在把perfetto作为统一的追踪底座往系统里塞,到 Android 10 基本完成了切换:atrace还是那个atrace,systrace还是那个systrace,但它们最终都是把参数翻译成一份 Perfetto 的 trace config,再交给traced、traced_probes这两个常驻服务去执行。真正干活的是perfetto这套东西。

对做性能分析的人来说,这意味着两件事。第一,你能采到的数据类型从"只有 ftrace 事件"扩展到了几十种 data source,CPU、内存、堆分配、Java 堆、Binder 事务、帧时间线、功耗、日志,全都塞进同一个 trace 文件里,时间轴天然对齐。第二,你必须学会写 trace config,因为perfetto不像atrace那样只接几个 category 字符串,它接的是一份结构化的 proto 配置。这也是很多人第一次用命令行perfetto卡住的地方——参数完全不知道从哪来。

1.2 设备上真正能用的几个可执行文件

先把设备端的家底摸清楚,不然你连命令敲什么都要靠猜。Android 10 及以后的机器上,跟追踪相关的可执行文件主要有这几个:

  • /system/bin/perfetto:主入口,既能当 trace 采集客户端用,也能读配置、传配置、控制后台会话。
  • /system/bin/atrace:老朋友,Android 10 上它还在,行为是往 Perfetto 上转。
  • /system/bin/tracebox:多合一二进制,靠argv[0]分派行为。很多设备上atrace其实只是它的一个软链接。R 版本之后的设备上这个基本上都能看到,Android 10 上不一定有,具体要看厂商。
  • /system/bin/trigger_perfetto:专门用来触发预设 trigger 的小工具,做"等某个时机再开始抓"的场景非常好用。
  • traced和traced_probes:不是给你直接敲的命令,是 init 拉起来的系统服务。前者负责会话管理和写文件,后者负责具体的数据源采集,通常以较高权限运行。

这里有个关键点很多人没意识到:你在adb shell里敲perfetto,并不是以当前用户身份直接去读/sys/kernel/tracing,而是通过 socket 把配置发给traced服务,由权限更高的traced_probes去干活。所以普通 shell 用户在没有 root 的 user build 上也能抓到 ftrace 数据,这是设计如此。反过来,一旦traced服务被禁用或者异常,你在 shell 里敲perfetto就会直接报连接失败,跟配置写得好不好没关系。

1.3 命令行和网页 UI 到底该怎么选

网上教程九成都在教你把 trace 拖进浏览器的 Perfetto UI 看,这没问题,但如果你只依赖 UI,有几个场景会很难受。

维度命令行方式网页 UI 方式
设备在远端、网络受限直接adb一条命令抓完需要先拉文件再上传,链路更长
批量回归、CI天然适合脚本化和自动化需要人点,无法自动化
交互式探索靠 SQL 逐层钻取,需要熟悉表结构可视化直接,缩放筛选方便
设备本地分析部分版本支持在设备上直接跑查询必须传到外部环境
学习曲线前期陡,配好之后复用性极强上手快,但难复用

我的实际做法是两个都用:命令行负责"把数据稳定地抓下来、把关键指标算出来",UI 负责"看形状、找异常点"。这篇主要讲前半段,因为它才是可以被固化成流程的部分。UI 你随时能开,但一条能跑通、能进脚本的采集命令,是要踩坑才攒出来的。

2. 把设备端的 perfetto 摸清楚:参数、配置格式与落盘位置

2.1perfetto --help里真正值得记住的开关

第一次敲adb shell perfetto --help,输出会长到让人想关掉。但其实日常真正高频用到的就那几个,先把它们记住再谈别的。

# 用文本格式的配置抓 10 秒,输出到指定路径 adb shell perfetto -c /data/local/tmp/cfg.txt --txt \ -o /data/misc/perfetto-traces/trace.perfetto-trace # 从标准输入读配置,配合 heredoc 快速试参数 adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/t1 # 配置里没写 duration 时,用 -t 兜底指定时长 adb shell perfetto -c /data/local/tmp/cfg.txt --txt -t 5s \ -o /data/misc/perfetto-traces/t2 # 后台抓,命令立刻返回 adb shell perfetto -c /data/local/tmp/cfg.txt --txt --background \ -o /data/misc/perfetto-traces/t3

几个容易忽略的点。--txt不是可选项,Perfetto 默认认为你传的是二进制 proto,配置是纯文本就必须显式声明,否则会直接解析失败。-t只在配置里没有duration_ms的时候生效,两者同时存在时以配置为准。--background会把会话挂到后台,命令马上返回,适合抓那些你不想阻塞终端的长 trace。

再往上一层,还有一组会话控制参数,做长 trace 的时候必须用:

  • --detach=key:启动一个带名字的会话,立刻返回,不占用终端。
  • --attach=key:附着到已有会话,等它结束并拿到结果。
  • --is_detached=key:查询这个会话是不是还在跑,返回值可以直接给脚本判断。
  • --dropbox=TAG:不写自定义路径,直接把 trace 丢进系统的 dropbox。这条路径会带默认的时长和体积保护,抓完不用自己清理文件。

提示:--dropbox这类走系统通道的方式有内置的 guardrail,时长和体积都被限制住了。想抓超长 trace 就不要走它,老老实实用-o指定路径。

2.2-c传配置:文本 proto 和二进制 proto 的那点区别

Perfetto 的配置格式是 protobuf。这带来一个新手很容易困惑的现象:网上抄来的配置,有的长这样,一行行带冒号;有的是一堆看不出所以然的二进制字符。前者是文本 proto,后者是序列化后的二进制 proto。

命令行场景里,一律建议用文本 proto,理由是它能改、能 diff、能进 Git。二进制 proto 的优势只在于体积小、解析快,那是给程序内部传参用的,不是给人写的。

文本 proto 的基本结构其实很朴素,就是一层层嵌套的字段。顶层的TraceConfig里有三大块:buffers(缓冲区)、data_sources(数据源)、以及各种全局控制字段(duration_ms、write_into_file、flush_period_ms等)。理解了这三块的关系,写配置就不再是背单词了。

顺手提一句字段名的版本差异。同一个数据源在不同 Android 版本上,字段名偶尔会调整,比如日志相关的配置在新老版本里命名不完全一致。所以跨版本复用的配置文件,最好在目标设备上先跑一次最小时长验证,别直接上生产脚本。

2.3 trace 文件写到哪:路径权限和 stdout 的取舍

在 Android 上,shell 用户能稳妥写进去的目录主要是/data/misc/perfetto-traces/。这个目录是专门留给 trace 的,权限已经配好了,不用自己chmod。往/sdcard写有时候也能成,但受分区格式和权限策略影响,不同设备表现不一致,不值得赌。

另一条路是写标准输出,配合-o -:

# 先把配置推到设备上,避免 stdin 被占用 adb push cfg.txt /data/local/tmp/cfg.txt # 用 exec-out 直接把二进制 trace 流回本地 adb exec-out perfetto -c /data/local/tmp/cfg.txt --txt -o - \ > trace.perfetto-trace

这条路省掉了一次adb pull,在抓小 trace、或者设备存储紧张的时候很好用。但必须用adb exec-out而不是adb shell,原因是adb shell会对输出做换行符转换,而 trace 是二进制格式,一个字节被改掉整份文件就可能打不开。这个坑我踩过,报错信息还很含糊,只说解析失败,不告诉你是传输过程被污染了。

3. 手写 trace config:从最小可用到分场景定制

3.1 一个能直接跑起来的最小配置

先给一份能直接用的配置,抓 10 秒,包含调度切换、CPU 频率和图形相关的 atrace 类别:

buffers { size_kb: 32768 fill_policy: RING_BUFFER } data_sources { config { name: "linux.ftrace" target_buffer: 0 ftrace_config { ftrace_events: "sched/sched_switch" ftrace_events: "sched/sched_wakeup" ftrace_events: "power/cpu_frequency" ftrace_events: "power/cpu_idle" atrace_categories: "gfx" atrace_categories: "view" atrace_categories: "input" atrace_categories: "binder_driver" atrace_apps: "com.example.app" buffer_size_kb: 2048 drain_period_ms: 250 } } } data_sources { config { name: "linux.process_stats" target_buffer: 0 process_stats_config { scan_all_processes_on_start: true } } } duration_ms: 10000 flush_period_ms: 5000

几个字段的作用解释一下。buffers.size_kb是全局的共享内存缓冲区,所有数据源都往里写,单位是 KB,32768 就是 32MB。fill_policy: RING_BUFFER表示写满之后覆盖最老的数据,这一条是绝大多数场景该选的策略,因为你永远不知道会抓到什么时候出问题,覆盖旧数据至少保证你能看到"最近发生了什么"。

target_buffer: 0表示这个数据源的数据往 0 号缓冲区写,如果你开了多个 buffer,可以做数据源隔离,比如把高频的 ftrace 和低频的进程统计分开,避免高频数据把低频数据挤掉。

ftrace_config里的ftrace_events是直接指定内核 tracepoint,atrace_categories是走一层高层封装,Perfetto 会自动把它展开成对应的 tracepoint 集合。你可以理解为ftrace_events是手动挡,atrace_categories是自动挡。atrace_apps用来限定只抓某个应用的 tag,不写就是抓全局的。buffer_size_kb是 ftrace 内核侧那个 per-CPU ring buffer 的大小,跟前面全局 buffer 是两个完全不同的东西,这个后面单独讲。

linux.process_stats这个数据源非常值得默认带上,它负责采集进程和线程的名字、PID/TID 映射。没有它,你打开 trace 看到的就是一堆数字 ID,全靠猜。它的开销很低,没有理由不加。

3.2 两个 buffer 不是一回事:为什么 trace 老是后半段空掉

这是命令行抓 trace 最高频的困惑:抓了 30 秒,前面 8 秒数据齐全,后面全是空白。

问题出在这条链路上有两个缓冲区串联。第一个是内核侧的 ftrace ring buffer,由ftrace_config.buffer_size_kb控制,数据先写到这里。第二个是 Perfetto 的全局共享内存 buffer,由顶层buffers.size_kb控制,traced_probes每隔drain_period_ms把内核侧的存量数据搬过来。

数据流是这样的:内核 tracepoint → ftrace per-CPU ring buffer → 按 drain 周期搬运 → Perfetto 全局 buffer → 按 flush 周期落盘(或留在内存里等读走)。

任何一个环节跟不上,都会丢数据,而且丢的位置不一样:

丢失位置表现调节参数
ftrace 内核 buffer 溢出丢的是采样点,时间轴上出现空的间隙调大buffer_size_kb
全局 buffer 被覆盖早期数据被吃掉,只剩最后一段调大buffers.size_kb
flush 间隔太长缓冲区里还没有落盘的数据在停止时丢了调小flush_period_ms
drain 周期太长内核侧先溢出,还没来得及搬运调小drain_period_ms

经验数值上,buffer_size_kb给 2048 到 8192 是常见区间,drain_period_ms给 250 比较稳,再低会增加 CPU 开销。全局buffers.size_kb需要按数据率估算,粗略的算法是:需要的 KB ≈ 数据率(MB/s) × 时长(s) × 1024。开gfx+sched的繁忙设备,数据率大致在 1 到 3 MB/s 之间,抓 10 秒给 32MB 一般够用,抓 60 秒就得往上翻。

不确定有没有丢数据?抓完跑一条查询就知道了:

select name, value, severity from stats where value != 0 order by value desc;

stats表是 Perfetto 自己记录的健康度报表,缓冲区溢出、数据源启动失败、样本丢弃都会在里面留痕。养成抓完先看stats的习惯,能省掉大量"为什么图上没数据"的排查时间。

3.3 长 trace 怎么抓:write_into_file和 detach 会话

默认情况下,trace 数据是先攒在缓冲区里,停止采样后一次性写文件。抓 5 秒、10 秒没问题,抓几分钟就会撞上缓冲区上限——不是你配得不够大,是共享内存这块就这么大,硬开太大反而会影响系统。

要抓长 trace,得开增量落盘:

buffers { size_kb: 8192 fill_policy: RING_BUFFER } data_sources { config { name: "linux.ftrace" target_buffer: 0 ftrace_config { ftrace_events: "sched/sched_switch" atrace_categories: "gfx" buffer_size_kb: 2048 drain_period_ms: 250 } } } write_into_file: true file_write_period_ms: 2500 flush_period_ms: 1000 duration_ms: 300000 max_file_size_bytes: 0

write_into_file: true是关键,它让 trace 数据在采集过程中就持续往目标文件写,而不是等结束了才一次性 dump。这样一来,全局 buffer 只需要承担两次落盘之间的数据量,配 8MB 就能撑很久。

配套要调的是file_write_period_ms。它决定多久把数据往文件里追加一次,默认 5 秒,长 trace 场景调到 2500 毫秒或者更低,能让"异常停止时丢掉的数据"这个窗口更小。flush_period_ms是把共享缓冲区刷到 writer 的周期,也可以适当调小。代价是更高的 I/O 和 CPU 占用,别一路调到 100 毫秒,那是在拿性能换性能。

max_file_size_bytes: 0表示不限制文件大小,需要限制就写具体字节数,比如max_file_size_bytes: 536870912对应 512MB。

会话控制建议配合--detach:

# 启动一个名为 longrun 的后台会话 adb shell perfetto --detach=longrun \ -c /data/local/tmp/long_cfg.txt --txt \ -o /data/misc/perfetto-traces/longrun.perfetto-trace # 想看它还在不在跑 adb shell perfetto --is_detached=longrun # 需要提前结束并取回结果 adb shell perfetto --attach=longrun

--detach的好处是命令立刻返回,会话挂在系统里,你的终端不会被占住。--attach会等会话结束再退出,适合脚本里同步等待。--is_detached的返回值可以直接写进if,用来判断长 trace 是否还在进行。

注意:开了write_into_file之后,如果进程被强杀,文件尾部可能不完整。已经落盘的部分通常还能打开,但做重要采集时尽量让它自然结束,或者用--attach正常收尾。

3.4 常用数据源速查:别每次都去翻文档

Perfetto 的数据源种类很多,但日常性能分析真正会用到的就那么十来个。列个表,照着抄名字就行。

数据源名称采什么典型用途
linux.ftrace内核 tracepoint、atrace 类别调度、卡顿、锁竞争、CPU 频率
linux.process_stats进程线程信息、内存统计建立 PID/TID 名称映射
linux.sys_stats系统级统计(部分版本)内存水位、vmstat
android.loglogcat 日志(字段名版本有差异)把日志和代码执行点对齐
android.packages_list包名与 UID 映射多用户、多包名场景
android.heapprofdNative 堆分配采样定位 native 内存泄漏
android.java_hprofJava 堆快照Java 层大对象排查
android.graphics.frame_timeline帧时间线事件掉帧归因
android.binderBinder 事务(部分版本为 ftrace 事件)跨进程调用阻塞
android.power功耗计数器(厂商支持度不一)耗电分析

有两点提醒。第一,数据源不是随便配了就能生效,部分数据源对目标应用有要求,比如堆采样通常需要目标是 debuggable 或者 profileable 的应用,否则配了也采不到数据,而且不一定会报错。第二,不同版本对数据源名称的支持情况不一样,某些名字在旧版本上根本不存在,抓之前先短时长试跑一次,比事后发现文件是空的要省事。

4. 命令行分析:trace_processor_shell 和 SQL

4.1 把分析工具弄到手

trace_processor一般不在设备上跑,而是在你的开发机上跑,处理拉回来的.perfetto-trace文件。获取方式有两种:一种是直接从上游的预编译产物里下载对应平台的trace_processor_shell,Linux、macOS、Windows 都有;另一种是如果你已经装了 Perfetto 仓库的工具链,用仓库里的脚本会自动处理下载和版本匹配。

版本匹配这件事值得单独说一句。trace_processor_shell的版本和 trace 的生成版本之间有一定兼容性要求,尤其是新版本数据源、新表结构这些。用太旧的分析器去读新设备抓出来的 trace,常见现象是表能查但少了些列,或者某些 metric 直接报错。反过来的组合问题更少。所以如果你手上工具版本比较旧,遇到莫名其妙的问题,第一反应应该是换个新版分析器再试。

还有一种情况是设备本身能跑查询。部分较新的版本里perfetto支持--query之类的参数,可以在设备上直接对 trace 做简单 SQL。这个能力挺有意思,但要注意设备端的解析器版本更旧、能用的表更少,而且大 trace 在设备上跑查询很容易把内存打满。我一般只在"想快速确认某个字段有没有采到"这种轻量场景用它。

4.2 交互模式还是一次性查询

trace_processor_shell有三种用法,各有各的合适场景。

# 交互模式:边想边查,适合探索 trace_processor_shell trace.perfetto-trace # 一次性查询:把 SQL 写在文件里执行,适合脚本化 trace_processor_shell -q query.sql trace.perfetto-trace # 起一个本地 HTTP 服务,用浏览器或 UI 连过来 trace_processor_shell -D --http-port 9001 trace.perfetto-trace

-q这个模式是自动化流程的核心。你可以把一组固定 SQL 存成文件,每次抓到 trace 就跑一遍,把结果输出成文本或者 CSV,直接进 CI 做对比。-D起服务的方式我更常用来配合本地 UI,好处是数据只在本地流转,而且加载一次可以反复查,不用每次都重新解析文件。

有个小细节:交互模式下结果集如果很大,终端会被刷爆。养成习惯,任何查询都带limit,先看形状再决定要不要全量。另外注意,未完成的 slice(比如采样期间还没结束的异步事件)的dur是-1,不加过滤条件的话,order by dur出来的排序会很奇怪。

4.3 几条能直接抄的 SQL

下面这几条是我用得最多的,覆盖了大部分"哪个函数慢、卡在哪、为什么卡"的问题。表结构在不同版本有细微差异,但主干字段是稳定的。

第一条,找耗时最长的 slice,先看全局:

select s.name, s.dur / 1e6 as dur_ms, s.ts from slice s where s.dur > 0 order by s.dur desc limit 30;

第二条,只看主线程,因为绝大多数 UI 卡顿都出在主线程:

select t.name as thread_name, s.name as slice_name, s.dur / 1e6 as dur_ms from slice s join thread_track tt on tt.id = s.track_id join thread t on t.utid = tt.utid where t.name = 'main' and s.dur > 16.6e6 order by s.dur desc limit 30;

16.6e6 是纳秒单位下的 16.6 毫秒,也就是 60fps 下一帧的预算。超过这个数的 slice,基本都有嫌疑。

第三条,看主线程到底把时间花在哪了。这条用的是thread_state表,它记录的是线程状态(Running、Runnable、Sleeping 等),能回答"是代码慢还是根本没被调度":

select t.name, ts.state, sum(ts.dur) / 1e6 as total_ms, count(*) as cnt from thread_state ts join thread t using(utid) where t.name = 'main' group by t.name, ts.state order by total_ms desc;

如果 Runnable 的时间远大于 Running,说明 CPU 抢不到,要看调度和 CPU 频率;如果 Sleeping 占大头,大概率是在等锁或者等 IO。

第四条,Binder 跨进程调用的耗时排行,排查"明明本地代码很快,整体就是慢"的问题:

select client_process, client_thread, server_process, server_thread, client_dur / 1e6 as client_ms, server_dur / 1e6 as server_ms from android_binder_txns order by client_dur desc limit 20;

这条查询的价值在于能直接区分"客户端调用本身就慢"和"服务端处理慢"。如果 client_ms 远大于 server_ms,问题在客户端侧排队或者线程调度;反过来就是服务端真的要优化。

第五条,看 CPU 频率和任务执行时间的关系,用来验证"降频导致卡顿"这个常见猜测:

select c.ts, c.value, ct.name from counter c join counter_track ct on c.track_id = ct.id where ct.name like '%cpu_freq%' order by c.ts limit 200;

4.4 内置 metrics:不想写 SQL 时候的退路

如果你只想快速拿到结论,不想一条条写 SQL,trace_processor_shell有一批预置的 metric 可以一次算完。常用的有帧统计、CPU 使用率、启动耗时这几类。命令形式大概是:

trace_processor_shell -m android_frame_stats trace.perfetto-trace

需要注意的是,不同版本里这个参数的写法不太一样,有的版本是--run-metrics,输出的格式也可以选 JSON、文本或者二进制。以你本地--help里的说明为准。这类 metric 的好处是"官方口径",指标定义稳定,适合放进回归脚本里做长期趋势对比;坏处是看不了细节,发现异常之后还是得回到 SQL 里一层层钻。

我一般的工作流是:先用 metric 拿整体结论,如果数字明显劣化,再用 SQL 定位到具体的 slice,最后开 UI 看那段区域的形状。三步走下来,基本没有查不清楚的问题。

5. 命令行 trace 的六个高频翻车点

5.1 权限问题往往表现为"静默成功"

最难受的一类问题不是报错,是命令返回 0、文件也生成了,但打开一看是空的或者只有几 KB 的元信息。出现这种情况,先把这三件事排一遍。

第一,确认traced服务是在跑的。它没起来的时候,perfetto连不上生产者,表现就是采不到数据。第二,确认输出路径你有权限写。用/data/misc/perfetto-traces/基本上不会错。第三,确认你让traced_probes去采的那些东西,在当前这套权限策略下是允许读的。厂商定制比较重的设备上,某些内核 tracepoint 可能被策略挡住了。

提示:抓完先看文件大小。10 秒的gfxtrace 怎么也得几百 KB 到几 MB。如果只有几十 KB,八成是没采到东西,别急着拿去分析。

5.2 category 写错不报错,只是没数据

atrace_categories写错一个字母,不会有任何警告。配置被接受,trace 正常生成,只是那个类别对应的 tracepoint 一个都没开。我见过把类别名写成大写、写成复数、或者写成 UI 上看到的中文标签的情况,全都一样静默失败。

排查方法很直接:抓一小段,然后查一下 trace 里到底有哪些事件名。

select name, count(*) as cnt from slice group by name order by cnt desc limit 50;

如果预期的事件一个都没出现,就去检查 category 拼写。另外要记住,atrace_categories和ftrace_events是可以混用的,有时候一个高层类别展开之后,跟你手动指定的某个 tracepoint 是重复的,重复指定不会出错,但会让你误判数据量。

5.3 缓冲区不够的典型症状

前面提过两层缓冲区,这里说说症状怎么区分。整段 trace 后面空掉,通常是全局buffers.size_kb不够或者 flush 没跟上。时间轴上出现密集的空洞、空洞之间数据又正常,通常是内核侧buffer_size_kb不够。两种情况都去stats表里找答案,比盲目调参数快得多。

还有一个不太常见的组合:drain_period_ms设得太长,加上内核 buffer 偏小,会出现"数据率不高但就是丢"的现象。这种情况下你把全局 buffer 加大一倍也没用,因为数据根本没从内核侧搬出来。

5.4 时间戳对不上:时钟域这件小事

Perfetto 内部默认用的是 BOOTTIME,也就是包含设备睡眠时间的那个时钟。而你在 logcat 里看到的、或者服务器日志里记的,往往是 realtime,也就是墙上时间。这两者之间有偏移,直接拿数字对比会得出完全错误的结论。

trace_processor提供了转换函数来解决这个问题:

select to_realtime(ts) as wall_clock_ts, name, dur / 1e6 as dur_ms from slice where dur > 16.6e6 limit 20;

反过来也可以用to_monotonic()、to_boottime()做转换。把 trace 里的时间戳转成墙上时间,再去跟其他来源的日志对齐,这个动作在跨系统联调的时候几乎是必须的。

5.5 长 trace 把设备本身拖慢

抓性能数据这件事本身就消耗性能,这个矛盾没法完全消除,只能控制。几个容易把设备拖垮的配置:write_into_file配了很低的file_write_period_ms,导致频繁写盘;drain_period_ms配得过低,导致搬运线程持续占 CPU;trace 文件写到 1GB 以上,存储 I/O 成为瓶颈。

我的经验是,如果抓取过程本身要让设备多消耗超过 10% 的 CPU,那这份数据用来分析卡顿就有问题了——你在测量的时候改变了被测对象。这种情况下更合适的做法是用触发式抓取,只在真正关心的那几秒钟打开采集。

5.6adb shell破坏二进制输出

这个坑前面提过,但值得再强调一次,因为它太隐蔽了。adb shell在传输过程中会做换行符处理,而 trace 文件里有大量恰好等于0x0A的字节,被替换成0x0D 0x0A之后文件结构就坏了。症状是文件大小比预期大一点,解析时报错但错误信息很含糊。

正确做法只有两条:要么输出到设备上的文件再adb pull(pull 是二进制安全的),要么用adb exec-out直接流出来。没有第三条。

6. 把命令行 trace 塞进日常工作流

6.1 一条脚本搞定抓取和回传

把前面所有东西串起来,一个能日常用的脚本大概长这样:

#!/usr/bin/env bash set -euo pipefail TAG="${1:-perfetto_trace}" CFG="$(dirname "$0")/trace_cfg.txt" adb push "$CFG" /data/local/tmp/perfetto_cfg.txt >/dev/null adb shell rm -f "/data/misc/perfetto-traces/${TAG}.perfetto-trace" adb shell perfetto \ -c /data/local/tmp/perfetto_cfg.txt --txt \ -o "/data/misc/perfetto-traces/${TAG}.perfetto-trace" adb pull "/data/misc/perfetto-traces/${TAG}.perfetto-trace" "./${TAG}.perfetto-trace" adb shell rm -f "/data/misc/perfetto-traces/${TAG}.perfetto-trace" echo "done: ./${TAG}.perfetto-trace"

几个细节值得说明。抓之前先删同名文件,避免 pull 到上一次的旧数据,这个错误我犯过不止一次,看着新文件的时间戳沾沾自喜,其实分析的是昨天的数据。抓完立刻从设备上删掉,因为/data/misc/perfetto-traces/的空间有限,攒几十个几百 MB 的文件很容易把分区写满,然后后面所有的抓取都会失败。配置文件放在脚本同级目录并用相对路径引用,这样整个工具目录可以直接整包拷给别人用。

6.2 用固定 SQL 做性能回归

脚本化的下一步是做对比。把每次抓到的 trace 都跑同一组 SQL,把关键指标抽出来存成一行记录,日子久了就是一份性能趋势。指标不用多,几个就够:主线程超过 16.6ms 的 slice 总时长、最长单次 slice、Runnable 状态占比、Binder 调用最大耗时。

-- 一次算完一组关键指标,方便直接进 CSV select 'long_slices_total_ms' as metric, sum(dur) / 1e6 as value from slice s join thread_track tt on tt.id = s.track_id join thread t on t.utid = tt.utid where t.name = 'main' and s.dur > 16.6e6 union all select 'max_slice_ms' as metric, max(dur) / 1e6 as value from slice s join thread_track tt on tt.id = s.track_id join thread t on t.utid = tt.utid where t.name = 'main' and s.dur > 0;

要注意的是,这类回归对比对采集条件很敏感。设备是否插电、当前温度、后台有没有别的任务,都会让数字波动百分之十几。所以基线不要只看一次,同一版本多跑几次取中位数,比单次对比可靠得多。

6.3 触发式抓取:只在关键时刻开采集

很多问题不是随时都能复现的,比如应用冷启动、某个特定操作、某次网络请求失败。这种情况下,让 trace 一直开着既不现实也会污染数据。Perfetto 的 trigger 机制就是为这个设计的:配置里声明一个触发器,采集进程先启动但处于等待状态,等你的代码或者命令去激活它,采集才真正开始。

配置侧大致是这样:

buffers { size_kb: 32768 fill_policy: RING_BUFFER } data_sources { config { name: "linux.ftrace" target_buffer: 0 ftrace_config { ftrace_events: "sched/sched_switch" atrace_categories: "gfx" atrace_categories: "view" } } } trigger_config { trigger_mode: START_TRACING trigger_timeout_ms: 30000 } duration_ms: 5000

然后用trigger_perfetto去激活:

adb shell perfetto -c /data/local/tmp/trigger_cfg.txt --txt --background \ -o /data/misc/perfetto-traces/on_trigger.perfetto-trace # 等你关心的操作发生前,手动触发 adb shell trigger_perfetto my_trigger_name

trigger_timeout_ms这个字段很实用,它保证如果等了 30 秒还没人触发,采集会话会自己退出,不会一直挂着占资源。触发之后采集固定时长(这里是 5 秒),刚好把关键路径包住。

我在实际使用中的体会是,trigger 机制是把"抓不到复现路径"这类问题变成可解的关键。以前遇到偶发卡顿只能靠运气蹲守,现在可以把采集挂上去,操作一遍业务,触发一次,抓一份干净的数据。

6.4 命令行和 UI 各司其职

最后说个流程上的习惯。我现在的基本节奏是:命令行负责采集和量化,把该抓的数据抓全、把该算的指标算出来;UI 负责形状识别,把 SQL 筛出来的可疑时间段拖进可视化里,看线程交错、看 CPU 频率曲线、看有没有意料之外的事件插入。量化能告诉你"哪里有问题",可视化能告诉你"问题长什么样",两者缺一不可。

如果一定要给一个入门顺序的建议:先把一份最小配置跑通,确认能采到数据;然后学会看stats表,把丢数据的问题解决掉;再开始写 SQL,从"最长 slice"这一条查起,慢慢往外扩。这三步走完,命令行 trace 就不再是那个让人望而生畏的黑盒了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 1:04:45

PICORV32软核源码解析:从Verilog到RISC-V处理器设计入门

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:04:26

工业气体泄漏检测数据集:双模态实例分割+多级语义标注

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:04:02

井盖缺陷检测数据集:2890张实拍图+VOC/YOLO双格式+5类细粒度标注

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:03:34

Switch原生运行Wine兼容层:无需刷系统跑PC游戏实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 1:01:56

Jenkins密码重置实战:通过config.xml恢复管理员访问

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华