写点什么

百度 App Android 启动性能优化 - 工具篇

作者:百度Geek说
  • 2022 年 9 月 15 日
    上海
  • 本文字数:11722 字

    阅读完需:约 38 分钟

一、前言

启动性能是 APP 的极为重要的一环,启动阶段出现卡顿、黑屏问题,会影响用户体验,导致用户流失。百度 APP 在一些比较低端的机器上也有类似启动性能问题,为保留存,需要对启动流程做深入优化。现有的性能工具,无法高效的发现、定位性能问题,归因分析和防劣化成本很高,需要对现有工具进行二次开发,提升效率。


1.1 工具选型


做好性能优化,不仅需要趁手的工具,而且对工具的要求还很高,具体来说,必需满足要求:


  • 高性能,保证自身高性能,以防带偏优化方向

  • 多维度,能监控多维度信息,帮助全面发现问题

  • 易用性,方便的可视化界面,方便分析


目前业界主流的 APP 性能探测工具有 TraceView、CPU Profiler、Systrace、Perfetto。



Perfetto 提供了强大的 Trace 分析模块:Trace Processor,可以把多种类型的日志文件(Android systrace、Perfetto、linux ftrace)通过解析、提取其中的数据,结构化为 SQLite 数据库,并且提供基于 SQL 查询的 Python API,可通过 python 实现自动化分析;同时有良好的可视化页面,可通过可视化页面查看火焰图和写 SQL 进行 Trace 分析。


从性能、监控维度的丰富程度和提供的配套的分析和可视化工具来选择,Perfetto 是最好的选择,但前期由于 Perfetto 是 9.0 以后默认内置服务但是默认不可用,Android 11 服务才默认可用,对低版本系统支持不够,所以我们选择了 Systrace+Perfetto 工具结合,可覆盖所有 Android 系统。随着 Perfetto 持续迭代,增加了对低版本 Android 系统支持,百度 APP 也全面切换到了 Perfetto 为基础采集和分析性能工具。


1.2 二次开发


Trace 采集


Perfetto 收集 App 的 Trace 是通过 Android 系统的 atrace 收集,需要自己手动添加 Trace 收集代码,添加 Trace 采集方式如下:


  • Java/Kotlin:提供了 android.os.Trace 类,通过在方法开始和结束点成对添加 Trace.beginSection 和 Trace.endSection;

  • NDK:通过引入<trace.h>,通过 ATrace_beginSection() / Atrace_endSection()添加 Trace;

  • Android 系统进程:提供了 ATRACE_*宏添加 Trace,定义在 libcutils/trace.h;


在 Android Framework 和虚拟机内部会默认添加一些关键 Trace,APP 层需要手动添加,监控 APP 启动流程,有海量的方法,手动添加耗时耗力。百度 APP 大部分逻辑都是 Java/Kotlin 编写,Java/Kotlin 代码会编译成字节码,在编译期间,可通过 gradle transform 修改字节码,我们需要开发一套自动插桩的 gradle 插件,在编译时自动添加 APP 层 Trace 收集代码,实现监控 APP 层所有方法。


防劣化


随着优化持续上线,对性能指标会有一定的正向收益,但是随着版本持续迭代,会有各种劣化问题,为保住优化成果,我们在线下每个版本发布之前都需要做真机启动性能测试,测试流程:



打包:需要打出自动插桩的包,需要一个基准包(上次发布版本的 release 分支的插桩包)和一个测试包(master 分支的插桩包),用来做真机测试。


真机测试:用基准包和测试包手动跑启动相关 case,启动 Perfetto Trace 抓取脚本,抓取 Trace 日志,会输出基准包 Trace 日志和测试包 Trace 日志,用作对比分析。


对比分析:Trace 日志通过https://ui.perfetto.dev/ 打开可生成的火焰图,通过火焰图进行对比分析,找到存在的劣化问题,这个流程是最耗时的,需要对比分析的调用栈非常繁杂。


