Perfetto 简易入门

参考链接:Perfetto详细解析

一、Perfetto 基础

1. Perfetto 介绍

Perfetto 是一个生产级的开源堆栈,用于性能插桩和 trace 分析。与 Systrace 不同,它提供了数据源的超集,可以用 protobuf 编码的二进制流形式记录任意长度的跟踪记录。

可以将 Perfetto 理解为 Systrace 的升级版,适用于更新的平台,以更丰富的图表展示更多信息。它可帮助开发者收集 Android 关键子系统(如 SurfaceFlinger、SystemServer、Input、Display 等 Framework 部分关键模块、服务,View 系统等)的运行信息,从而更直观地分析系统瓶颈、改进性能。

Perfetto 基本功能包括:

更多官方文档参考:perfetto.dev


2. Perfetto 使用流程

使用 Perfetto 前,需先了解其基本流程:

  1. 在手机上准备好要抓取的界面
  2. 点击开始抓取(命令行的话就是执行命令)
  3. 在手机上操作(不要太长时间,文件过大会很卡,且不好定位问题)
  4. 设定的时间到了之后,将生成的 trace 文件用 Perfetto UI 打开分析

3. 抓取 Trace 的几种方式

3.1 使用 perfetto 命令抓取

命令行形式比较灵活,速度也快。相关的 TAG 一次性配置好后,再次使用会很快出结果;也可以写成脚本,方便复用。

常用参数说明:

参数 说明
-o 输出文件的路径和名字
-t 抓取时间(最新版本可以不指定,按 Enter 即可结束)
-b 指定 buffer 大小(默认 Buffer 一般够用;抓很长的 Trace 建议调大)
-a 指定 app 包名(如果 Debug 自定义 Trace 点,记得加这个)

在低于 Android R 的版本上,perfetto 默认是关闭的,需要先执行以下命令开启:

adb shell setprop persist.traced.enable 1

抓取并导出 perfetto-trace:

# 抓取 trace(可根据需要增减 tag)
adb shell perfetto \
    -o /data/misc/perfetto-traces/trace_file.perfetto-trace \
    -t 10s \
    sched freq idle am wm gfx view binder_driver hal dalvik camera input res memory

# 导出 trace 文件到本地
adb pull /data/misc/perfetto-traces/trace_file.perfetto-trace
3.2 使用 atrace 命令抓取
# 抓取 atrace
adb shell atrace -z -b 40000 \
    am wm view res ss gfx view hal bionic pm sched freq idle disk load sync \
    binder_driver binder_lock memreclaim dalvik input \
    -t 10 \
    > /data/local/tmp/trace_output.atrace

# 导出 trace 文件
adb pull /data/local/tmp/trace_output.atrace
3.3 使用 systrace 脚本抓取
  1. 配置好 Python 环境
  2. 解压 systrace.zip
  3. 进入解压后的 systrace 目录,找到 systrace.py 文件,执行:
python systrace.py \
    am wm view res ss gfx rs hal bionic pm sched freq idle disk \
    binder_driver binder_lock memreclaim dalvik input database \
    -t 10 \
    -o tracelog/systrace.html

生成的 systrace.html 文件在 systrace/tracelog/ 目录下。

3.4 通过手机 System Tracing 抓取
  1. 打开 开发者选项系统跟踪(System Tracing)→ 开启 Show Quick Settings tile,添加快捷图标
  2. 在设置中配置需要录制的 类别(Categories)缓冲区大小(Buffer Size)
  3. 点击 录制跟踪记录(Record trace) 开始录制
  4. 复现问题或进行特定操作
  5. 完成后点击通知栏中的 点按即可停止跟踪 停止并保存
  6. 弹出 “trace saved” 通知后,文件保存在 /data/local/traces/
  7. 使用 adb pull /data/local/traces/ 将 trace 文件导出到本地

4. 查看线程状态

