跳转至

Android 性能优化利器 —— Simpleperf

Simpleperf 是 Android 官方推出的底层性能分析工具,专为移动端场景设计,可帮助开发者进行函数级 CPU 性能剖析、调用图分析和热点定位。作为 Android NDK 工具链的重要组成部分,它能够以极低开销捕获应用程序和系统底层的运行时行为,为性能优化提供数据支撑。

基本原理

Simpleperf 是基于 Linux 内核性能事件采集机制(perf_event)的 Android 性能分析工具,其核心能力依赖于 CPU 内置的 PMU(Performance Monitoring Unit,性能监控单元)。

PMU 是 CPU 的硬件模块,通过专用计数器实时监测底层性能事件,包括 CPU 周期、指令执行数、缓存命中/失效、分支预测错误等, 精度可达指令级。PMU 通过周期性中断或溢出采样触发数据采集,并将原始数据通过 Linux 内核的 perf_event 子系统传递给用户态工具,比如 Simpleperf。

Simplerperf 工作原理

如上图所示,完整工作流程分为三个阶段:

  1. 配置阶段:Simpleperf 通过 perf_event_open() 系统调用初始化 PMU 寄存器,指定监测事件(如 cpu-cycles)。

  2. 采集阶段:PMU 触发硬件级中断,采样数据通过内核的 mmap 内存映射高效传输至用户空间。

  3. 分析阶段:Simpleperf 解析二进制数据,生成火焰图、热点函数统计等可视化报告。

整个流程通过内核桥接硬件与工具链,实现低开销(<3%) 的性能分析。Android 针对移动端场景优化了该路径,包括 JNI 符号自动解析、动态频率调节 等特性,确保高性能与低功耗的平衡。

下载工具

下载 Simpleperf 工具的方式主要有如下三种:

  1. 通过 AOSP 源码,编译 Simpleperf;
  2. 通过下载 Android NDK 文件,直接获取 Simpleperf;
  3. 通过 ADB 进入设备的 Shell 环境,使用内置的 Simpleperf。
对比项 源码编译 NDK 获取 设备内置
易用性 简单
推荐程度 不推荐 强烈推荐 推荐
优点 可以根据不同的源码,编译针对 Android 版本的工具 提供了 Simpleperf 封装的高级脚本 不需要下载其他的库和工具,原生自持
缺点 费时。对于初学者不友好,需要搭建源码编译环境 需要下载 NDK 文件,国内下载速度较慢 只能够获取到文本型数据,解析数据还需要借助外部工具和第三方库

AOSP 源码编译

访问 Simpleperf 的 源码仓库 获取源码,然后阅读 README 文件进行源码编译。

Android NDK 获取

下载 Android NDK 文件,直接获取 Simpleperf 工具:

  1. 解压 NDK 文件,找到 simpleperf 文件夹;
  2. 不同的操作系统,在寻找可执行文件存在细微差异。比如在 Windows 设备上,依次进入:simpleperf > bin > windows > x86_64,找到 simpleperf.exe 可执行文件;
  3. 执行 simpleperf -h 命令,获取帮助信息。
$ simpleperf -h
Usage: simpleperf [common options] subcommand [args_for_subcommand]
common options:
    -h/--help     Print this help information.
    --log <severity> Set the minimum severity of logging. Possible severities
                     include verbose, debug, warning, info, error, fatal.
                     Default is info.
    --version     Print version of simpleperf.
subcommands:
    dump                dump perf record file
    help                print help information for simpleperf
    inject              parse etm instruction tracing data
    kmem                collect kernel memory allocation information
    merge               merge multiple perf.data into one
    report              report sampling information in perf.data
    report-sample       report raw sample information in perf.data

simpleperf 目录中,相关文件或脚本的作用:

  1. bin/:包括可执行文件和共享库;
  2. bin/android/${arch}/simpleperf:用于设备上的静态 Simpleperf 可执行文件;
  3. bin/${host}/${arch}/simpleperf:用于主机的 Simpleperf 可执行文件;
  4. bin/${host}/${arch}/libsimpleperf_report.${so/dylib/dll}:用于主机的报告共享库;
  5. *.py, inferno, purgatorio:高级的 Python 封装脚本。

设备内置

通常在高版本的 Android 设备中,内置了 Simpleperf,检查方式如下:

$ adb shell 
> cd /system/bin
> ls | grep simpleperf # 查看设备使用有 Simpleperf 工具,较低 Android 版本可能没有
simpleperf
simpleperf_app_runner
> simpleperf -h # 查看 Simpleperf 的帮助信息

若设备没有内置 Simpleperf,可以在 Android NDK 中的 simpleperf 目录,找到 android 目录(不是 windows 目录)中simpleperf 可执行文件,然后将其 push 到设备中:

$ adb push android-ndk-版本号\simpleperf\bin\android\arm64\simpleperf /data/local/tmp
$ adb shell
> cd /data/local/tmp # 推荐此路径,其他路径可能无法添加权限
> chmod a+x simpleperf
> ls -al | grep simpleperf
-rwxrwxrwx 1 shell shell 3853120 2023-01-25 13:24 simpleperf
> simpleperf -h

Warning

从 Android 15 开始,Android 兼容性定义文档(Android CDD)开始要求设备必须支持 Simpleperf。

