Systrace 和 Perfetto 的录制与解读

Systrace 和 Perfetto 的录制与解读

Systrace 和 Perfetto 的录制与解读


Systrace 和 Perfetto 的录制与解读:一个 Android 性能工程师的实战笔记


从一次 ANR 定位说起


去年处理过一个棘手的 Input ANR,用户反馈在列表滑动时偶发卡死,线下复现概率不到 5%。常规的 logcat 抓栈只能看到主线程阻塞在 android.os.BinderProxy.transactNative,调用链断在 IPC 边界,完全不知道对端是谁、在干什么。当时团队里有人提议加日志暴力排查,被我拦住了——这种时序敏感的问题,打日志本身就会改变行为,而且偶发问题的有效日志命中率极低。


最后是靠 Systrace 的 binder_transaction 标签定位到的。主线程发起 Binder 调用后,对端进程的一个线程持有锁的同时触发了 GC,GC 期间又发生了堆裁剪,整个链路拖了 800ms。这个案例让我重新意识到:在 Android 性能分析工具链里,低开销的全局追踪仍然是不可替代的底牌。Systrace 和它的继任者 Perfetto,就是这个领域最核心的两个工具。


这篇文章不会给你罗列官方文档里已有的基础命令,而是围绕实际录制中的参数选择、解读时的思维模型、以及两个工具各自的边界和陷阱,分享一些踩坑后的经验。


Systrace:被低估的老工具,和被误解的 `--app`


Systrace 作为 Android SDK 内置工具,路径在 $ANDROID_HOME/platform-tools/systrace/systrace.py(Android 11 后标记为 deprecated,但至今仍然可用)。它的本质是利用 Linux 内核的 ftrace 机制,配合 atrace 的用户态探针,生成基于 HTML 的交互式报告。


很多人用 Systrace 就只会 python systrace.py -o trace.html sched gfx view,这种抓法在 Android 8.0 之前的设备上勉强够用,但现代系统里会漏掉大量关键信息。我推荐的基线参数是这样的:


python systrace.py -o trace.html -t 10 -a com.example.app \
    sched freq idle am wm gfx view binder_driver hal dalvik camera input res

-a com.example.app 这个参数是启用应用自定义 atrace 标签的关键。没有它,你在代码里写的 Trace.beginSection("MyOperation") 根本不会出现在报告里。这个坑我踩过不止一次——花了半天在代码里插桩,结果抓出来的 trace 一片空白,最后发现是漏了 -a


-t 10 指定 10 秒录制窗口,实际有效数据会略短于这个值,因为 buffer 有刷新延迟。Systrace 的 ring buffer 默认只有 2MB,对于高负载场景很容易丢事件。可以通过 --buf-size 8192 扩大到 8MB,但再大就没意义了,Systrace 的 HTML 渲染器本身有性能瓶颈,过大的 trace 文件在 Chrome 里打开会卡死。


Systrace 的 HTML 报告有个设计上的历史包袱:它用 Chrome 的 Catapult 库渲染,依赖旧版 Chrome 的特定 API。Google 在 2020 年后基本停止维护这个前端,新版 Chrome 打开大 trace 时经常出现 "Aw, Snap!" 崩溃。我的 workaround 是装一个独立的 Chromium 90 便携版专门看 Systrace,或者直接用 Perfetto UI 的 legacy trace viewer 模式打开 Systrace 生成的文件——虽然不完全兼容,但至少不会崩溃。


Perfetto:新架构,新思维,新坑


Perfetto 从 Android 10 开始成为系统默认的追踪框架,取代 Systrace 的底层实现。它的命令行入口是 perfetto,但更多时候我们通过 adb shell perfetto 或者 Android Studio 的 CPU Profiler 间接使用。


Perfetto 的核心优势在于统一的 protobuf 格式和 SQL 查询接口。trace 文件是 .pb.perfetto-trace 格式,可以用 https://ui.perfetto.dev 直接打开,完全在浏览器本地解析,不需要上传服务器。这个在线工具是 Google 团队维护的,更新频率很高,我上周还看到它新加了 Jank CUJ(Critical User Journey)的自动检测面板。


录制 Perfetto trace 最灵活的方式是写 config 文件。一个典型的 120Hz 设备上的流畅度分析配置长这样:


