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

发表于 1年以前  | 总阅读数:1405 次

一、前言

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

1.1 工具选型

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

  • 高性能,保证自身高性能,以防带偏优化方向
  • 多维度,能监控多维度信息,帮助全面发现问题
  • 易用性,方便的可视化界面,方便分析

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

性能损耗 监控维度 易用性 缺陷
TraceView 性能损耗大,支持Trace采集模式,MethodTracing和Sampling,MethodTracing模式需要禁掉JIT,记录所有方法调用,性能损耗大;Sampling模式通过堆栈采样,需要挂起线程,也存在性能能问题,而且耗时计算不够精准,和采样频率相关 支持采集所有Java/kotlin方法耗时 火焰图分析 性能差
CPU Profiler 性能损耗较大,TraceView的替代工具,同样支持MethodTracing和Sampling模式,存在类似的性能损耗大的问题 支持采集所有Java/kotlin方法耗时; 支持采集C/C++方法的耗时; 火焰图分析 性能差
Systrace 性能损耗小,Android 4.0默认集成性能工具,基于Android系统层的atrace实现,atrace能力最终是通过Linux内核的性能工具ftrace实现;采样监控,Android系统在Framework层关键点加了Trace收集代码,APP需手动添加Trace收集;内核支持和部分方法采样收集,性能损耗低 支持采集Framework关键点耗时; APP耗时需手动添加; 支持采集锁等待、binder调用和IO等信息; 支持ftrace数据源,包括CPU调度信息和系统调用等内核相关信息; 火焰图分析 APP耗时监控需要手动添加采样代码,耗时耗力
Perfetto 性能损耗小,Android 9.0全新的性能工具套件,Systrace的升级版,实现类似,同样性能损耗较低 支持Systrace所有监控维度; 支持内存和电量信息监控; 火焰图分析 支持写SQL分析 同Systrace

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之后)。

部分文件说明:

文件 描述
tracing_on 设置是否写入trace到ringbuffer,写入0表示disable,写入1表示enable,注意:这个disable仅仅是不写入ringbuffer。
events/* events目录包含很多ftrace支持采集的内核数据类型,也同通过写入文件节点启停事件收集,如开启cpu调度trace收集,可通过写入events/sched/enable文件节点,写入0表示disable,写入1表示enable
buffer_size_kb 设置每个cpu的ringbuffer的size,单位是KB
trace 这个文件输出可读格式的Trace文件,所有的Ftrace ringbuffer数据,不是一个消费者,读完ringbuffer数据,不会清空ringbuffer
trace_pipe 和trace文件一样,但是是个消费者,读完数据会清空读取的ringbuffer
trace_marker 将用户空间与内核中发生的事件同步,往这个文件写入字符串,会写入ftrace buffer
per_cpu 目录里包含每个cpu的trace信息
per_cpu/cpu0/buffer_size_kb 设置当前cpu的ringbuffer的size,单位KB
per_cpu/cpu0/trace 和根目录trace类似,但是这个trace只是当前cpu的ringbuffer数据
per_cpu/cpu0/trace_pipe 和根目录trace_pip类似,但是这个trace只是当前cpu的ringbuffer数据
per_cpu/cpu0/trace_pipe_raw 和per_cpu/cpu0/trace_pipe类似,只是数据格式是二进制

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调度时长,表结构:

列名 类型 描述
id SchedSliceTable::Id id
type string 事件类型
ts int64_t 开始调度时间戳,单位:nanoseconds
dur int64_t cpu调度时长,单位:nanoseconds
utid uint32_t 线程唯一表示utid
cpu uint32_t 调度的cpu编号
end_state string 线程结束状态
priority int32_t 调度优先级

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

列名 类型 描述
id SchedSliceTable::Id id
type string 事件类型
ts int64_t 开始调度时间戳,单位:nanoseconds
dur int64_t cpu调度时长,单位:nanoseconds
arg_set_id uint32_t
track_id TrackTable::Id thread_track表id,表示关联的线程
category string slice的类别
name string Trace#beginSection(name),写入的字符串,可以是方法签名
depth uint32_t 表示当前调用的深度
stack_id int64_t
parent_stack_id int64_t
parent_id SchedSliceTable::Id 上层的slice的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采集等)。

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/UCQzt3weg7RYwnn0pZ2WfA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 相关文章
简化Android的UI开发 4年以前  |  520698次阅读
Android 深色模式适配原理分析 3年以前  |  28624次阅读
Android阴影实现的几种方案 1年以前  |  10782次阅读
Android 样式系统 | 主题背景覆盖 3年以前  |  9584次阅读
 目录