乐趣区

关于android:百度App-Android启动性能优化工具篇

一、前言

启动性能是 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 是 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 通过 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_operations
struct 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)V
flags: ACC_PUBLIC
Code:
  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 timeout
ILOAD 3 # 加载 int nanos
INVOKEVIRTUAL java/lang/Object.wait (JI)V # 调用 Object 实例办法 

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

ALOAD 4 # 加载对象援用
LLOAD 1 # 加载 long timeout
ILOAD 3 # 加载 int nanos
INVOKESTATIC 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/per…

[3] Ftrace 原理解析:

https://blog.csdn.net/u012489…

[4] Ftrace 官网文档:

https://www.kernel.org/doc/ht…

[5] Linux 内核源码:

https://elixir.bootlin.com/li…

举荐浏览:

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

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

超大模型工程化实际打磨,百度智能云公布云原生 AI 2.0 计划

前后端数据接口合作提效实际

前端的状态治理与工夫旅行:San 实际篇

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

面向大规模数据的云端治理,百度桑田存储产品解析

加强剖析在百度统计的实际

退出移动版