分发问题:梳理相关劣化问题,分发跟进对应业务负责同学。


这一整套流程完成,需要 2 人天,而对比分析工作量最大,需要实现自动化分析 Trace 日志功能,自动发现新增耗时、耗时劣化、锁等待等问题。


Perfetto 提供了强大的 Trace 分析模块:Trace Processor,把多种类型的日志文件(Android systrace、Perfetto、linux ftrace)通过解析、提取其中的数据,结构化为 SQLite 数据库,并且提供基于 SQL 查询的 python API,可通过 python 实现自动化分析。


为提高效率,需基于 Trace Processor 的 python API,开发一套 Trace 自动分析工具集,实现快速高效分析版本启动劣化问题。

二、Perfetto 介绍

百度 APP 启动性能优化工具是基于 Perfetto 二次开发,下面对 Perfetto 的架构和原理做相应的介绍。


2.1 整体介绍



△Perfetto 整体介绍


Perfetto 是 Google 开源的一套性能检测和分析框架。按照功能可分成 3 大块,Record traces(采集)、Analyze traces(分析)、Visualize traces(可视化)。


Record traces


Trace 采集能力,支持采集多种类型的数据源,支持内核空间和用户空间数据源。


内核空间数据源是 Perfetto 内置的,需要系统权限,主要的数据源包括:


  • Linux ftrace:支持收集内核事件,如 cpu 调度事件和系统调用等;

  • /proc 和/sys pollers:支持采样进程或者系统维度 cpu 和内存状态;

  • heap profilers:支持采集 java 和 native 内存信息;


用户空间数据采集,Perfetto 提供了统一的 Tracing C++库,支持用户空间数据性能数据收集,也可用 atrace 在用户层添加 Trace 收集代码采集用户空间 Trace。


Analyze traces


Trace 分析能力,提供 Trace Processor 模块可以把支持的 Trace 文件解析成一个内存数据库,数据库实现基于 SQLite,提供 SQL 查询功能,同时提供了 python API,百度 APP 也是基于 Trace Processor 开发了一套 Trace 自动化分析工具集。


Visualize traces


Perfetto 还提供了一个全新的 Trace 可视化工具,工具是一个网站:https://ui.perfetto.dev/ 。在可视化工具中可导入 Trace 文件,并且可使用 Trace Processor 和 SQLite 的查询和分析能力。


2.2 Perfetto 采集


采集指令


./record_android_trace -c atrace.cfg -n -o trace.html
复制代码


**record_android_trace:**Perfetto 提供的 Trace 采集帮助脚本,对低版本 Trace 采集做了兼容,Android 9 以上会通过 adb 调用默认内置 Perfetto 执行文件,Android 9 以下会根据不同的 CPU 架构下载外置的 Perfetto 可执行文件,把可执行文件 push 到 /data/local/tmp/tracebox,最后通过 adb 指令启动 Perfetto Trace 采集,通过这个脚本能够支持所有机型的 Trace 采集。


-c path:指定 trace config 配置文件,配置 Trace 采集时长、buffer_size、buffer policy、data source 配置等;


-o path:指定 Trace 文件输出路径。


Trace config


Trace config 配置当次采集的一些核心配置,采集时长、trace buffer size、buffer policy 和 data source 配置等;


示例:


buffers: {    size_kb: 522240    fill_policy: DISCARD}data_sources: {    config {        name: "linux.ftrace"        ftrace_config {            ftrace_events: "sched/sched_switch"            atrace_categories: "dalvik"            atrace_categories: "view"            atrace_apps: "com.xx.xx"        }    }}duration_ms: 30000
复制代码


‍buffers:设置当次采集的内存 trace buffer 配置,size_kb,配置当次 trace buffer 大小,单位 kb;fill_policy,配置 trace buffer 的策略,RING_BUFFER,trace buffer 满了后,新的内容会把最老的内容覆盖,DISCARD,trace buffer 满了以后,新的 trace 会直接丢弃。