buffers: {
  size_kb: 131072
  fill_policy: RING_BUFFER
}
data_sources: {
  config {
    name: "linux.ftrace"
    ftrace_config {
      ftrace_events: "sched/sched_switch"
      ftrace_events: "sched/sched_wakeup"
      ftrace_events: "sched/sched_waking"
      ftrace_events: "power/cpu_frequency"
      ftrace_events: "power/cpu_idle"
      ftrace_events: "binder/binder_transaction"
      ftrace_events: "binder/binder_transaction_received"
      atrace_categories: "gfx"
      atrace_categories: "view"
      atrace_categories: "wm"
      atrace_categories: "am"
      atrace_categories: "input"
      atrace_apps: "com.example.app"
    }
  }
}
data_sources: {
  config {
    name: "android.log"
    android_log_config {
      log_ids: LID_EVENTS
      log_ids: LID_MAIN
      log_ids: LID_CRASH
    }
  }
}
duration_ms: 10000

把这个 config 存为 trace_config.pbtxt,然后执行:


adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/trace \
    < trace_config.pbtxt

--txt 表示用文本格式,方便调试;生产环境建议用 protobuf 二进制格式减少解析开销。atrace_apps 对应 Systrace 的 -a,同样容易漏掉。


Perfetto 的 buffer 配置是个精细活。RING_BUFFER 模式下,超过 size_kb 的旧数据会被覆盖。131072 KB 约 128MB,对于 10 秒录制、包含全量 ftrace 的场景是够用的。但如果要抓更长时间,或者开了 heap_profile 这类高体积数据源,就必须改用 DISCARD 策略或者进一步扩容。我曾经在分析夜间待机功耗时设了 8 小时录制,结果默认的 32MB buffer 在第一个小时就被 ftrace 的 sched_switch 事件撑爆了,后面七小时全是空白——这种失败成本很高,因为问题只在特定时段出现。


解读框架:从"看颜色"到"算时间"


拿到 trace 文件后,新手容易陷入两个极端:要么盯着 UI 线程的彩色条带看"有没有红色",要么直接写 SQL 查原始事件,被海量数据淹没。我摸索出的有效路径是三层递进。


第一层是建立时序体感。在 Perfetto UI 里,先框选一个完整的用户交互周期——比如从手指 ACTION_DOWN 到屏幕完成响应。看三个关键轨道:input 里的 TouchMotionEvent 时间戳、SurfaceFlingeronMessageReceived 对应哪一帧 VSync、Choreographer#doFrame 的实际执行区间。理想情况下,这三者应该形成稳定的流水线:input 事件在 VSync 边界附近到达,Choreographer 在下一个 VSync 启动,SurfaceFlinger 在再下一个 VSync 合成。如果看到 input 到达后过了两个 VSync 才开始 doFrame,说明主线程被抢占了,这时候再下钻看 sched 轨道,找到那段时间谁占了 CPU。


第二层是量化延迟分布。Perfetto UI 的 "Jank" 面板会自动标记掉帧,但它的算法比较保守,只检测 doFrame 超过 1.5 个 VSync 周期的情况。对于 120Hz 设备,一个帧是 8.3ms,1.5 倍就是 12.5ms,这意味着 10ms 的帧它不会标红——但用户已经能感知到顿挫了。我的做法是手动写 SQL 查更精细的阈值:


SELECT
  ts,
  dur,
  name,
  (dur / 1e6) as dur_ms
FROM slice
WHERE name = 'Choreographer#doFrame'
  AND dur > 8333333  -- 8.33ms in nanoseconds
ORDER BY ts;

这个查询在 Perfetto 的 "Query SQL" 面板直接执行,结果可以导出 CSV 做进一步统计。注意 dur 的单位是纳秒,和 ts(时间戳)一致。我第一次写的时候忘了除 1e6,看到全是八位数直接懵了半分钟。


第三层是关联跨进程调用。Android 的渲染流水线涉及 App 进程、SurfaceFlinger、HWC(Hardware Composer),有时候卡顿根因在 GPU 驱动或者厂商定制的合成器里。Perfetto 的 binder_transactionbinder_reply 切片可以画出跨进程的因果关系线,但默认视图里这些线很细,容易被忽略。我习惯在 "Flags" 设置里把 "Flow arrows" 的透明度调低、粗细调大,这样从 App 的 dequeueBuffer 到 SurfaceFlinger 的 onMessageReceived 的箭头会更明显。如果看到箭头末端有长时间空白,说明对端进程没及时响应,这时候就要切换进程视角去看了。


Systrace vs Perfetto:不是简单的替代关系


Google 官方文档说 Systrace 已 deprecated,建议迁移到 Perfetto。但实际工作中,两者各有不可替代的场景。


Systrace 的 HTML 报告在特定场景下反而更直观。它的线程状态着色方案(绿色 Running、蓝色 Runnable、白色 Sleeping、橙色 Uninterruptible Sleep)是行业内最广泛认知的配色,和 Linux 的 schedstat 语义完全一致。Perfetto UI 虽然也能显示线程状态,但默认配色更淡,而且把 D 状态(Uninterruptible Sleep)和 S 状态混在一起用相近的蓝色,不如 Systrace 里醒目的橙色一眼能抓出问题。我在做演示或者写报告截图时,仍然会用 Systrace 的图,因为非性能专业的同事更容易看懂。


