什么是simpleperf
Simpleperf是Android平台的一个本地层性能分析工具。它的命令行界面支持与linux-tools perf大致相同的选项,但是它还支持许多Android特有的改进。
Simpleperf是Android开源项目(AOSP)的一部分。其源代码 位于。其最新的文档 位于。Bugs 和 功能需求可以提交到 githb上。
Simpleperf是如何工作的
现代的CPU具有一个硬件组件,称为性能监控单元(PMU)。PMU具有一些硬件计数器,计数一些诸如 经历了多少次CPU周期,执行了多少条指令,或发生了多少次缓存未命中 等的事件。
Linux内核将这些硬件计数器包装到硬件perf事件 (hardware perf events)中。此外,Linux内核还提供了独立于硬件的软件事件和跟踪点事件。Linux内核通过 perf_event_open 系统调用将这些都暴露给了用户空间,这正是simpleperf所使用的机制。
Simpleperf具有三个主要的功能:stat,record 和 report。
Stat命令给出了在一个时间段内被分析的进程中发生了多少事件的摘要。以下是它的工作原理:
- 给定用户选项,simpleperf通过对linux内核进行系统调用来启用分析。
- Linux 内核在调度到被分析进程时启用计数器。
- 分析之后,simpleperf从内核读取计数器,并报告计数器摘要。
Record 命令在一段时间内记录剖析进程的样本。它的工作原理如下:
- 给定用户选项,simpleperf通过对linux内核进行系统调用来启用分析。
- Simpleperf 在simpleperf 和 linux 内核之间创建映射缓冲区。
- Linux 内核在调度到被分析进程时启用计数器。
- 每次给定数量的事件发生时,linux 内核将样本转储到映射缓冲区。
- Simpleperf 从映射缓冲区读取样本并生成 perf.data。
Report 命令读取 “perf.data” 文件及所有被剖析进程用到的共享库,并输出一份报告,展示时间消耗在了哪里。
主 simpleperf 命令
Simpleperf 支持一些子命令,包括 list,stat,record,report。每个子命令支持不同的选项。这一节只描述最重要的子命令和选项。要了解所有的子命令和选项,请使用 –help。
simpleperf list
simpleperf list 被用于列出设备上所有可用的事件。由于应该和内核的差异,不同的设备可以支持不同的事件。
simpleperf stat
simpleperf stat 被用于获取被剖析程序或系统范围内的原始事件计数器信息。通过传入选项,我们可以选择使用哪些事件,监视哪个进程/线程,监视多长时间,以及打印的间隔。下面是一个例子。
选择事件
我们可以通过 -e 选项选择使用哪个事件。下面是例子:
当运行 stat 命令时,如果硬件事件的数量大于 PMU中可用的硬件计数器的数量,则内核在事件间共享硬件计数器,因此每个事件只在总时间中的一部分内被监视。在下面的例子中,每一行的最后都有一个百分比,展示了每个事件实际被监视的时间占总时间的百分比。
|
|
在上面的例子中,每个事件被监视的时间大概占总时间的 87%。但是,不保证任何一对事件总是在相同的时间被监视。如果我们想要让一些事件在同一时间被监视,我们可以使用 –group 选项。下面是一个例子。
选择监视目标
我们可以通过 -p 选项或 -t 选项选择监视哪个进程或线程。监视一个进程如同监视进程中的所有线程。Simpleperf 也可以 fork 一个子进程来运行新命令,然后监视子进程。下面是例子。
决定监视多长时间
当监视已有线程时,我们可以使用 –duration 选项决定监视多长时间。当监视执行一个新命令的子进程时,simpleperf 将一直监视子进程直至其结束。在这种情况下,我们可以使用 Ctrl-C 在任何时间停止监视。例子如下。
决定打印的间隔
当监视 perf 计数器时,我们还可以使用 –interval 选项决定打印的间隔。例子如下。
在 systrace 中显示计数器
simpleperf 还可以与systrace一起工作来将计数器转储进收集的trace中。下面是一个执行系统范围的 stat 的例子。
simpleperf record
simpleperf record用于转储被剖析程序的记录。通过传入选项,我们可以选择使用哪个事件,监视哪个进程/线程,以什么频率转储记录,监视多长时间,以及将记录存储到哪里。
|
|
选择事件
在大多数情况下,cpu-cycles 事件被用于评估消耗的CPU时间。作为一个硬件事件,它精确而高效。我们还可以通过 -e 选项使用其它事件。下面是一个例子。
选择监视目标
record命令中选择目标的方式与 stat 命令中的类似。例子如下。
设置记录的频率
我们可以通过 -f 或 -c 选项设置转储记录的频率。比如,-f 4000 意味着当监视的线程运行时每秒转储接近 4000 个记录。如果监视的线程一秒钟运行了 0.2 s(其它时间它可能被抢占或阻塞),simpleperf 每秒转储大约 4000 * 0.2 / 1.0 = 800 个记录。另一种方式是使用 -c 选项。比如,-c 10000 意味着每发生 10000 次事件转储一个记录。例子如下。
决定监视多长时间
record 命令中决定监视多长时间的方式与 stat 命令中的类似。例子如下。
设置存储记录的路径
默认情况下,simpleperf 将记录保存至当前文件夹下的 perf.data 文件中。我们可以使用 -o 选项设置存储记录的路径。下面是一个例子。
simpleperf report
simpleperf report 被用来基于 simpleperf record 命令生成的 perf.data 产生报告。Report 命令将记录分组为不同的样本项,基于每个样本项包含的事件的多少对样本项排序,并打印每个样本项。通过传入选项,我们可以选择到哪里寻找被监视的程序使用的 perf.data 和 可执行二进制文件,过滤不感兴趣的记录,并决定如何分组记录。
下面是一个例子。记录被分为 4 个样本项,每项一行。有一些列,每列展示了属于一个样本项的信息片段。第一列是 Overhead,它展示了当前样本项中的事件占总事件的百分比。由于 perf 事件是 cpu-cycles,overhead 可被视为是每个函数占用的 cpu 的百分比。
设置存储记录的路径
默认情况下,simpleperf 读取当前目录下的 perf.data。我们可以使用 -i 选项选择从另一个文件读取记录。
设置查找可执行二进制文件的路径
如果要生成函数符号报告,simpleperf 需要读取被监视的进程使用的可执行二进制文件来获取符号表和调试信息。默认情况下,路径是记录时被监视的进程使用的可执行二进制文件,然而,在生成报告时这些二进制文件可能不存在,或不包含符号表和调试信息。因此我们可以使用 –symfs 来重定向路径。下面是一个例子。
过滤记录
当生成报告时,可能不是对所有记录都感兴趣。Simpleperf 支持五钟过滤器来选择感兴趣的记录。下面是例子。
决定如何将记录分组为样本项
Simpleperf 使用 –sort 选项决定如何分组样本项。下面是例子。
simpleperf 的特性
Simpleperf 的工作方式与 linux-tools-perf 类似,但它有如下的提升:
- Aware of Android environment. Simpleperf 在剖析时处理一些Android特有的情形。例如,它可以剖析 apk 中嵌入的共享库,从 .gnu_debugdata 段读取符号表和调试信息。如果可能,当出现错误时它会给出一些提示,如如何禁用perf_harden启用分析。
- 记录时支持展开。如果我们想使用 -g 选项来记录并报告一个程序的调用图,我们需要转储每个记录中的用户栈和寄存器集合,然后展开栈来查找调用链。Simpleperf 支持在记录时展开,因此它无需在 perf.data 中存储用户栈。因而我们可以在设备上有限的空间内剖析更长的时间。
- 支持脚本使Android上的剖析更方便。
- 编译进静态的二进制文件。Simpleperf 是一个静态库,因此它无需支持运行共享库。这意味着对于 simpleperf 可以运行的的 Android 版本没有限制,尽管有些设备不支持剖析。
ndk中的Simpleperf工具
ndk 中的 simpleperf 工具包含三个部分:在 Android 设备上运行的 simpleperf 可执行文件,在主机上运行的 simpleperf 可执行文件,和 python 脚本。
设备上的 simpleperf
在设备上运行的 simpleperf 位于 bin/android 目录下。它包含不同体系架构的 Android 上运行的静态二进制文件。它们可被用于剖析运行在设备上的进程,并生成 perf.data。
主机上的 simpleperf
运行与主机上的 Simpleperfs 位于bin/darwin,bin/linux 和 bin/windows。它们可被用于在主机上解析 perf.data。
脚本
脚本被用于使得剖析和解析剖析结果更方便。app_profiler.py 被用于剖析一个 android 应用程序。它准备剖析环境,下载 simpleperf 到设备上,在主机上生成并拉出 perf.data。它由 app_profiler.config 配置。binary_cache_builder.py 被用于从设备拉出本地层二进制文件到主机上。它由 app_profiler.py 使用。annotate.py 被用于使用 perf.data 注解源文件。它由 annotate.config 配置。report.py 在一个 GUI 窗口中报告 perf.data。simpleperf_report_lib.py 被用于枚举 perf.data 中的样本。在内部它使用 libsimpleperf_report.so 来解析 perf.data。它可被用于将 perf.data 中的样本翻译为其它形式。使用 simpleperf_report_lib.py 的一个例子是 report_sample.py。
使用 simpleperf 工具的例子
这个部分展示了如何使用 simpleperf 工具来剖析一个 Android 应用。
准备一个可调试(debuggable)的应用
应用程序的包名是 com.example.sudogame。它包含 java 代码和 c++ 代码。我们需要运行一份在其 AndroidManifest.xml 元素中 android:debuggable=”true” 的app,因为我们不能为 non-debuggable apps 使用 run-as。应用应该已经安装在设备上了,而且我们可以通过 adb 连接设备。
使用命令行剖析
为了记录剖析数据,我们需要下载 simpleperf 和包含调试信息的本地库到设备上,运行 simpleperf 产生剖析数据:perf.data,并运行 simpleperf 为 perf.data 生成报告。步骤如下。
1. Enable profiling
|
|
2. 寻找运行 app 的进程
在 app 的上下文运行 ps
。在 >=O 的设备上,用 ps -e
来替代。
因此是进程 10324 运行app。
3. 将 simpleperf 下载到 app 的 data 目录
首先我们需要找出 app 使用的是哪个体系架构。有许多中方式,这里我们只检查进程的映射。
路径显示是它是 arm。因此我们将 simpleperf 下载到设备上的 arm 目录下。
4. 记录 perf.data
|
|
在记录时不要忘记运行 app。否则,我们可能无法获得样本,由于进程仍在休眠。
5. 为 perf.data 生成报告
有不同的方式来为 perf.data 生成报告。下面展示了一些例子。
报告不同线程中的样本。
报告主线程中不同二进制文件中的样本。
报告主线程中 libsudo-game-jni.so 中的不同函数的采样。
在上面的结果中,大多数符号是 二进制文件名[+virual_addr] 的形式呈现。那是由于设备上使用的 libsudo-game-jni.so 已经抛离了 .symbol 段。我们可以将带有调试信息的 libsudo-game-jni.so 下载到设备上。在 android studio工程中,它位于 app/build/intermediates/binaries/debug/arm/obj/armeabi-v7a/libsudo-game-jni.so。我们不得不下载 libsudo-game-jni.so 到与 perf.data 中记录的相同的相对路径(否则,simpleperf无法找到它)。在这个例子中,是/data/app/com.example.sudogame-1/lib/arm/libsudo-game-jni.so。
为包含了调试信息的库使用的符号生成报告。
报告一个函数中的采样。
6. 记录并报告调用图
调用图是显示了函数调用关系的树。下面是一个例子。
基于调用图记录 dwarf
为了生成调用图,simpleperf 需要为每个记录生成调用链。Simpleperf 需要内核为每个记录转储用户栈和用户寄存器集,然后它追踪用户栈来查找函数调用链。为了解析调用链,它需要 dwarf 调用帧信息的支持,这些通常位于二进制文件的 .eh_frame 或 .debug_frame段。因此我们需要使用 –symfs 指出带有调试信息的 libsudo-game-jni.so 位于哪里。
注意内核无法转储 >= 64K 的用户栈,因此基于调用图的 dwarf 不要包含消耗了 >= 64K 栈的调用链。此外,由于我们需要转储每个记录的栈,则可能丢失一些记录。通常,失去一些记录没关系。
基于调用图记录栈帧
另外一种生成调用图的方式依赖内核为每个记录解析调用链。为了使它成为可能,内核需要能够识别每个函数调用的栈帧。这不总是可能的,因为编译器可能优化掉栈帧,或内核无法识别使用的栈帧风格。因此它如何工作视情况而定(它在 arm64 上工作的很好,但在 arm 上不行)。
报告调用图
报告累积的周期。在下面的表中,第一列是 “Children”,它是函数及那个函数调用的函数所占的cpu 周期百分比。第二列是 “Self”,它只是一个函数的cpu 周期百分比。比如,checkValid() 自身消耗 1.28% 的 cpus,但通过运行它自身及调用其它函数,它消耗了 29.43%。
报告调用图。
以 callee 模式报告调用图。我们还可以展示一个函数是如何被其它函数调用的。
剖析 Java 代码
Simpleperf 只支持剖析 ELF 格式二进制文件中的本地指令。如果 java 代码由解释器执行,或使用 jit 缓存,则它不能由 simpleperf 剖析。由于 Android 支持提前编译,它可以将 java 字节码编译为包含调试信息的本地层指令。在 Android 版本 <= M 的设备上,我们需要 root 权限来编译包含调试信息的 java 字节码。然而,在 Android 版本 >= N 的设备上,我们无需 root 权限就可以做这些。
在Android N上
1. 将 java 代码完整编译为本地层指令。
|
|
2. 记录 perf.data
|
|
3. 为 perf.data 生成报告
|
|
在 Android M上
在 M 设备上,我们需要 root 权限来强制 Android 将 java 代码完全编译为带调试信息的 ELF 二进制文件中的本地层指令。我们还需要 root 权限来读取编译后的本地层二进制文件(由于 installd 将它们写到了一个 uid/gid 是 system:install的目录下)。因而剖析 java 代码只能在 root 了的设备上完成。
在 Android L上
在 L 设备上,我们也需要 root 权限来编译带调试信息的 app 并访问本地层二进制文件。
使用脚本剖析
尽管使用命令行很灵活,但它可能太复杂了。因而我们提供了 pthon 脚本来帮助运行命令。
使用 app_profiler.py 记录
app_profiler.py 用于剖析 Android 应用程序。它设置剖析环境,下载 simpleperf 和带有调试信息的本地层库,运行 simpleperf 产生 perf.data,并从设备上将 perf.data 和二进制文件拉到主机上。它由 app_profiler.config 配置。下面是一个例子。
app_profiler.config:
运行 app_profiler.py:
它将生成的 perf.data 拉到主机上,并从设备的 binary_cache 中收集二进制文件。
使用 report.py 生成报告
|
|
它生成一个GUI接口来报告数据。
使用 simpleperf_report_lib.py 处理样本
simpleperf_report_lib.py 提供了一个接口从 perf.data 读取样本。一个例子是 report_sample.py。
展示流程图
|
|
注解源代码
annotate.py 读取 perf.data 和 binary_cache 下的二进制文件。然后它知道每个样本命中哪个 源文件:line。因此它可以注解源代码。annotate.py 由 annotate.config 配置。下面是一个例子。
annotate.config:
运行 annotate.py:
它生成 annotated_files 目录。 annotated_files/summary 文件包含每个源文件的概要信息。例子如下。
annotated_files/ 还包含由 annotate.py 找到的经过注解的源文件。比如, libsudo-game-jni.cpp 中的 checkValid() 函数的一部分注解后如下。