基础使用

通过 Android NDK 获取到的 Simpleperf 和在 Android Shell 环境下使用内置的 Simpleperf 使用方式有点区别,但是功能基本相同。首先,我们看下这 两种方式的帮助信息:

Android Shell 环境下的帮助信息
$ simpleperf --help                                                              
Usage: simpleperf [common options] subcommand [args_for_subcommand]                       
common options:                                                                           
    -h/--help     Print this help information.                                            
    --log <severity> Set the minimum severity of logging. Possible severities             
                     include verbose, debug, warning, info, error, fatal.                 
                     Default is info.                                                     
    --log-to-android-buffer  Write log to android log buffer instead of stderr.           
    --version     Print version of simpleperf.                                            
subcommands:                                                                              
    api-collect         Collect recording data generated by app api                       
    api-prepare         Prepare recording via app api                                     
    boot-record         record at boot time                                               
    debug-unwind        Debug/test offline unwinding.                                     
    dump                dump perf record file                                             
    help                print help information for simpleperf                             
    inject              parse etm instruction tracing data                                
    kmem                collect kernel memory allocation information                      
    list                list available event types                                        
    merge               merge multiple perf.data into one                                 
    monitor             monitor events and print their textual representations to stdout  
    record              record sampling info in perf.data                                 
    report              report sampling information in perf.data                          
    report-sample       report raw sample information in perf.data                        
    stat                gather performance counter information                            
    trace-sched         Trace system-wide process runtime events.                         
Android NDK 的帮助信息
$ simpleperf.exe --help
Usage: simpleperf [common options] subcommand [args_for_subcommand]
common options:
    -h/--help     Print this help information.
    --log <severity> Set the minimum severity of logging. Possible severities
                     include verbose, debug, warning, info, error, fatal.
                     Default is info.
    --version     Print version of simpleperf.
subcommands:
    dump                dump perf record file
    help                print help information for simpleperf
    inject              parse etm instruction tracing data
    kmem                collect kernel memory allocation information
    merge               merge multiple perf.data into one
    report              report sampling information in perf.data
    report-sample       report raw sample information in perf.data                        

上述帮助信息可以整理成如下的表格:

子命令/选项 Shell 环境 Android NDK 说明
通用选项
-h/--help 打印帮助信息
--log <severity> 设置日志级别(verbose/debug/warning/info/error/fatal),默认为 info
--log-to-android-buffer 将日志写入Android日志缓冲区(仅Shell环境支持)
--version 打印版本信息
子命令
api-collect 收集通过应用 API 生成的录制数据
api-prepare 通过应用 API 准备录制
boot-record 在启动时录制
debug-unwind 调试/测试离线栈展开
dump 转储 perf 录制文件
help 打印帮助信息
inject 解析 ETM 指令追踪数据
kmem 收集内核内存分配信息
list 列出可用事件类型
merge 合并多个 perf.data 文件
monitor 监控事件并输出文本表示
record 录制采样信息到 perf.data
report 报告 perf.data 中的采样信息
report-sample 报告 perf.data 中的原始采样信息
stat 收集性能计数器信息
trace-sched 追踪系统级进程运行时事件

说明:

  1. ✅ 表示支持,❌ 表示不支持。
  2. NDK 环境缺少部分系统级功能(如 boot-recordmonitor 等),但保留了核心分析功能(如 reportdump 等)。
  3. Shell 环境特有的 --log-to-android-buffer 选项在 NDK 中不可用。

也就是说,相比 Android NDK 提供的 Simpleperf,Android Shell 环境内置的 Simpleperf 支持更多功能。为了可以更好的了解 Simpleperf 的基本使用,本章节的命令特别的说明,默认均在 Android Shell 环境下执行。

list

list 命令用于枚举当前设备支持的所有性能监控事件(PMU events)。由于不同设备的硬件架构(如 CPU/GPU 型号)和内核版本存在差异, 所支持的事件集合也会有所不同。

$ simpleperf list
List of hw-cache events:    # 硬件缓存事件(需CPU缓存支持)
  branch-loads             # 分支预测加载
  ...
List of hardware events:    # 硬件性能计数器事件
  cpu-cycles               # CPU时钟周期
  instructions             # 退休指令数
  ...
List of software events:    # 软件模拟事件(通过内核计数)
  cpu-clock                # CPU时间
  task-clock               # 任务占用CPU时间
  ...

Danger

部分事件可能因 CPU 型号不同而不可用,尝试执行会返回 <not supported> 提示。

在基于 ARM 架构的设备上,输出会额外包含原始PMU事件(raw events):

  1. 这些事件直接来自ARM性能监控单元(PMU)的硬件能力
  2. 内核已对部分常用原始事件做了标准化封装:
    • raw-cpu-cycles → 标准化为 cpu-cycles
    • raw-instruction-retired → 标准化为 instructions
  3. 保留原始事件的目的:
    • 访问设备特有的高级监控能力(如特定微架构事件)
    • 兼容尚未被内核标准化的新硬件特性

stat

stat 命令用于获取调试线程的 event 的计数器的值。通过参数传递,可以选择使用哪个事件,哪个进程/线程被监控,需要监控多长时间和打印间隔。