Perfetto 会用不同颜色标识不同线程状态。在每个方法上面都有对应的线程状态标记,通过查看线程状态可以判断当前瓶颈所在——是 CPU 执行慢、Binder 调用、IO 操作,还是拿不到 CPU 时间片。

4.1 绿色 — 运行中(Running)

只有处于该状态的线程才可能在 CPU 上运行。同一时刻可能有多个线程处于可执行状态,这些线程的 task_struct 结构被放入对应 CPU 的可执行队列(一个线程最多只能出现在一个 CPU 的可执行队列中)。调度器从各个 CPU 的可执行队列中分别选择一个线程在该 CPU 上运行。

分析方向

  • 是否频率不够?
  • 是否跑在了小核上?
  • 是否频繁在 Running 和 Runnable 之间切换?
  • 是否频繁在 Running 和 Sleep 之间切换?
  • 是否跑在了不该跑的核上?(如不重要的线程占用了超大核)
4.2 蓝色 — 可运行(Runnable)

线程可以运行但当前没有被调度,正在等待 CPU 调度。

分析方向

  • 是否后台有太多任务在跑?
  • 是否频率太低导致处理不及时?
  • 是否被限制到某个 cpuset 里,但该 CPU 负载已满?
  • 此时 Running 的任务是什么?为什么?
4.3 白色 — 休眠中(Sleep)

线程没有工作要做,可能是因为在互斥锁上被阻塞,也可能在等待某个线程返回。

分析方向:查看被谁唤醒,确认等待的目标线程。

4.4 橘色 — 不可中断睡眠态(Uninterruptible Sleep - IO Block)

线程在 I/O 上被阻塞或等待磁盘操作完成,通常底部会标识出 callsite:wait_on_page_locked_killable

分析方向:大量橘色出现时,一般是由于低内存状态下申请内存触发 page fault,Linux 的 page cache 链表中有些 page 还没准备好(磁盘内容未完全读出),此时访问该 page 就会出现 wait_on_page_locked_killable 阻塞。当 IO 操作繁忙、每笔 IO 都需要排队时,极易出现且阻塞时间往往较长。

4.5 棕色 — 不可中断睡眠态(Uninterruptible Sleep - non-IO)

线程在另一个内核操作(通常是内存管理)上被阻塞。

分析方向:一般陷入了内核态,需根据具体情况分析是否正常。


5. 任务唤醒信息分析

线程被唤醒的信息非常重要。知道线程被谁唤醒,就能理清调用等待关系。如果某线程出现较长的 Sleep 后被唤醒,可查看是谁唤醒了它,分析唤醒者为什么这么晚才唤醒。

典型场景一:应用主线程通过 Binder 与 SystemServer 的 AMS 通信,但 AMS 的函数正在等待锁释放(或函数本身执行时间很长),导致应用主线程长时间等待,出现响应慢或卡顿。这就是后台大量进程运行或跑完 Monkey 后整机性能下降的主要原因。

典型场景二:应用主线程在等待本应用其他线程的执行结果。此时线程唤醒信息可用于分析主线程究竟被哪个线程 Block。

操作方式:点击 Sleeping 之后的 Running 段,查看它运行在哪个 CPU 上,以及是哪个线程唤醒它的,从而判断是否存在异常。


6. 信息区数据解析

6.1 CPU 架构

目前手机的 CPU 按核心数和架构分为三类:

  1. 非大小核架构:所有核心相同
  2. 大小核架构:通常 0-3 为小核,4-7 为大核
  3. 大中小核架构:通常 0-3 为小核,4-6 为中核,7 为超大核
6.2 CPU Info

Perfetto 中 CPU Info 信息一般在最上方,常用的分析维度:

  • CPU 频率变化情况
  • 任务执行情况
  • 大小核的调度情况
  • CPU Boost 调度情况

典型分析问题:

  • 任务执行慢 → 是否被调度到了小核?
  • 任务执行慢 → 当前 CPU 频率是否不够?
  • 特殊任务 → 能否调度到大核执行?
  • 场景对 CPU 要求高 → 能否限制 CPU 最低频率?