duration_ms:trace 采集时长,单位 ms,到达指定时长后,会停止收集 Trace。


data_sources:name,当前 data source 名称,如 linux.ftrace 表示 ftrace 的配置;ftrace_config,ftrace 的配置;ftrace_events,配置需要抓取的 ftrace 事件,内核空间 trace;atrace_categories,配置需要收集的 atrace category,用户空间 Trace;atrace_apps,配置需要采集 trace 的应用进程包名。


原理简介


启动性能重点关注方法耗时,Perfetto 采集方法耗时 trace 依赖 atrace 和 ftrace 实现。相关实现如下:



△Perfetto 采集 Trace 原理


Perfetto 通过 atrace 设置用户空间 category(数据类型),包括 APP 自定义 Trace 事件、系统 view Trace、系统层 gfx 渲染相关 Trace 等,其最终都是通过调用 Android SDK 提供 Trace.beginSection 或者 ATrace 宏记录到同一个文件 /sys/kernel/debug/tracing/trace_marker 中,ftrace 会记录该写入操作时间戳。其中 Android Framework 里面一些重要的模块都加了 Trace 收集,用户 APP 代码需要手动加入;


内核空间数据主要一些和系统内核相关数据,如 sched(CPU 调度信息)、binder(binder 驱动)、freq(CPU 频率)等信息,Perfetto 通过控制一些文件节点实现打开和关闭;


最终两种类型数据会写入 ftrace RingBuffer 中,Perfetto 通过读取 ftrace RingBuffer 数据,实现 Trace 收集。


ftrace


ftrace 是 trace 采集的核心实现,ftrace 其实也是 Perfetto 的支持的一个 data source,通过 ftrace 可实现收集用户空间和系统空间 trace 数据。


ftrace 是 linux 系统内核的 trace 工具,其中 RingBuffer 是 ftrace 的基础,所有的 trace 原始数据都是通过 RingBuffer 记录的;


ftrace 使用 tracefs file system 用来控制 ftrace 的配置和 Trace 日志输出,ftrace 目录:/sys/kernel/debug/tracing(内核 4.1 之前) 或者 /sys/kernel/tracing(内核 4.1 之后)。


部分文件说明:



ftrace 如何通过在相应的文件节点写入信息和读取,实现 ftrace 的配置和 Trace 日志的输出?


ftrace 使用了 tracefs 文件系统注册 file_operations 结构体,对文件进行系统调用会关联对应的函数指针,实现 ftrace 配置和 ftrace Trace 日志读取功能,相关代码实现:


// 创建文件,关联file_operationsstruct dentry *trace_create_file(const char *name,                 umode_t mode,                 struct dentry *parent,                 void *data,                 const struct file_operations *fops){    struct dentry *ret;
ret = tracefs_create_file(name, mode, parent, data, fops); if (!ret) pr_warn("Could not create tracefs '%s' entry\n", name);
return ret;}
// 定义操作trace文件系统调用对应的函数指针static const struct file_operations tracing_fops = { .open = tracing_open, .read = seq_read, .write = tracing_write_stub, .llseek = tracing_lseek, .release = tracing_release,};
trace_create_file("trace", TRACE_MODE_WRITE, d_tracer, tr, &tracing_fops);
复制代码


Perfetto 采集 ftrace 数据


下面介绍一下完整采集流程:


  • 通过 adb 的方式启动执行 perfetto,指定 Trace config,配置 buffer_size、buffer policy、data source ftrace 配置;

  • Perfetto 读取 Trace config 配置,写入 ftrace 文件节点,配置收集的数据类型和设置 ftrace 每个 cpu 的 ringbuffer size,并且定期读取 per_cpu/cpu0/trace_pipe_raw 内容,即定期读取每个 cpu 的 ringbuffer 数据,解析转换成对应的 probuf 格式,写入 Producer 和 tracing service 的共享内存中,tracing service 会把共享内存的 trace 数据拷贝到 trace buffer。

  • 采集结束,停止 trace 收集,把 tracing service 的 trace buffer 数据读取出来,生成文件,通过 Perfetto web ui 查看。


