西瓜视频稳定性治理体系系列文章
背景
卡顿和 ANR 问题一直是 Android 性能优化的重点问题,直接关系到用户体验。当主线程的消息执行耗时过长时,轻则出现不流畅,不跟手,重则有肉眼可见的卡顿感,最严重则是发生 ANR,系统会弹出弹窗提示用户等待或关掉程序,严重影响用户体验。
对于卡顿的监控,现有的方案大多是在消息执行前埋点,当消息耗时过长时进行抓栈操作,ANR 的监控则是通过监听 SIGQUIT 信号并判断进程状态,在 ANR 时拿到各线程堆栈及各类辅助信息来定位问题。这种抓堆栈的方案对于单点长耗时是有效果的,比如锁等待和死锁,但是对于由多个消息和函数耗时累积造成的 ANR,一次堆栈是无法定位问题的,所以经常会出现 ANR 堆栈不准的情况。
西瓜视频的 Android TOP 1 ANR 就是聚类到了 nativePollOnce,根据 ANR 前的消息调度耗时可以看到存在许多长耗时消息,这些长消息是造成 ANR 的根本原因,而不是真的阻塞在了 nativePollOnce。分析发现其实这部分问题都是由于抓栈时机滞后导致的,从感知到 ANR 到完成抓栈存在一定的时间间隔,很容易错过现场,而 nativePollOnce 是主线程中执行频率最高的函数,抓栈很容易落到这里,所以导致很多 ANR 都是 nativePollOnce 的堆栈。另外传统的监控方案对于多个耗时消息累积导致的 ANR 也很难做到有效监控,单次抓栈大概率无法明确问题。所以如果能拿到 ANR 前的一段时间的 Method Trace,那么就能看到主线程到底在做什么,进而准确定位问题。
现状
Android 可用的 trace 工具有很多,如 Android Studio 的 Profiler(与代码内的 Debug.startMethodTracing 效果类似),Uber 开源的 Nanoscope,Facebook 开源的 Profilo,Systrace 也可以统计一些关键节点的耗时。按照各个工具抓取函数耗时的实现原理,可以分为两类:埋点型和采样型。
埋点型
Systrace、Debug.startMethodTracing 和 Nanoscope 都属于埋点型,埋点型就是在函数执行的出入口记录时间和方法名,这样就能获取到程序运行时都执行了哪些方法和对应的耗时。
Systrace 是分析系统性能的工具,包含了 CPU、IO、内核运行信息等。Systrace 通过一些关键路径上的埋点,抓取的时候拿到这些关键路径的耗时,将数据结合起来对性能做综合分析。Systrace 抓取 App 函数耗时需要额外进行埋点适配(Trace.traceBegin 与 Trace.traceEnd),且 Framework 的绝大部分函数无法抓取。
Debug.startMethodTracing 是在 ArtMethod 的解释器出入口做了埋点,在虚拟机内设置了一个 instrumentation 监听函数的进入与退出,当函数是解释执行时,就可以获取到函数执行耗时。但此方案仅限于解释执行,所以为了抓取到完整的 trace,开启该功能时,虚拟机会将目标应用设置成仅解释执行,禁用掉机器码执行和 JIT,这样会严重影响运行性能,得到的数据也会失真。
Nanoscope 不同于其他两类方案,它不仅在解释器中埋点,还对编译器做了修改,当函数被编译成机器码时,会在方法的出入口增加机器码逻辑,用来记录编译后的方法的执行。这样做的好处就是,无需限制程序为解释执行,性能得到了保障,但由于修改了编译器代码,所以需要刷定制后的 ROM 才能使用 Nanoscope,失去了通用性。
除了这三种方案外,直接通过字节码插桩对应用内方法加埋点也可以达到目的,但是插桩会导致性能损耗、占用内存以及包大小增加的问题,且无法支持 Framework 堆栈,这里不再详细讨论。
采样型
Debug.startMethodTracingSampling 和 Profilo 属于采样型,其原理是每隔一定的时间,抓取一次线程的堆栈,通过在程序运行时不断的抓取,最终组合形成 trace 数据。
Debug.startMethodTracingSampling 是通过创建一个独立线程,在循环内对所有线程进行抓栈并记录数据。该方案每次抓栈前都会 SuspendAll,等待所有线程抓栈完毕再 ResumeAll,并且在线程挂起时间内,还要进行堆栈的处理,以及文件的写入,导致每次采样所有线程都需要等待很长时间,性能损耗较大。
Profilo 的方案较为新颖,创建定时器不断的向目标线程发送 SIGPROF 信号,在注册的信号回调内执行抓栈操作。这样做的好处是性能损耗较低,可以直接在当前线程抓栈,损耗近似为抓栈耗时,缺点是方案复杂适配成本高,Profilo 的抓栈方案使用了大量的固定偏移,兼容性稳定性以及后续扩展性较差。
采样抓栈的限制也比较明显,采样间隔过大,trace 的精度会变低,而采样间隔过小,性能损耗就会增大。优点是可控性强,大部分情况下短耗时函数我们并不关心,而合适的采样间隔可以保证只监控长耗时函数。
方案 | 原理 | 场景 | 性能 | 主要缺点 |
---|---|---|---|---|
Systrace | 埋点型 | 线下 | 中 | 无法抓取应用函数栈 |
Debug.startMethodTracing | 埋点型 | 线下 | 低 | 强制解释执行,性能差 |
Nanoscope | 埋点型 | 线下 | 高 | 只能在定 |