# 使用默认事件(cpu-cycles、instructions 等),监控进程 7394,持续 10 秒钟。
$ simpleperf stat -p 7394 --duration 10 
Performance counter statistics:

#         count  event_name                # count / runtime
     16,513,564  cpu-cycles                # 1.612904 GHz
      4,564,133  stalled-cycles-frontend   # 341.490 M/sec
      6,520,383  stalled-cycles-backend    # 591.666 M/sec
      4,900,403  instructions              # 612.859 M/sec
         47,821  branch-misses             # 6.085 M/sec
  25.274251(ms)  task-clock                # 0.002520 cpus used
              4  context-switches          # 158.264 /sec
            466  page-faults               # 18.438 K/sec

Total test time: 10.027923 seconds.

stat 命令可以传递 list 命令中获取到的事件参数:

# 选择 cpu-cycles 事件
$ simpleperf stat -e cpu-cycles -p 11904 --duration 10

# 选择 cache-references 和 cache-misses 事件
$ simpleperf stat -e cache-references,cache-misses -p 11904 --duration 10

想要了解 stat 命令的参数含义可以执行 simpleperf stat --help 获取。下表是 stat 命令参数的含义:

选项 描述
-a 收集系统全局的性能计数器信息
--app package_name 分析指定 Android 应用的进程(非 root 设备需为可调试应用)
--cpu cpu_item1,cpu_item2,... 仅在选定的 CPU 上收集信息(支持单个 CPU 号或范围,如 0-3
--csv 以 CSV 格式输出报告
--duration time_in_sec 监控指定秒数(支持浮点数)
--interval time_in_ms 每隔指定毫秒打印一次统计(支持浮点数,默认累计统计)
--interval-only-values 仅打印每个间隔内的事件数值(需配合 --interval
-e event1[:modifier],... 选择要计数的事件列表(事件名或原始 PMU 事件如 r1b
修饰符:
- u:仅监控用户空间事件
- k:仅监控内核空间事件
--group event1[:modifier],... 将事件分组监控(同组事件同时调度,减少多路复用影响)
--no-inherit 不统计创建的子线程/进程
-o output_filename 将报告写入指定文件(默认输出到标准输出)
--per-core 为每个 CPU 核心单独打印计数器
--per-thread 为每个线程单独打印计数器
-p pid1,pid2,... 监控现有进程(与 -a 互斥)
-t tid1,tid2,... 监控现有线程(与 -a 互斥)
--print-hw-counter 测试并打印设备可用的 CPU PMU 硬件计数器
--sort key1,key2,... 指定排序键(配合 --per-thread--per-core 使用)
可选键:
- count:事件计数
- count_per_thread:线程在所有 CPU 上的计数
- cpu:CPU ID
- pid:进程 ID
- tid:线程 ID
- comm:线程名称
默认排序键:count_per_thread,tid,cpu,count
--use-devfreq-counters 在高通 SOC 设备上临时释放被内存延迟监控占用的硬件计数器(可能影响功耗)
--verbose 显示详细模式的结果

record

record 命令用于转储所分析进程的样本。每个样本可以包含样本生成时间、自上次样本以来的事件数、线程的程序计数器、线程的调用链等信息。

# 在进程 7394 上记录 10 秒,使用默认事件(cpu-cycles),使用默认采样频率(每秒 4000 个样本),将记录写入 perf.data 文件
$ simpleperf record -p 7394 --duration 10
simpleperf I cmd_record.cpp:316] Samples recorded: 21430. Samples lost: 0.

# 使用事件 instructions 进行记录。
$ simpleperf record -e instructions -p 11904 --duration 10

# 使用 task-clock 进行记录,该事件显示以纳秒为单位的 CPU 经过时间。
$ simpleperf record -e task-clock -p 11904 --duration 10

Warning

执行 record 命令时若报如下的错误:

$ adb shell "simpleperf record -p 1816 --duration 10"
simpleperf E cmd_record.cpp:480] Can't create output file in directory .: Read-only file system
说明文件存储的路径没有权限,需要通过 -o 的方式将其保存到 /data/local/tmp/ 目录下:

$ adb shell "simpleperf record -p 1816 --duration 10 -o /data/local/tmp/perf.data"

Note

Q: recordstat 命令的关系与区别? stat 直接统计性能事件的总次数,适合快速定位问题。record 记录详细执行过程,生成 perf.data 文件用于深入分析代码瓶颈。 二者通常配合使用,先通过 stat 发现异常,再用 record 查明原因。

想要了解 record 命令的参数含义可以执行 simpleperf record --help 获取。下表是 record 命令参数的含义:

选项 描述
-a 系统级采集(需配合 --exclude-perf 排除 simpleperf 自身样本)
--app package_name 分析指定 Android 应用的进程(非 root 设备需为可调试应用)
-p pid1,pid2,... 监控现有进程(与 -a 互斥)
-t tid1,tid2,... 监控现有线程(与 -a 互斥)
-e event1[:modifier],... 选择记录的事件(支持事件名、原始PMU事件如 r1b、kprobe事件)
修饰符:u(用户空间)、k(内核空间)
--group event1[:modifier],... 将事件分组监控(同组事件同时调度)
--trace-offcpu 记录线程被调度下CPU的事件(等效于 -c 1 -e sched:sched_switch
--kprobe kprobe_event 添加动态kprobe事件(格式参见内核文档)
--add-counter event1,... 在采样中附加事件计数(如同时记录cycles和instructions)
-f freq 设置采样频率(Hz,默认4000)
-c count 设置采样周期(每N次事件记录一次)
--call-graph fp/dwarf 启用调用图记录(默认dwarf,65528)
-g 等效于 --call-graph dwarf
--clockid clock_id 指定采样时间戳时钟(realtime/monotonic等)
--cpu cpu_items 仅在指定CPU核心采集(支持单个号或范围如0-3)
--duration 秒数 设置监控时长(支持小数)
-j branch_filters 启用分支栈采样(any/any_call/any_ret等过滤器)
-b 等效于 -j any
--addr-filter 为指令追踪设置地址过滤器(格式:filter/start/stop)
--tp-filter 设置tracepoint事件过滤器(格式参见内核文档)
--post-unwind 控制DWARF解栈时机(默认实时解栈)
--no-unwind 禁用调用栈解栈
--no-callchain-joiner 禁用调用链合并(默认启用以突破64K栈限制)
--exclude-perf 排除simpleperf自身样本
--exclude-pid/tid 排除指定进程/线程
--include-pid/tid 仅包含指定进程/线程
--exclude-process-name 通过正则排除进程名
--include-uid 仅包含指定UID的进程
-o 文件名 设置输出文件名(默认perf.data)
--size-limit SIZE 设置最大记录大小(支持K/M/G单位)
--symfs <dir> 指定符号文件搜索目录
--no-dump-symbols 禁止在记录文件中转储符号
--exit-with-parent 随父进程退出停止记录
--stdio-controls-profiling 通过stdin/stdout控制采样启停
--in-app 声明已在应用上下文中运行

说明:

  1. 默认行为:不加选项时默认记录 cpu-cycles 事件,采样频率 4000Hz,输出到 perf.data
  2. DWARF 解栈:默认实时解栈用户调用栈(可通过 --post-unwind 调整);
  3. 分支分析:-j 支持多种分支类型过滤(如函数调用/返回);
  4. Android 适配:--app--in-app 选项专门用于 Android 应用分析。

report

report 命令用于解析 record 命令生成的采样数据(如 perf.data),生成可读的性能分析报告,帮助开发者定位性能瓶颈。

# 基础报告
simpleperf report -i perf.data
# 生成调用图
simpleperf report -g caller --sort comm,symbol
# 过滤特定动态库
simpleperf report --dsos libc.so

想要了解 report 命令的参数含义可以执行 simpleperf report --help 获取。下表是 report 命令参数的含义:

选项 描述
-b 使用分支目标地址代替指令地址(需配合 -b/-j 录制的数据)
--children 显示调用链累计开销(Children列包含子函数开销)
--csv 以CSV格式输出报告
--csv-separator 设置CSV列分隔符(默认逗号)
--full-callgraph 打印完整调用图(需配合 -g
-g [callee\|caller] 打印调用图(callee模式显示被谁调用,caller模式显示调用谁,默认caller)
-i <file> 指定输入文件(默认perf.data)
--kallsyms <file> 指定内核符号文件路径
--max-stack <frames> 设置调用图最大显示栈帧数
-n 显示每个条目的采样次数
--no-demangle 禁止符号名反混淆
--no-show-ip 对未知符号不显示虚拟地址
-o <file> 设置报告输出文件(默认stdout)
--percent-limit <percent> 设置报告条目最小百分比阈值
--print-event-count 显示每个条目的事件计数(需record时用--add-counter
--raw-period 显示原始周期计数而非百分比
--sort key1,key2,... 设置报告排序键
可选键:pid/tid/comm/dso/symbol/vaddr_in_file
分支专用键:dso_from/dso_to/symbol_from/symbol_to
默认:comm,pid,tid,dso,symbol
--symfs <dir> 指定符号文件搜索目录
--vmlinux <file> 指定内核符号文件
--comms comm1,comm2,... 仅显示指定线程名的样本
--cpu cpu_items 仅显示指定CPU的样本(支持单号或范围如0-3)
--dsos dso1,dso2,... 仅显示指定动态库的样本
--pids/--tids 仅显示指定进程/线程的样本
--symbols sym1;sym2;... 仅显示指定函数的样本
--exclude-pid/tid 排除指定进程/线程样本
--include-process-name 通过正则包含进程名样本
--filter-file <file> 使用时间戳过滤样本(文件格式见doc)

高级脚本

这里说的 “高级脚本” 指的是在 Android NDK 下的将 simpleperf 命令使用高级脚本语言(比如:Python)进行了二次封装的脚本。 按照功能的不同,这些脚本可以分成三种类型:

  1. 用于记录的脚本(功能和 record 命令相似):app_profiler.pyrun_simpleperf_without_usb_connection.py
  2. 用于报告的脚本(功能和 report 命令相似):report.pyreport_html.pyinferno
  3. 用于分析的脚本:simpleperf_report_lib.py

Warning

这些脚本需要在 Python 3.9 及其以上的版本中才可以正常运行。

记录的脚本

app_profiler.py

app_profiler.py 脚本是对 simpleperf record 命令的高级封装,用于记录(采集)应用程序或 Native 程序的数据。 默认记录完成后,会在本地生成一个 perf.data 文件。

# 记录一个 Android 应用程序。
$ ./app_profiler.py -p simpleperf.example.cpp

# 记录一个将 Java 代码编译为本机指令的 Android 应用程序。
$ ./app_profiler.py -p simpleperf.example.cpp --compile_java_code

# 记录启动 Android 应用程序的 Activity。
$ ./app_profiler.py -p simpleperf.example.cpp -a .SleepActivity

# 记录一个本机进程。
$ ./app_profiler.py -np surfaceflinger

# 记录给定 pid 的本机进程。
$ ./app_profiler.py --pid 11324

# 记录一个命令。
$ ./app_profiler.py -cmd \
    "dex2oat --dex-file=/data/local/tmp/app-debug.apk --oat-file=/data/local/tmp/a.oat"

# 记录一个 Android 应用程序,并使用 -r 将自定义选项发送到记录命令。
$ ./app_profiler.py -p simpleperf.example.cpp \
    -r "-e cpu-clock -g --duration 30"

# 同时记录 CPU 时间和非 CPU 时间。
$ ./app_profiler.py -p simpleperf.example.cpp \
    -r "-e task-clock -g -f 1000 --duration 10 --trace-offcpu"

# 将分析数据保存在一个自定义文件中(如 perf_custom.data),而不是 perf.data。
$ ./app_profiler.py -p simpleperf.example.cpp -o perf_custom.data

run_simpleperf_on_device.py

有时候会有想要分析应用启动(冷热启动)的需求。此时,可以考虑使用 run_simpleperf_on_device.py 脚本。通过添加参数 --app 监控某个应用的进程是否存在。如果不存在,会以 1ms 的间隔循环轮询应用程序进程。

$ ./run_simpleperf_on_device.py record --app simpleperf.example.cpp \
    -g --duration 1 -o /data/local/tmp/perf.data
# 运行完命令后,手动启动应用或者通过 am start 的方式启动应用

api_profiler.py

api_profiler.py 用于控制应用程序代码中的记录。 它在记录前进行准备工作,并在记录后收集分析数据文件。

run_simpleperf_without_usb_connection.py

在设备没有通过 USB 连接电脑时,也可以使用 Simpleperf 记录数据。

$ ./run_simpleperf_without_usb_connection.py start -p simpleperf.example.cpp
# 在命令成功完成后,拔掉 USB 数据线,运行 SimpleperfExampleCpp 应用程序。
# 几秒钟后,再次插入 USB 数据线。

$ ./run_simpleperf_without_usb_connection.py stop
# 停止可能需要一点时间。等待停止后,性能分析数据会保存在本地的 perf.data 文件中

binary_cache_builder.py

binary_cache_builder.py 文件可以从 Android 设备中提取二进制文件,也可以在本地的目录中查找二进制文件。默认情况下,执行完 app_profiler.py 脚本后,会在本地生成一个 binary_cache 的目录,该目录用于保存在分析数据时(生成报告时)所需要的调试信息和符号表。

# 通过从设备中拉取二进制文件,为 perf.data 生成 binary_cache
$ ./binary_cache_builder.py

# 通过从设备中拉取二进制文件并在 SimpleperfExampleCpp 中查找二进制文件,生成 binary_cache
$ ./binary_cache_builder.py -lib path_of_SimpleperfExampleCpp

run_simpleperf_on_device.py

run_simpleperf_on_device.py 脚本将 simpleperf 可执行文件推送到设备上,并在设备上运行 simpleperf 命令。 它比手动运行 adb 命令更方便。

报告的脚本

report.py

report.py 是对 simpleperf record 命令的高级封装,它可以接收 record 命令的所有参数。

# 调用图
$ ./report.py -g

# 在由 Python Tk 实现的 GUI 窗口中报告调用图。
$ ./report.py -g --gui

report_html.py

report_html.py 可以将本地的 perf.data 数据转换成一个 HTML 文件,然后通过浏览器可以直接访问查看性能数据信息,包括图表统计、示例表、火焰图、每个函数的带注释的源代码、每个函数的带注释的反汇编等。

# 生成基于 perf.data 的图表统计、样本表和火焰图。
$ ./report_html.py

# 添加源代码。
$ ./report_html.py --add_source_code --source_dirs path_of_SimpleperfExampleCpp

# 添加反汇编内容。
$ ./report_html.py --add_disassembly

# 添加所有二进制文件的反汇编内容可能会耗费大量时间。因此,我们可以选择只为选定的二进制文件添加反汇编内容。
$ ./report_html.py --add_disassembly --binary_filter libgame.so

# report_html.py 接受多个记录数据文件。
$ ./report_html.py -i perf1.data perf2.data

Danger

注意:生成的 Html 可能会加载失败,这是无法访问 JavaScript 源,解决办法参考 生成火焰图无法加载

inferno

inferno 是一个用于在 html 文件中生成火焰图的工具。

# 基于 perf.data 生成火焰图。
# 在 Windows 上,请使用 inferno.bat 而不是 ./inferno.sh。
$ ./inferno.sh -sc --record_file perf.data

# 记录一个本地程序并生成火焰图。
$ ./inferno.sh -np surfaceflinger

report_sample.py

report_sample.py 将分析数据文件转换为 linux-perf-tool 输出的 perf 脚本文本格式。

该格式可以导入到:

# 将性能分析记录到 perf.data 中。
$ ./app_profiler.py <args>

# 将当前目录中的 perf.data 转换为 FlameGraph 使用的格式。
$ ./report_sample.py --symfs binary_cache > out.perf

$ git clone https://github.com/brendangregg/FlameGraph.git
$ FlameGraph/stackcollapse-perf.pl out.perf > out.folded
$ FlameGraph/flamegraph.pl out.folded > a.svg

脚本总结

下表是对上水提到的脚本功能的总结:

脚本名称 功能描述
性能分析相关脚本
app_profiler.py 用于分析应用程序性能,收集性能数据(如CPU使用率、内存等)
run_simpleperf_on_device.py 在设备上运行Simpleperf工具进行性能分析(需USB连接)
api_profiler.py 分析应用程序的API调用性能
run_simpleperf_without_usb_connection.py 在无USB连接的情况下运行Simpleperf进行性能分析
binary_cache_builder.py 构建二进制缓存,用于优化性能分析过程中的符号解析
报告生成相关脚本
report.py 生成性能分析报告(文本格式)
report_html.py 生成HTML格式的性能分析报告,支持可视化展示
inferno 将Simpleperf数据转换为火焰图(FlameGraph)的工具
report_sample.py 生成采样报告,展示具体的采样数据点

Simpleperf 的脚本很多,到底应该如何选择这些脚本呢?首先,我们需要弄清楚这些脚本的之间的关系:

脚本的选择

上图中提到了一些 Simpleperf 以外的性能分析工具,比如 Firefox Profiler、FlameGrap、PProf 和 Android Studio 等。通过 Simpleperf 提供的脚本可以将数据很轻松地转换成其他工具可以识别的格式,从而在不同的工具上打开和访问它们。以下是这些工具的基本介绍:

  • Firefox Profiler:火狐性能分析器。是一个用于分析和优化 Firefox 浏览器性能的工具。通过 Firefox Profiler,用户可以捕获和分析浏览器在运行过程中的各种性能数据,包括 CPU 使用率、内存占用、网络请求、渲染时间等指标。这些数据可以帮助开发者识别潜在的性能瓶颈,并进行针对性的优化。它提供了直观的可视化界面,包括图表和时间线,帮助用户更好地理解性能数据。开发者可以利用这些信息来改进网站性能、优化代码以及提升用户体验。
  • FlameGrap:是一种用于可视化软件程序性能分析数据的工具,最初由 Brendan Gregg 创建。它以图形方式呈现了程序在执行过程中的调用栈信息和函数调用关系,帮助开发人员快速了解程序中的性能瓶颈和优化方向。该工具被广泛应用于分析各种软件系统的性能特征,包括操作系统、应用程序和服务端程序等。
  • PProf:PProf 是一个性能分析工具,通常用于分析 Go 语言程序的性能特征。它可以帮助开发人员定位并解决程序中的性能瓶颈,提高程序的运行效率。PProf 可以生成可视化的分析报告,展示程序在运行过程中 CPU 使用情况、内存分配情况、函数调用频率等信息。开发人员可以通过这些报告快速了解程序的性能特征,并据此进行优化。
  • Android Studio:本身就内置很多 Android 开发中常使用的工具,比如 Profiler、Logcat 等。Android Studio 自身就是一个分析 Android 性能的工具之一。

其实,通过上图不难看出来:Simpleperf 中 perf.data 可以说是一个“万金油”的文件,只要想办法获取到 perf.data 文件就可以通过已有的脚本将其转换成任意格式的文件,然后在不同的性能分析工具上打开它们。

实际生产过程中,如何选择脚本呢?首先,确定你想要什么样的格式文件,然后选择一个最短路径。比如:

  1. 希望获取系统状态的火焰图及其系统信息,那么最短路径就是:perf.data > report.html,脚本选择 report_html.py
  2. 希望获取到差分火焰图,那么最短路径就是:perf.data > a.folded > 火焰图 > 差分火焰图,脚本选择:stackcollapse.py > flamegrap.pl

Note

flamegrap.pldifffolded.pl 不是 Simpleperf 工具自带的,需要单独到 Github 下载

Framework

代码插桩

在 Framework 开发过程中,想要在某个位置抓取 Simpleperf 的数据,可以添加如下的代码:

try {
  // for capability check
  Os.prctl(OsConstants.PR_CAP_AMBIENT, OsConstants.PR_CAP_AMBIENT_RAISE,
           OsConstants.CAP_SYS_PTRACE, 0, 0);
  // Write to /data instead of /data/local/tmp. Because /data can be written by system user.
  Runtime.getRuntime().exec("/system/bin/simpleperf record -g -p " + String.valueOf(Process.myPid())
            + " -o /data/perf.data --duration 30 --log-to-android-buffer --log verbose");
} catch (Exception e) {
  Slog.e(TAG, "error while running simpleperf");
  e.printStackTrace();

Danger

在设备上,需要先关闭 SElinux:adb shell setenforce 0。因为 SELinux 仅允许 Simpleperf 运行在 Shell 或者是可以 debug 和可分析的应用中;

设备启动分析

在 userdebug/eng/userroot 设备上,Simpleperf 可以获取到设备启动时的数据:

Step 1:设置系统 persist 属性

adb shell setprop persist.simpleperf.boot_record true

在 Android 系统中,persist.simpleperf.boot_record 用于控制 Simpleperf 是否在每次系统启动时记录性能数据。如果这个属性被设置为 true,则表示系统会在每次启动时自动记录性能数据;如果设置为 false,则不会。

Step 2:重启设备

adb reboot

当重启设备时,init 查询到这个 persist 属性已经设置的话,它就会 fork 一个后台进程执行 Simpleperf 录制 boot-time 的 profile。init 启动 Simpleperf 在 zygote-start 阶段(即 zygote 启动后)。

Step 3:获取数据

重启完成后,在 /data/simpleperf_boot_data 目录下会存在记录到的数据:

$ adb shell ls /data/simpleperf_boot_data
perf-20220126-11-47-51.data

以下是一个获取到的数据例子。从时间戳看,第一个 sample 被生成大约在启动后的 4.5 秒:

设备启动数据

高级进阶

了解 stat 的工作原理

stat 命令的核心工作原理可分为事件配置、数据采集、结果计算三个阶段:

  1. 事件配置阶段初始化性能监控环境,确定要测量的硬件/软件事件及其监控方式。
    • 事件选择:通过 -e 参数指定监控事件(如 cpu-cyclescache-misses),支持的事件列表可通过 simpleperf list 查询。
    • 多路复用处理:若事件数超过 CPU PMU 硬件计数器数量(如 ARM 通常 4-6 个),内核自动启用时间分片轮询,分组轮流测量事件(默认每组监控 1-10ms)。
    • 监控目标绑定:确定监控范围(如特定进程 -p PID、整个系统 -a 或 Android 应用 --app package_name)。
  2. 数据采集阶段通过 CPU PMU 硬件计数器实时采集性能数据。
    • PMU 寄存器配置:通过 Linux perf_event_open 系统调用,初始化 PMU 计数器并开始计数。
    • 数据读取机制:通过 -I 参数设置读取间隔(如 -I 100 表示每 100ms 读取一次计数器值)。
    • 内核将计数器数据写入 perf buffer,用户态工具(Simpleperf)从中读取。
    • 若启用多路复用,内核按时间片切换事件组,记录每个事件的 time_enabled(总启用时间)和 time_running(实际计数时间)。
  3. 结果计算阶段对原始计数器数据进行处理,生成用户可读的统计结果。
    • 若发生多路复用,按公式缩放数据:真实估值 = 测量值 × (time_enabled / time_running)
    • 指标计算:绝对值计算直接输出事件计数,如 1,000,000 cpu-cycles)。比率指标计算,如 IPC = instructions / cpu-cycles

Note

在执行 Simpleperf 命令时,可以通过 -v 参数查看详细日志。

PMU 多路复用问题

在 Simpleperf 工具中,PMU(Performance Monitoring Unit)多路复用问题 指的是由于 CPU 硬件性能计数器的数量有限(如 ARM Cortex-A77 仅有 6 个物理 PMU 计数器),当使用 stat 命令监控的硬件事件数量(如 CPU 周期、缓存命中/失效等)超过可用计数器时, 内核会通过时间片轮转方式共享硬件寄存器,导致每个事件仅能部分时间被监测,从而引发测量精度下降或某些事件无法同时测量的问题。

在 Simpleperf 输出中,如果看到如下警告,说明发生了多路复用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
$ simpleperf stat -p 7394 -e cache-references,cache-references:u,cache-references:k \
      -e cache-misses,cache-misses:u,cache-misses:k,instructions --duration 1
Performance counter statistics:

#   count  event_name           # count / runtime
  490,713  cache-references     # 151.682 M/sec
  899,652  cache-references:u   # 130.152 M/sec
  855,218  cache-references:k   # 111.356 M/sec
   61,602  cache-misses         # 7.710 M/sec
   33,282  cache-misses:u       # 5.050 M/sec
   11,662  cache-misses:k       # 4.478 M/sec
        0  instructions         #

Total test time: 1.000867 seconds.
simpleperf W cmd_stat.cpp:946] It seems the number of hardware events are more than the number of
available CPU PMU hardware counters. That will trigger hardware counter
multiplexing. As a result, events are not counted all the time processes
running, and event counts are smaller than what really happen

Simpleperf 默认允许多路复用,但可通过 --no-multiplexing 强制禁用:

$ simpleperf stat --no-multiplexing -e cpu-cycles,branch-misses --app com.example.app

或者在执行命令前,先查询每个 CPU 可用的计数器:

1
2
3
4
5
6
7
8
9
$ simpleperf stat --print-hw-counter
There are 2 CPU PMU hardware counters available on cpu 0.
There are 2 CPU PMU hardware counters available on cpu 1.
There are 2 CPU PMU hardware counters available on cpu 2.
There are 2 CPU PMU hardware counters available on cpu 3.
There are 2 CPU PMU hardware counters available on cpu 4.
There are 2 CPU PMU hardware counters available on cpu 5.
There are 2 CPU PMU hardware counters available on cpu 6.
There are 2 CPU PMU hardware counters available on cpu 7.

为了降低 PMU 多路复用对采集到的数据的影响,建议可以从这些方面进行优化。

优化方向 1:减少监控事件的数量,只选择最关键的几个核心事件进行监控(如 cpu-cycles + instructions):

优化方向 2:使用事件分组测量,将需要监控的事件分成多组,分别进行测量。

# 第一次测量 CPU 相关事件
simpleperf stat -e cpu-cycles,instructions -p 1234
# 第二次测量缓存相关事件
simpleperf stat -e cache-references,cache-misses -p 1234

或者,添加 --group 参数将参数进行分组监控。将多个事件绑定为一组,确保它们在同一时间片被测量,避免因多路复用导致的时间片轮换误差:

# 确保 cycles 和 instructions 同步测量,避免比值失真。
simpleperf stat --group cpu-cycles,instructions -p 1234
# 缓存相关事件和分支事件分别成组,减少组内误差。
simpleperf stat \
 --group cache-references,cache-misses \
 --group branch-instructions,branch-misses \
 --app com.example.app

注意:并不是所有的事件都支持分组,比如 ARM 的 stalled-cycles 就有可能独占计数器。也可以手动将某个事件强制独占计数器:

$ simpleperf stat -e 'cpu-cycles:e'  # 保证全程监控

优化方向 3:调整采样参数。可以增大采样间隔(减少多路复用切换频率)或者延长监控时长(提高数据准确性):

simpleperf stat -I 100 -e event1,event2...  # 100ms 采样间隔
simpleperf stat --duration 10 -e event1,event2...  # 监控 10 秒

优化方向 4:绑定到特定CPU核心测量(减少多核竞争):

simpleperf stat --cpu 0 -e event1,event2...  # 仅监控 CPU 0

FAQs

火焰图无法加载

使用 Android NDK 中的 Simpleperf 生成的网页无法打开或加载的问题,一直困扰了很多使用 Simpleperf 的 Android 性能开发工作者。这是因为本地生成的页面依赖一些 JavaScript 文件,国内的网络访问不到 CDN 源,此时需要将这些源替换成国内可以访问的源。

以下是两种解决办法:

  1. 修改 Simpleperf 生成的 HTML 文件,将 JavaScript 源替换成国内网络可以访问到的源;
  2. 修改 Python 脚本,使其在生成 HTML 前完成 JavaScript 源的替换工作。

这两种方法前者仅一次生效,后者永久生效。

修改 HTML 文件

因为这个方法是使用了 Simpleperf 的 Python 生成 HTML 后再去做替换,所以最大的弊端就是每次都需要手动替换。具体的替换步骤如下:

  1. 使用记事本软件打开 HTML 文件(我本地这个文件名字叫:report.html);
  2. 按照如下的方式进行替换:

替换前:

<html>
<head>
    <link rel="stylesheet" type="text/css"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css"></link>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css"></link>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js"></script>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script>google.charts.load('current', {'packages': ['corechart', 'table']});</script>
    <style type="text/css">
        .colForLine {
            width: 50px;
        }

        .colForCount {
            width: 100px;
        }

        .tableCell {
            font-size: 17px;
        }

        .boldTableCell {
            font-weight: bold;
            font-size: 17px;
        }
    </style>
</head>
<body>
<script>

替换后:

<html>
<head>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css"></link>
    <link rel="stylesheet" type="text/css"
          href="https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css"></link>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
    <script src="https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js"></script>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script>google.charts.load('current', {'packages': ['corechart', 'table']});</script>
    <style type="text/css">
        .colForLine {
            width: 50px;
        }

        .colForCount {
            width: 100px;
        }

        .tableCell {
            font-size: 17px;
        }

        .boldTableCell {
            font-weight: bold;
            font-size: 17px;
        }
    </style>
</head>
<body>
<script>

修改 Python 脚本

因为这个方法是在 HTML 生成之前就做了替换,所以最大的优点就是不需要每次手动替换。在获取到 perf.data 文件后,我们会使用 Simpleperf 中的 report_html.py 脚本进行渲染出 report.html 文件:

python ./report_html.py -i D:\temp\perf.data -o D:\temp\report.html

因此,需要修改的是 report_html.py 文件。具体步骤如下:

  1. 使用记事本打开 report_html.py;
  2. 搜索关键字“URLS”,找到 JavaScript 数据源;
  3. 按照如下的方式进行替换:

替换前:

URLS = {
    'jquery': 'https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js',
    'bootstrap4-css': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css',
    'bootstrap4-popper':
        'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js',
    'bootstrap4': 'https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js',
    'dataTable': 'https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js',
    'dataTable-bootstrap4': 'https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js',
    'dataTable-css': 'https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css',
    'gstatic-charts': 'https://www.gstatic.com/charts/loader.js',
}

替换后:

URLS = {
    'jquery': 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js',
    'bootstrap4-css': 'https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css',
    'bootstrap4-popper':
        'https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js',
    'bootstrap4': 'https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.1.2/js/bootstrap.min.js',
    'dataTable': 'https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js',
    'dataTable-bootstrap4': 'https://cdn.datatables.net/1.10.19/js/dataTables.bootstrap4.min.js',
    'dataTable-css': 'https://cdn.datatables.net/1.10.19/css/dataTables.bootstrap4.min.css',
    'gstatic-charts': 'https://www.gstatic.com/charts/loader.js',
}