相关数据流如下图:



△Perfetto 采集 ftrace 数据流


2.3 Perfetto 分析


Perfetto 分析模块,其核心是 Trace Processor,其功能如下:



△Trace Processor


解析 Trace 文件、提取其中的数据,结构化为 SQLite 的内存数据库,并且提供基于 SQL 查询的 API,通过写 SQL 的方式,查询对应的方法耗时,同时提供 Python API。


支持的 trace 数据格式:


  • Perfetto native protobuf format

  • Linux ftrace

  • Android systrace

  • Chrome JSON (including JSON embedding Android systrace text)

  • Fuchsia binary format

  • Ninja logs (the build system)

三、自动插桩工具

自动插桩工具是一个 gradle 编译插件,全方法 Trace 插桩,保证 Trace 闭合,支持监控系统类,同时需要考虑包体积和性能问题。


3.1 自动插桩


Android 系统会内置一些 Trace,在 APP 代码需要手动添加,耗时耗力,需要实现一个自动插桩工具,自动在 APP 的方法添加 Trace 代码。


插桩代码:


class Test {   public void test() {       Trace.benginSection("test");       // 方法体       // ...       Trace.endSection();   }}
复制代码


自动插桩工具是利用 Gradle Transform(Gradle Transform 是 Android 官方提供给开发者在项目构建阶段中由 class 到 dex 转换之前修改 class 文件的一套 api),开发的一个 Gradle 编译插件。利用 ASM 字节码操作框架,遍历所有的类的方法,在方法开始和结束点插入收集 Trace 的代码,实现 APP 全方法监控。


3.2 Did Not Finish 问题


自动插桩工具投入使用后,遇到了 Did Not Finish 的问题,如果出现这种问题,整个 Trace 都错乱了,如下图所示:



Did Not Finish,表示方法没有结束,经过定位,是因为 Trace.benginSection 和 Trace.endSection 没有成对调用。为什么会出现这种问题呢?


示例问题代码:


class Test {    public void test() throws Exception {         Trace.benginSection("test");         // 方法体,代码出现异常,外部调用方法catch住         testThrowException();// 这个方法抛出异常,代码返回,endSection不会调用         // endSection可能存在不调用的情况         Trace.endSection();        }}
复制代码


运行期间,方法可能存在主动抛出异常和运行时异常的情况,如存在这种情况,Trace.endSection 就得不到调用,就会存在问题。


如何保证 Trace.benginSection 和 Trace.endSection 的成对调用?


理想的解决方案是使用 try-finally 块整体包裹整个方法体,在方法开始点插入 Trace.benginSection 在 finally 块插入 Trace.endSection,Java 虚拟机会保证 finally 块的代码在 try 块代码结束前都会调用,可以保证 Trace.benginSection 和 Trace.endSection 的成对调用。


示例代码:


class Test {   public void testMethod(boolean a, boolean b) {     try {            Trace.beginSection("com.sample.systrace.TestNewClass.testMethod.()V");            if (!a) {                throw new RuntimeException("test throw");            }            Log.e("testa", "com.sample.systrace.TestNewClass.testMethod.()V");            if (b) {                return;            }            Log.e("testb", "com.sample.systrace.TestNewClass.testMethod.()V");        } finally {            Trace.endSection();        }    }}
复制代码


在字节码层面是没有 finally 关键字对应的字节码指令,为了搞明白 finally 的具体实现逻辑,对编译的字节码反编译:


public void testMethod(boolean, boolean);descriptor: (ZZ)Vflags: ACC_PUBLICCode:  stack=3, locals=4, args_size=3     0: ldc           #15                 // String com.sample.systrace.TestNewClass.testMethod.(ZZ)V     2: invokestatic  #21                 // Method android/os/Trace.beginSection:(Ljava/lang/String;)V     5: iload_1     6: ifne          19     9: new           #23                 // class java/lang/RuntimeException    12: dup    13: ldc           #25                 // String test throw    15: invokespecial #27                 // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V    18: athrow    // 手动抛出异常,没有添加finally块的字节码指令    19: ldc           #29                 // String testa    21: ldc           #31                 // String com.sample.systrace.TestNewClass.testMethod.()V    23: invokestatic  #37                 // Method android/util/Log.e:(Ljava/lang/String;Ljava/lang/String;)I    26: pop    27: iload_2    28: ifeq          35    31: invokestatic  #40                 // Method android/os/Trace.endSection:()V    34: return    // if(b)如果b为true的一个return指令,上一个指令添加了invokestatic,即增加了Trace.endSection调用    35: ldc           #42                 // String testb    37: ldc           #31                 // String com.sample.systrace.TestNewClass.testMethod.()V    39: invokestatic  #37                 // Method android/util/Log.e:(Ljava/lang/String;Ljava/lang/String;)I    42: pop    43: invokestatic  #40                 // Method android/os/Trace.endSection:()V    46: return    // 代码正常结束点,也插入了invokestatic,即增加了Trace.endSection调用    47: astore_3  // 开始异常处理,抛出异常之前也插入了invokestatic,即增加了Trace.endSection调用    48: invokestatic  #40                 // Method android/os/Trace.endSection:()V    51: aload_3    52: athrow     Exception table:    // 异常表,只要行号,from-to之间字节码指令发生异常,则跳转到target行进行处理     from    to  target type          0    46    47   Class java/lang/Throwable // 处理的异常类型  LocalVariableTable:    Start  Length  Slot  Name   Signature        5      42     0  this   Lcom/sample/systrace/TestNewClass;        5      42     1     a   Z        5      42     2     b
复制代码


