美文网首页perfetto 官方文档翻译
Perfetto 翻译第十篇-案例学习-分析内存使用

Perfetto 翻译第十篇-案例学习-分析内存使用

作者: David_zhou | 来源:发表于2023-10-07 20:22 被阅读0次

    前言:虽然有翻译软件,虽然有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并展开。这将显示相机的各种内存统计数据的时间线。

    camera_memory.png
    我们可以看到,大约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.

    memory_lmk.png
    我们可以看到,相机的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,并单击显示的菱形标记。

    profile-diamond.png

    可用的选项卡包括

    • 未释放的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 ,然后点击菱形标记。

    profile-diamond.png

    这将显示对象到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 水平实在有限,闻过则喜,希望有更多的人反馈,期待更好的建议

    相关文章

      网友评论

        本文标题:Perfetto 翻译第十篇-案例学习-分析内存使用

        本文链接:https://www.haomeiwen.com/subject/usevbdtx.html