7. 快捷键

掌握快捷键可大幅提升 Perfetto 查看效率:

快捷键 功能
W 放大(查看局部细节)
S 缩小(查看整体)
A 左移
D 右移
M 选中该时间段范围,方便上下对照
Ctrl + F 搜索(与自带搜索框配合使用,自带搜索需 4 个字符以上)

8. 何为刷新率

  • 60 fps 指画面每秒更新 60 次(针对软件),每次更新约为 1000ms / 60 ≈ 16.67 ms
  • 这 60 次更新需要匀速,时快时慢同样会让视觉不流畅
  • 屏幕刷新率针对硬件。目前大部分手机屏幕刷新率维持在 60Hz,移动设备上使用 60Hz 是因为对功耗要求更高——提高刷新率意味着逻辑功耗线性增大,同时更短的 TFT 数据写入时间对屏幕设计难度也更大

二、主线程与渲染线程

1. 主线程的创建

Android App 的进程基于 Linux,其管理也基于 Linux 的进程管理机制,因此创建过程同样调用了 fork 系统调用。

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

/**
 * Zygote 进程通过 fork() 创建新的 App 进程。
 * fork 之后子进程继承了父进程的地址空间,
 * 然后通过 exec 或直接调用 ActivityThread.main() 进入 App 主线程逻辑。
 */
pid_t pid = fork();

if (pid == 0) {
    // 子进程:进入 App 进程初始化流程
    // ...
} else if (pid > 0) {
    // 父进程(Zygote):记录子进程 pid,继续等待下一个请求
    // ...
} else {
    // fork 失败处理
    ALOGE("Fork failed: %s", strerror(errno));
}

Fork 出来的进程可以视为主线程,但此时它还未与 Android 运行时连接,无法处理 Android App 的 Message。由于 Android App 基于消息机制运行,这个 Fork 出来的主线程需要与 Android Message 体系绑定,才能处理各种消息。

这里引入 ActivityThread。确切地说,ActivityThread 更应该叫 ProcessThread——它连接了 Fork 出来的进程和 App 的 Message 机制,二者配合组成了我们熟知的 Android App 主线程。

关键认知:ActivityThread 并不是一个 Thread,它只是初始化了 Message 机制所需的 MessageQueue、Looper、Handler,其 Handler 负责处理大部分 Message 消息。因此我们习惯上认为 ActivityThread 是"主线程",其实它只是主线程的一个逻辑处理单元

1.1 ActivityThread 的创建

App 进程 fork 出来后,查找 ActivityThread 的 main 函数入口。

com/android/internal/os/ZygoteInit.java

static final Runnable childZygoteInit(
        int targetSdkVersion, String[] argv, ClassLoader classLoader) {
    RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv);
    return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader);
}

这里的 args.startClass 就是 ActivityThread。找到之后调用,逻辑进入 ActivityThread.main()

android/app/ActivityThread.java

public static void main(String[] args) {
    // 1. 初始化主线程 Looper、MessageQueue
    Looper.prepareMainLooper();

    // 2. 实例化 ActivityThread
    ActivityThread thread = new ActivityThread();

    // 3. 调用 AMS.attachApplicationLocked,同步进程信息,完成初始化
    thread.attach(false, startSeq);

    // 4. 获取主线程 Handler(H 类),App 的 Message 绝大部分在此处理
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    // 5. 初始化完成,Looper 开始轮询消息队列
    Looper.loop();

    // 注意:Looper.loop() 是死循环,后面的代码正常情况下不会执行
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

startSeq 是由 AMS 分配的进程启动序列号,在进程 attach 前传入,用于跟踪和去重。

main 函数执行完成后,主线程正式上线工作。


文档完结。如需补充其他 Perfetto 进阶用法(如自定义 Trace 点、SQL 查询分析、堆分析等),可进一步查阅 Perfetto 官方文档

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