Perfetto 翻译第十篇-案例学习-分析内存使用
前言:虽然有翻译软件,虽然有chatgpt,毕竟语言隔阂,对这个工具还是一知半解,因此想通过翻译的方式和大家来一起学习下Perfetto这个强大的工具
#####################以下分割线#####################
英文原文在这里
前提条件
ADB已安装。
运行Android 10+的设备。
可评测或可调试的应用程序。如果你运行的是Android的“用户”版本(而不是“userdebug”或“eng”),你的应用程序需要在manifest中标记为可评测或可调试。有关更多详细信息,请参阅文档。
dumpsys meminfo
dumpsys meminfo是开始研究进程内存使用情况的一个较好的方式,它提供了一个进程正在使用的各种类型内存的概况。
$ adb shell dumpsys meminfo com.android.systemui
Applications Memory Usage (in Kilobytes):
Uptime: 2030149 Realtime: 2030149
** MEMINFO in pid 1974 [com.android.systemui] **
Pss Private Private SwapPss Rss Heap Heap Heap
Total Dirty Clean Dirty Total Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------
Native Heap 16840 16804 0 6764 19428 34024 25037 5553
Dalvik Heap 9110 9032 0 136 13164 36444 9111 27333
[more stuff...]
查看Dalvik堆(即Java堆)和Native堆的“Private Dirty”列,我们可以看到SystemUI在Java堆上的内存使用量是9M,在Native堆上是17M。
linux内存管理
那上面其他的clean, dirty, Rss, Pss, Swap 代表什么意思,为了回答这个问题,我们需要深入研究下linux内存管理。
从内核的角度来看,内存被划分为大小相等的块,称为页。页的大小通常是4KiB。
页被组织在称为VMA(虚拟内存区域)的虚拟连续范围中。
当进程通过mmap())系统调用请求新的内存页池时,就会创建VMA。应用程序很少直接调用mmap(),而是通过由本地进程的分配器malloc()/operator new()或Java应用程序的Android RunTime来间接调用。
VMA可以有两种类型:有后备文件的映射和匿名的映射。
有后备文件 VMAs: 有后备文件的VMA是内存中文件的映射。它们是通过向mmap()传递文件描述符而获得的。内核在VMA上通过缺页中断的方式来传输文件,因此读取指向VMA的指针相当于文件上的read()。例如,动态链接器(ld)在执行新进程或动态加载库时使用文件支持的VMA,或者Android framework在加载新的.dex库或访问APK中的资源时使用的都是有后备文件的的VMA。
匿名VMA:是没有任何文件映射的内存区域。这是内核请求内存时分配的方式。匿名VMA是通过调用mmap(…MAP_ANONMOUS…)获得的。
只有当应用程序尝试从VMA读取/写入时,才会以页粒度分配物理内存。如果你分配了32 MiB的页,但如果只改动了一个字节,你的进程的内存使用量只会增加4KiB。进程的虚拟内存将增加32 MiB,但其驻留物理内存只增加4 KiB。
在优化程序的内存使用时,我们感兴趣的是减少它们在物理内存中的占用。在现代操作上,虚拟内存使用率高通常不值得担心(除非地址空间用完,这在64位系统上极少发生)。
我们将驻留在物理内存中的进程内存大小称为RSS(resident Set Size)。但常驻内存是分类型的。
从内存消耗的角度来看,VMA中的各个页面可以具有以下状态:
-
常驻: 页面被映射到一个物理内存页面。常驻页面可以处于两种状态:
-
** Clean** (仅适用于文件映射的页): 页的内容与磁盘上的内容相同. 在内存压力较大的情况下,内核可以更容易地收回干净的页。这是因为如果再次需要它们,内核知道可以通过从底层文件中读取它们来重新创建内容。
-
Dirty: 页面的内容与磁盘不同,或者(在大多数情况下)页面没有磁盘备份(即匿名)。脏页无法收回,因为这样做会导致数据丢失。但是,如果存在数据,它们可以在磁盘或ZRAM上交换。
-
-
交换: 脏页可以写入磁盘上的交换文件(在大多数Linux桌面发行版上)或压缩(在Android和CrOS上通过ZRAM)。页将保持交换状态,直到其虚拟地址出现新的缺页中断,此时内核将把它带回主存中。
-
不存在:在页上从未发生过缺页中断,或者页是被回收,随后被释放。
减少dirty内存的数量通常更重要,因为dirty内存不能像其他内存那样回收。而且在安卓系统上,即使换成ZRAM,也会占用部分系统内存,这就是我们在dumpsys meminfo示例中查看Private Dirty的原因。
共享内存可以映射到多个进程中。这意味着不同进程中的VMA指向相同的物理内存。这种情况通常发生在常用库(如libc.so、framework.dex)的文件备份内存中,或者更罕见的是,当进程fork()和子进程从其父进程继承dirty内存时。
这介绍了PSS(Proportional Set Size)的概念。在PSS中,驻留在多个进程中的内存按比例分配给每个进程。如果我们将一个4KiB页面映射到四个进程中,每个进程的PSS将增加1KiB。
回顾
动态分配的内存,无论是通过C的malloc()、C++的运算符new()还是Java的new X()分配的,除非从未使用过,否则总是以匿名和脏的方式启动。
如果此内存在一段时间内没有读/写,或者在内存压力的情况下,它会在ZRAM上交换出来,并变为交换。
匿名内存,无论是驻留的(因此是脏的)还是交换的,总是占用资源,如果不必要,应该避免。
文件映射内存来自代码(java或native)、库和资源,并且几乎总是干净的。干净的内存也会消耗系统内存,但通常应用程序开发人员对它的控制较少。
随时间变化的内存
dumpsys meminfo可以很好地获取当前内存使用情况的快照,但即使是很短的内存峰值也会导致内存不足,从而导致LMK。我们有两种工具来调查这种情况
RSS高水位线。
内存跟踪点。
RSS高水位线
我们可以从/proc/[pid]/status文件中获得很多信息,包括内存信息。VmHWM 显示进程自启动以来的最大RSS使用量。该值由内核保持更新。
$ adb shell cat '/proc/$(pidof com.android.systemui)/status'[...]
VmHWM: 256972 kB
VmRSS: 195272 kB
RssAnon: 30184 kB
RssFile: 164420 kB
RssShmem: 668 kB
VmSwap: 43960 kB
[...]
内存跟踪点
注意:有关内存跟踪点的详细说明,请参阅{数据源>内存>计数器和事件](https://perfetto.dev/docs/data-sources/memory-counters)页面。
我们可以使用Perfetto从内核获取有关内存管理的信息。
$ adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace \
<<EOF
buffers: {
size_kb: 8960
fill_policy: DISCARD
}
buffers: {
size_kb: 1280
fill_policy: DISCARD
}
data_sources: {
config {
name: "linux.process_stats"
target_buffer: 1
process_stats_config {
scan_all_processes_on_start: true
}
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "mm_event/mm_event_record"
ftrace_events: "kmem/rss_stat"
ftrace_events: "kmem/ion_heap_grow"
ftrace_events: "kmem/ion_heap_shrink"
}
}
}
duration_ms: 30000
EOF
当执行命命令时,会不断收集内存的使用情况。
使用adb Pull/data/misc/perfetto-traces/trace~/mem-trace提取文件并上传到perfetto UI。这将显示有关系统ION使用情况的总体统计数据,以及每个进程的统计数据。向下滚动(或按Ctrl-F键)到com.google.android.GoogleCamera并展开。这将显示相机的各种内存统计数据的时间线。
我们可以看到,大约2/3的位置,内存飙升(在mem.rss.anon track中)。这就是拍照的地方。这是了解应用程序的内存不同情况使用内存的好方法。
工具的选择
如果想深入研究由Java代码分配的匿名内存(由dumpsys meminfo标记为Dalvik Heap),请参阅分析Java堆部分。
如果想深入研究由native代码分配的匿名内存,该内存由dumpsys meminfo标记为native堆,请参阅分析native堆一节。请注意,即使您的应用程序没有任何C/C++代码,也经常会使用native内存。这是因为某些框架API(例如Regex)的实现是通过native代码在内部实现的。
如果想深入到有文件映射的内存中,最好的选择是使用adb shell showmap PID(在Android上)或检查/proc/PID/smaps。
Low-memory kills
当Android设备的内存不足时,名为lmkd的守护程序将开始终止进程,以释放内存。不同设备的策略不同,但通常进程将按oom_score_adj分数的降序被终止(即首先被终止的是后台应用程序和进程,最后是前台进程)。
Android上的应用程序在切换后不会被终止。相反,即使用户不再使用app,它们也会保持缓存状态。这是为了使应用程序的后续启动更快。这样的应用程序通常会首先被杀死(因为它们具有更高的oom_score_adj)。
我们可以使用Perfetto收集有关LMK和oom_score_adj的信息。
$ adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace \
<<EOF
buffers: {
size_kb: 8960
fill_policy: DISCARD
}
buffers: {
size_kb: 1280
fill_policy: DISCARD
}
data_sources: {
config {
name: "linux.process_stats"
target_buffer: 1
process_stats_config {
scan_all_processes_on_start: true
}
}
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "lowmemorykiller/lowmemory_kill"
ftrace_events: "oom/oom_score_adj_update"
ftrace_events: "ftrace/print"
atrace_apps: "lmkd"
}
}
}
duration_ms: 60000
EOF
使用命令`adb pull /data/misc/perfetto-traces/trace ~/oom-trace提取文件,并上传到 Perfetto UI.
我们可以看到,相机的OOM分数在打开时减少(使其不太可能被杀死),在关闭后再次增加。
分析native内存使用
Native Heap Profiles 需要 Android 10.
注意:有关native堆分析器和疑难解答的详细说明,请参阅数据源>堆分析器页面。
应用程序通常通过malloc或C++的new获得内存,而不是直接从内核获得内存。分配器确保更有效地处理内存(即不会太浪费内存),并且请求内核的开销保持较低。
我们可以记录native分配,并使用heapprofd释放进程所做的操作。生成的profile文件可以用于将内存使用情况归因于特定的函数调用堆栈,支持native代码和Java代码的混合。profile文件仅显示其运行时完成的分配,不会显示之前完成的任何分配。
抓取profile
使用 tools/heap_profile
脚本profile 进程。如果遇到问题,请确保使用的是最新版本。使用tools/heap_profile-h查看所有参数,或使用默认值并profile 一个进程(例如,system_server):
$ tools/heap_profile -n system_server
Profiling active. Press Ctrl+C to terminate.
You may disconnect your device.
Wrote profiles to /tmp/profile-1283e247-2170-4f92-8181-683763e17445 (symlink /tmp/heap_profile-latest)
These can be viewed using pprof. Googlers: head to pprof/ and upload them.
当Profiling运行时,稍稍把玩下手机。之后按Ctrl-C结束profile。对于本教程,我打开了几个应用程序。
查看数据
然后将原始跟踪文件从输出目录上传到Perfetto UI,并单击显示的菱形标记。
可用的选项卡包括
-
未释放的malloc大小:在创建dump时,在此调用堆栈中分配但未释放的字节数。
-
总malloc大小:在此调用堆栈中分配了多少字节(包括dump时释放的字节)。
-
未释放的malloc计数:在此调用堆栈中多少未释放的分配对象数。
-
总malloc计数:在此调用堆栈中完成了多少分配的对象数(包括具有匹配空闲的分配)。
默认视图将显示在profile时完成但未释放的所有分配(空格选项卡)。
memory_profile.png
我们可以看到,通过AssetManager.applyStyle在调用过程中分配了大量内存。要获得调用分配的总内存,我们可以在Focus文本框中输入“applyStyle”。这将仅展示与“applyStyle”匹配的调用堆栈。
native-heap-prof-focus.png
从这里我们清晰的知道我们想要查找的的代码。从代码中,我们可以看到内存是如何使用的,以及我们是否确实需要所有的内存。
分析 Java Heap
Java Heap Dumps 需要系统在Android 11及以上
注意:有关捕获Java堆转储和故障排除的详细说明,请参阅Data sources>Java heap dumps页面。
Dumping the java heap
我们可以获得构成Java堆的所有Java对象的图的快照。我们使用tools/java_heap_dump脚本。如果遇到问题,请使用的最新版本。
$ tools/java_heap_dump -n com.android.systemui
Dumping Java Heap.
Wrote profile to /tmp/tmpup3QrQprofile
This can be viewed using https://ui.perfetto.dev.
查看数据
将文件上传到 Perfetto UI ,然后点击菱形标记。
这将显示对象到GC root的最短路径的内存的火焰图。通常,对象可以通过许多路径到达,我们只显示最短的,因为这降低了所显示数据的复杂性,并且通常是高可信.。最右边的[合并]堆栈是所有太小而无法显示的对象的总和。
java-heap-graph.png
可用的选项卡包括
- 内存大小:通过此GC根路径保留的字节数。
- 对象数量:通过该GC根路径保留的对象数量。
如果我们只想看到具有包含某些字符串的帧的调用堆栈,则可以使用Focus功能。比如如果我们想知道与notification相关的所有分配,我们可以将“notification”放在“焦点”框中。
与navtive堆配置文件一样,如果我们希望关注图的某些特定方面,则可以按类的名称进行过滤。比如如果我们想查看notification可能关联的内存使用,我们可以将“notification”放在“焦点”框中。
java-heap-graph-focus.png
我们聚合每个类名的路径,因此如果java.lang.Object[]还有很多存活的对象,我们将显示一个元素作为其子元素,正如您在上面最左侧的堆栈中看到的那样。
#####################以上分割线#####################
后记:
1 本次主要使用百度翻译,虽然被骂,但至少翻译这个工具降低了门槛。
2 英文文档中的长难句真的是又长又难,基于百度的翻译,然后自己再调整下,水平实在有限。
3 技术背景知识不够,有些专有名词不知道怎么翻译,也不知道百度翻译的是否准确,功夫在诗外。
4 万事开头难,中间难不难,还不知道。中间的事后面再说,正确一天翻译一篇。
5 虽然可能会有人不屑,但总要有人去做不起眼的小事。
6 google 厉害,这个perfetto 工具也很厉害。君子善假于物也。
7 工具的使用是最简单的入门,背后还有更多的东西值得学习。
8 水平实在有限,闻过则喜,希望有更多的人反馈,期待更好的建议