另一个差异是启动开销。Systrace 的 atrace 机制在用户态写 /sys/kernel/debug/tracing/trace_marker,路径短、延迟低。Perfetto 的 traced 守护进程需要额外的 IPC 和序列化开销,在极端轻量场景(比如分析启动首帧,总时长只有 200ms)下,Perfetto 自身的开销可能占到 5-10%。这个比例在常规分析里可以忽略,但在做基线对比或者竞对分析时,需要控制变量。我的做法是在同一台设备上,用两种工具各抓 10 次取中位数,确认差异方向一致后再下结论。


Perfetto 真正的杀手级特性是 heap_profilejava_heap_dump 的整合,以及即将稳定的 android.game_interventions 数据源。前者可以在同一份 trace 里同时看 CPU 调度、渲染流水线和内存分配,解决 "GC 导致的卡顿" 这类需要多维度关联的问题;后者是 Android 13 引入的,能直接看到系统对游戏的干预策略(比如 CPU/GPU 频率限制、帧率降低),这对游戏性能分析是颠覆性的——以前这些信息分散在厂商私有接口里,现在标准化了。


但 Perfetto 的坑也很具体。它的 SQL 查询引擎基于 SQLite 扩展,语法和标准的 SQLite 有微妙差异。比如 TIMESTAMP_SUB 函数在标准 SQLite 里不存在,是 Perfetto 为时间运算定制的;JOIN 的性能在百万级事件表上会很差,需要手动限制时间窗口。我有一次写了个跨全量 sched_switchbinder_transaction 的 JOIN,直接让浏览器 tab 内存飙到 4GB 然后崩溃。后来学会先用 WHERE ts > X AND ts < Y 裁剪时间范围,或者先用 CREATE TABLE ... AS SELECT 做物化视图,查询效率提升两个数量级。


实战案例:一个"伪主线程阻塞"的排查


去年有个案例特别典型,值得展开说。用户反馈点击按钮后 300ms 才响应,但我们在主线程加日志,从 onClick 到业务逻辑执行完只花了 15ms。是日志不准吗?还是问题在别处?


用 Perfetto 抓 trace 后,先看 input 轨道。TouchMotionEventACTION_UP 时间戳和按钮点击的物理发生时间吻合,说明触摸事件确实在那一刻就上报了。但接下来的 deliverInputEvent 切片显示,InputDispatcher 花了 280ms 才把事件分发给目标窗口。这 280ms 的延迟不在 App 进程,而在 system_server 的 Input 子系统。


下钻看 InputDispatcher 线程的 sched 状态,发现这段时间它大部分处于 Runnable(蓝色),而不是 Running(绿色)。再看 CPU 轨道,CPU 0-3 被四个 kworker 线程占满,执行的是某个厂商定制的存储优化任务。InputDispatcher 作为高优先级线程(nice=-10),理论上应该能抢占,但那个 kworker 被设置了 SCHED_FIFO 实时调度策略,且优先级更高。结果就是:用户点击时,系统正在后台整理存储,InputDispatcher 虽然醒着,但抢不到 CPU。


这个案例的启示是:卡顿的根因不一定在 App 内部。Systrace/Perfetto 的全局视角价值就在这里——它让你看到整个系统的时序,而不是单个进程的盲区。如果当时只用 Method Tracing 或者简单的日志,这个结论几乎不可能得到。


最后的修复方案是和厂商沟通,把那个存储优化任务的调度策略从 SCHED_FIFO 改为 SCHED_OTHER,并限制在 CPU 大核上运行,避免和系统关键线程抢小核。这个改动不在 App 侧,但如果没有 trace 提供的证据链,厂商根本不会认。


录制时的隐性成本:功耗和温控


很多人忽略的一点是,开 trace 本身会影响被测系统的行为。ftrace 的 sched_switch 事件在内核态高频产生,对于 8 核设备、线程数 200+ 的典型负载,每秒可能有 10 万次以上的上下文切换事件需要记录。这些事件从内核 ring buffer 拷贝到用户态、序列化、写文件,全程有 CPU 和内存开销。


在分析功耗问题时,这个开销会扭曲结果。我曾经用 Perfetto 抓待机场景,同时用 Power Monitor 测电流,发现开 trace 时平均电流高出 80mA。原因是 traced 进程频繁唤醒小核做 buffer 处理,而待机场景本来应该让 CPU 长时间处于 deep idle。后来学会用 on_device = true 配置让 Perfetto 在设备本地做部分处理减少 IPC,以及用更精简的 ftrace 事件集合(去掉 sched_switch,只留 sched_wakeup),才把开销压到 20mA 以内。