  1. 其实本质就是一个 try-catch 块,catch 块捕获的异常类型为 Throwable;

  2. 在正常结束点(各类 return 指令)前,把 finally 块的指令冗余的添加到各类 return 指令之前,保证正常退出;

  3. 异常结束点处理,主动抛出异常或者运行时异常,都统一由 catch 块处理,会在抛出异常之前插入 finally 块的指令。


对应的 Java 代码实现:


classTest {    public void testMethod(boolean a, boolean b) {    try {      Trace.beginSection("com.sample.systrace.TestNewClass.testMethod.()V");      if (!a) {          throw new RuntimeException("test throw");      }      Log.e("testa", "com.sample.systrace.TestNewClass.testMethod.()V");      if (b) {          Trace.endSection();          return;      }      Log.e("testb", "com.sample.systrace.TestNewClass.testMethod.()V");      Trace.endSection();    } catch(throwable e) {      Trace.endSection();      throw e;    }}
复制代码


综上,为了保证 Trace.beginSection 和 Trace.endSection 成对调用,参考了虚拟机实现 try-finally,完美的插桩方案如下:


  1. 方法开始点只有一个,在方法开始点添加 Trace.beginSection 即可;

  2. 方法结束点会有多个,结束点存在两种情况,正常结束和异常结束,针对正常结束点(各类 return 指令)前添加 Trace.endSection;

  3. 异常结束(主动抛出异常或者运行时异常),则用 try-catch 住整个方法体,catch 异常类型为 Throwable,在 catch 块中添加 Trace.endSection,并且抛出捕获的异常。


3.3 监控系统类方法


自动插桩方案,只能对 APP 的代码编译的字节码进行插桩,由于 Android 系统和 Java 提供的系统类的字节码不参与打包,不能进行插桩,但还是想监控系统相关的类的一些不合理的调用。比如在主线程调用 Object.wait,强制主线程进行等待,放弃 CPU 的使用权,线程进入 sleep 状态,等待其他线程 notify 或者 wait 的超时,可能会导致严重的性能问题。


为了监控此系统类问题,需要把调用系统 Object.wait 的代码,前后进行插桩,如下所示:


boolean isMain = Looper.getMainLooper() == Looper.myLooper();  try {      if (isMain) {          Trace.beginSection("Main Thread Wait");      }      lock.wait(timeout, nanos);  } finally {      if (isMain) {          Trace.endSection();      }  }
复制代码


直接在每个方法里调用了 Object.wait 的方法调用处进行以上的插桩逻辑,插桩实现异常复杂,容易出错,而且这种实现会在每个 Object.wait 调用处进行相同逻辑的插桩,会增加指令数量,导致包体积增加。


为了实现 Object.wait 方法监控,同时减少插桩复杂读,最终决定采用字节码指令替换的方案,即在字节码层面把调用 Object.wait 方法指令,替换成自定义的 wait 方法,功能和系统的 wait 一样,只是添加了自定义的 Trace。


Object 类定义的 wait 方法有三个:


public final native void wait(long timeout, int nanos) throws InterruptedException; public final void wait(long timeout) throws InterruptedException {   wait(timeout, 0);}
public final void wait() throws InterruptedException { wait(0);}
复制代码


重写后的自定义的增加监控的 wait 方法,增加 Trace 监控代码,最终还是调用系统的 Object.wait 方法:


public static void wait(Object lock, long timeout, int nanos) throws InterruptedException {    // 监控主线程wait    boolean isMain = Looper.getMainLooper() == Looper.myLooper();    try {        if (isMain) {            Trace.beginSection("Main Thread Wait");        }        lock.wait(timeout, nanos);    } finally {        if (isMain) {            Trace.endSection();        }    }}
public static void wait(Object lock) throws InterruptedException { wait(lock, 0L, 0);}
public static void wait(Object lock, long timeout) throws InterruptedException { wait(lock, timeout, 0);}
复制代码


在字节码里调用类方法指令有:INVOKEVIRTUAL(调用类实例方法)、INVOKESTATIC(调用静态方法)、INVOKESPECIAL(调用构造函数),这里我们主要关注下 INVOKEVIRTUAL 和 INVOKESTATIC。


方法调用主要有两步:


  1. 参数加载,按照参数顺序从左到右加载方法指令的依赖的参数到操作数栈;

  2. 方法调用,执行 INVOKEVIRTUAL 或者 INVOKESTATIC,指定类名、方法名、方法签名,调用方法。


其中 INVOKEVIRTUAL 是类实例方法调用,需要依赖对象引用,最先入操作数栈的是类对象引用,然后才是方法参数。


调用 Object.wait(long timeout, int nanos)的字节码指令:


ALOAD 4 # 加载对象引用LLOAD 1 # 加载long timeoutILOAD 3 # 加载int nanosINVOKEVIRTUAL java/lang/Object.wait (JI)V # 调用Object实例方法
复制代码


重写的 wait 方法是静态方法,有个细节,第一个入参必须是一个 Object 对象,不能换位置,对应字节码:


ALOAD 4 # 加载对象引用LLOAD 1 # 加载long timeoutILOAD 3 # 加载int nanosINVOKESTATIC com/baidu/systrace/SystraceInject.wait (Ljava/lang/Object;JI)V # 调用SystraceInject.wait的静态方法
复制代码


从上面的字节码分析,自定义方法 SystraceInject.wait 参数和系统方法 Object.wait 参数顺序保持一致,保证操作数栈入栈顺序一致,参数加载流程一致,所以,我们只需要替换方法调用指令即可实现替换,遍历 APP 所有方法的字节码指令,替换方法目标 wait 方法调用的指令,INVOKEVIRTUAL java/lang/Object.wait (JI)V 替换为 INVOKESTATIC com/baidu/systrace/SystraceInject (Ljava/lang/Object;JI)V,即可实现监控主线程 wait 问题。


同理,其他需要动态替换的系统类也可用相同的方式进行替换,也可实现对系统方法调用的监控。


3.4 包尺寸和性能问题


自动插桩工具会对百度 APP 所有方法进行插桩,会导致包尺寸增加 10M 左右大小,为了减少包尺寸,需要对插桩的方法进行一些过滤,如一些确定不耗时的方法,比如简单的 get、set 方法、空方法。


在分析的过程中,还发现一些插桩导致的性能问题,如下图所示:



EventBus 组件使用 rxjava 实现,调用层级非常深,在分析的过程中会认为 EventBus 组件非常耗时,但是经过优化 EventBus 组件,自定义实现了一套高性能的 EventBus 组件,通过 AB 实验查看整个启动流程只快了 50ms,收益没有预期的大。


通过源码分析,收集 App 的 trace,java/kotlin 使用 android.os.Trace,把 trace 信息最终会写入/sys/kernel/tracing/trace_marker 中,写入 ftrace RingBuffer。这种方式有一定的性能损耗,这是因为每个事件都涉及到一个字符串化、一个 JNI 调用,以及一个用户空间<->内核空间的写入 trace_marker 的系统调用(最耗时的部分)。


为解决此类问题,在自动插桩工具增加黑名单机制,可通过配置文件,配置类名或者包名,指定类或者包下的所有类不进行插桩,达到减少性能损耗和包体积的效果。

四、Trace 自动分析工具

Trace 自动分析工具主要是为了提升分析效率,基于基准版本自动化分析耗时劣化和锁问题。工具基于 Trace Processor 提供的 Python API,可自己写 SQL 脚本查询内存数据库表中的 Trace 数据。


百度 APP 基于 Trace Processor 开发了一系列的自动分析工具集:


  • 分析大于指定耗时阈值的方法列表;

  • 对比分析版本耗时劣化、新增耗时问题;

  • 支持统计 TOP N 异步线程 CPU 耗时;

  • 支持分析主线程锁问题(monitor contention 前缀)。


4.1 核心表


自动化分析是基于内存数据库表,其使用的核心表如下:



△自动分析使用的表


process:进程信息表,通过进程名,可拿到内存表中进程唯一 upid;


thread:线程信息表,通过 upid 可以查询到进程下的所有线程,同时线程唯一表示使用 utid 表示;


thread_track:线程上下文,和 utid 绑定,可以通过 track_id 关联 slice 表,表示指定线程下的时间片事件;


sched_slice:cpu 调度线程表,一条记录表示 cpu 调度一个线程的时间片,可用于计算线程被 cpu 调度时长,表结构:



slice:线程时间片表,和线程关联,关联一个 track_id,记录用户空间的线程时间片事件,可用统计方法耗时,表结构:



4.2 方法耗时统计


分析性能问题,最重要的是统计方法耗时,自动化分析工具统计方法耗时有两种口径:


Wall Duration:方法整体耗时,包含等待 CPU 调度(sleep、等待 IO、时间片耗尽)和 CPU 执行方法指令耗时,统计方法实际运行时长;


CPU Duration:CPU 执行方法指令耗时,不包含等待调度的时间,统计方法自身指令执行的真实耗时;


**Wall Duration = CPU Duration + 等待调度时长,**通过分析方法的 Wall Duration 和 CPU Duration 可以分析出方法耗时是因为方法自身逻辑耗时,还是因为执行过程中存在锁、IO 或者线程抢占的问题。


Wall Duration 统计


Wall Duration 是根据 slice 表中的 dur 字段统计方法整体耗时。


CPU Duration 统计


CPU Duration 统计需要结合 slice 表和 sched_slice 表动态计算,CPU 调度的最小单位是线程,方法运行在线程,所以计算方法 CPU 耗时的思路就是统计在方法运行这段时间,所有 CPU 调度方法所在线程的累积时长,即为方法的 CPU 执行耗时。slice 表,统计了方法开始时间戳、时长和 track_id(可通过 thread_track 表找到对应的线程 Id),可确定线程 Id、开始和结束时间戳;sched_slice 表包含了 CPU 调度线程信息,包括调度的 CPU 编号、线程 id、时长和开始时间戳,通过线程 Id、开始和结束时间戳,可以把这段时间内调度指定线程 Id 的记录,累加即可,需要注意处理一些边际条件。如下图所示,CPU duration 需要把 sched_slice1 和 sched_slice2 累加。



4.3 问题分析


百度 APP 目前自动化 trace 分析主要分析主线程耗时劣化,分析方法是基于一个基准版本(如线上版本 release 分支包)做为参照,与测试版本的每个主线程调用进行对比分析。自动分析支持分析以下几类问题:


主线程锁


主要分析 synchronize 关键字导致的锁问题,虚拟机会通过 atrace 添加 Trace 信息,Trace 信息有固定前缀 monitor contention,并且会说明占用锁的线程 ID,直接分析 slice 表 name 字段前缀为 monitor contention。


方法耗时劣化


此类问题关注的是主线程的方法耗时劣化,通过对比基准版本和测试版本,耗时劣化是指测试的版本对比基准版本耗时有增加,到了一定阈值(当前阈值 10ms),会认为是耗时劣化问题。


方法 CPU 耗时劣化


此类问题劣化问题和方法耗时劣化类似,统计的是方法的 CPU 耗时。


新增方法耗时


此类问题关注的是主线程的新增方法耗时,测试版本新增方法的耗时到达一定阈值(目前是 5ms),会认为是新增耗时问题。

五、最佳实践

百度 APP 基于自动插桩工具和 Trace 自动化分析工具,构建了一套线下防劣化监控流水线,流程如下:



其中的打包流程使用的是自动插桩工具,Trace 自动分析用的是 Trace 自动分析工具。流水线自动打包,自动启动测试抓取 trace,自动化分析和根据堆栈自动分发问题,无需人工介入,只需投入很少人力处理一些需要豁免的问题(方法改名、系统锁、线程调度问题等),对比之前单次性能人工测试和人工分析需要 2 人天,极大提升了效率。


性能测试报告:



报告中的指标计算和问题分析都是有 Trace 自动化分析工具产出,同时问题详情会有详细的劣化数据和堆栈,能快速定位劣化问题。

六、小结

百度 APP 启动性能工具基于 perfetto 结合自动插桩和自动化分析能力,支持采集 APP 全 Java/kotlin 方法 Trace 日志,同步支持自动化分析劣化问题,能极大提升效率。由于是全 Java/kotlin 方法插桩还存在影响包体积问题,同时采集 trace 也存在一定性能损耗,后续还需要持续优化(继续减少不必要插桩、控制采集层级、接入 Perfetto SDK 采集等)。


——END——


参考资料:


[1] Perfetto 官方文档:


https://perfetto.dev/docs/


[2] Perfetto 源码:


https://github.com/google/perfetto


[3] Ftrace 原理解析:


https://blog.csdn.net/u012489236/article/details/119494200


[4] Ftrace 官方文档:


https://www.kernel.org/doc/html/latest/trace/ftrace.html


[5] Linux 内核源码:


https://elixir.bootlin.com/linux/latest/source/kernel/trace/trace.c#L8783


推荐阅读:


数字人技术在直播场景下的应用


百度工程师教你玩转设计模式(工厂模式)


超大模型工程化实践打磨,百度智能云发布云原生 AI 2.0 方案


前后端数据接口协作提效实践


前端的状态管理与时间旅行:San实践篇


百度App 低端机优化-启动性能优化(概述篇)


面向大规模数据的云端管理,百度沧海存储产品解析


增强分析在百度统计的实践

用户头像

百度Geek说

关注

百度官方技术账号 2021.01.22 加入

关注我们,带你了解更多百度技术干货。

评论

发布
暂无评论
百度App Android启动性能优化-工具篇_android_百度Geek说_InfoQ写作社区