温控是另一个变量。持续录制 10 分钟以上,部分设备会因为 traced 的 CPU 占用触发温控降频,导致后续抓到的 trace 反映的是"降频后"的系统行为,和正常状态不一致。我的 workaround 是分段录制,每段 2-3 分钟,中间留冷却间隔;或者直接用 max_file_size_bytes 限制单文件大小,让 trace 在 buffer 满时自动轮转。


工具链整合:从手动到自动化


个人开发者可以手动 adb shell perfetto 抓 trace,但团队规模扩大后需要自动化。我们内部搭了一套基于 Perfetto 的持续性能监控,核心思路不复杂:


录制端用 AOSP 的 record_android_trace 脚本(在 platform-tools/perfetto/ 下)做封装,通过 CI 触发。每次提交后,在固定型号的测试机上跑预设的 CUJ(比如冷启动、列表滑动、页面跳转),自动抓 trace 并上传到内部服务器。


分析端用 Perfetto 的 trace_processor 命令行工具做离线 SQL 查询。这个工具在 https://github.com/google/perfetto releases 里可以下载,Linux 版约 80MB。它把 .perfetto-trace 文件加载成内存数据库,然后执行 SQL 返回结果。我们的监控脚本会定期跑一批标准查询,比如 "启动阶段主线程 Running 时间占比"、"连续掉帧的最大长度"、"Binder 调用 P99 延迟",把结果写入时序数据库做趋势图。


trace_processor 有个坑:它的 SQL 方言和在线 UI 的略有不同,部分 UI 里的辅助表(比如 chrome_slices,是 UI 为了方便做层级展开而虚拟出来的)在命令行里不存在。我花了半天排查一个查询在 UI 里能跑、在 trace_processor 里报 no such table 的问题,最后发现需要手动用 EXPERIMENTAL_PROFILE_DESERIALIZATION 宏重新编译。这个体验很糟糕,但官方 issue 列表里已经有人提了,预计 2024 年下半年的版本会统一。


局限与边界:什么时候不该用 trace


Systrace/Perfetto 不是万能药。它们的采样粒度受限于 ftrace 的事件频率,对于纳秒级的细粒度分析(比如单个 Java 方法的耗时分布、锁竞争的微秒级抖动),需要用 Simpleperf 或者 Android Studio 的 Method Tracing。


更根本的局限是:trace 告诉你"发生了什么",但不直接告诉你"为什么"。看到主线程在 BinderProxy.transactNative 阻塞了 500ms,你能知道对端是哪个进程、哪个接口,但不知道为什么这个 IPC 要 500ms——是对端在忙?锁竞争?还是 Binder 驱动本身的调度延迟?这时候需要结合 binder/binder_lock 事件、对端进程的 trace、甚至内核的 sched 统计,做跨层推理。这种推理没有标准答案,依赖工程师对系统架构的理解。


另外,trace 对 Native 代码的可见性有限。除非手动插了 ATrace_beginSection/ATrace_endSection,否则 so 库内部的执行是黑盒。我们在集成某第三方广告 SDK 时遇到过这种情况:SDK 的 Native 层有大量加密计算,Java 层只看到一个 nativeEncrypt 调用占 200ms,里面发生了什么完全不知道。最后是靠在 Native 层也接入 Perfetto 的 SDK(#include <perfetto/tracing.h>),手动埋点才解开。这个方案需要重新编译 so,对第三方闭源库不适用。


写在最后


Systrace 和 Perfetto 的演进,某种程度上反映了 Android 性能分析从"黑盒猜测"到"白盒观测"的整体趋势。Systrace 代表了旧时代的简洁和直观,Perfetto 代表了新时代的灵活和深度。两者都不是完美的工具,都有学习曲线、都有特定场景下的陷阱,但在这个领域,没有替代品能同时提供同等的系统级可见性和足够低的运行时开销。


如果你现在还在用 System.out.println 或者简单的 Debug.startMethodTracing 排查卡顿问题,建议花一个下午认真跑一次 Perfetto 的完整流程:从写 config、到抓 trace、到在 UI 里框选时序、到写第一条 SQL 查询。这个投入的收益是长期的——一旦建立起"看 trace"的思维习惯,很多之前需要猜的问题会变得一目了然。


至于具体选 Systrace 还是 Perfetto,我的建议是:快速验证、截图汇报用 Systrace;深度分析、自动化监控、跨版本对比用 Perfetto。两者都装,按需切换,不纠结。

DataStore 替代 SharedPreferences,迁移要注意什么 2026-07-13
Lottie 动画的文件体积优化,JSON 精简技巧 2026-07-15

评论区