得物之前对于线上的ANR问题都是采用接入的bugly平台进行问题收集和平台展示的,bugly提供的ANR相关信息对于 实际解决ANR问题 信息度不够、并且对于ANR日志的聚合实现效果并不好。因此得物在参考业界各大厂商分享的ANR监控及治理方案后,我们开始着手建设自己内部的ANR监控平台,本文主要介绍 ANR监控 平台部分的实现,Android客户端测相关ANR上下文信息的采集实现不做具体阐述。
ANR 信息的处理
后端在收集到客户端上报的ANR信息后,需要对ANR数据进行数据分析、日志聚合。首先整理下目前端上报的ANR信息内容,主要包括以下几点信息
- ProcessErrorStateInfo
- tomstone信息
- 以采样方式 收集的主线程函数调用信息
- 主线程 Looper处理的 Message信息,包括 历史调度的Message信息、当前处理的Message、未处理的Message队列
- APP状态信息
- 应用前后台状态
- 应用运行时长
- 等等
以下针对上述几个信息的处理,阐述需要解析的内容。
ProcessErrorStateInfo解析
ProcessErrorStateInfo 是通过ActivityManager的getProcessesInErrorState()函数获取的,主要包含以下信息
- processName
- pid
- uid
- tag (如果ANR发生在 Activity、Service等系统组件上,则tag会被设置为对应的组件名)
- shortMsg (ANR 信息的简短描述)
- longMsg (ANR 的详细信息)
这里主要关注的时longMsg的信息, 通过正则匹配的方式,从 longMsg中可以解析出以下关键信息
- ANR 发生的组件
- ANR 触发的原因
tomstone 信息解析
关于 Android tombstone文件包含的信息可以参考 debuggerd中 tombstone的相关代码 ,得物目前在线上采用的是iQiyi开源的xcrash进行ANR tomsbtone信息的采集。 采集的tombstone文件中主要包含以下信息
- Crash相关元信息
- Crash类型 (anr、java 、native)
- 应用启动的时间
- Crash 的时间
- 应用版本号
- 系统版本
- ABI
- pid、tid、crash 线程名
- 虚拟机GC相关信息
- ANR时各线程的现场信息
- 线程当前调用栈
- 线程优先级 prio
- 线程状态
- 持有、或者等待的锁信息
- 线程 nice值、utm、stm等信息
- logcat信息
- open files
- memory info
- …
对xcrash的 tombstone信息的收集,直接采用的是 xcrash中的代码。
Looper Message回溯信息的解析
在Looper回溯的数据中包含,在处理状态上包含三类Message,1. 已处理的Message 2.当前正在处理的Message 3.未处理的Message;
对于已处理的Message,采集的Message执行的耗时时间(wallTime),以及cpu执行的时间(cpuTime),根据这两个时间的可以大致判断,主线程cpu的调度情况,如果两者之间的差值较大 (cpuTime 只占 wallTime的30%以下) ,考虑可能存在比较严重的线程调度抢占问题。
堆栈采样信息(method trace)解析
得物会在APP运行时,每隔一定的时间间隔(50ms)采集主线程当前的执行堆栈,总计会保存10S~20S 的线程调用栈,当发生ANR时,采集的堆栈信息会随ANR信息一起上报。
通过在后头对该采集的堆栈进行处理,可以预先分析出较为耗时的函数,这里简单描述下解析的方式
- 首先将采集的堆栈遍历解析,并按照函数的调用关系转成NodeTree的形式
- 从根节点开始,层级遍历 MethodNodeTree,找出所有耗时大于一定阈值的函数
- 过滤掉一些白名单函数,比如 com.android.internal.os.ZygoteInit.main 、android.app.ActivityThread.main
部分代码
public class MethodNode {
private List<MethodNode> children = new ArrayList<>();
//method cost time
private int cost;
private String fullMethodName;
private JavaStackFrameSnapshot javaStackFrameSnapshot;
}
ANR 日志的聚合
对端上上报的 ANR 日志进行聚合可以更好的对ANR问题进行分类,评估不同问题在线上发生ANR的严重程度,集中治理。
聚合策略
在线上ANR发生的原因多种多样,简单归类的话可以分为以下几种情况
- 主线程慢函数执行导致
- 比如主线程执行繁重的代码,比如对大数据量的排序
- 主线程上执行 文件IO操作
- 锁等待、死锁
- 系统负载较高、当前进程的CPU调度受到影响
- …其他异常情况
根据以上的分析,针对可能因为慢函数导致的ANR问题,通过上节的 “堆栈信息解析”环节已经解析出所有耗时大于一定阈值的应用内函数,在符合一定条件的情况下(主线程cpu调度正常) 可以按照 耗时函数进行聚合。
对于不存在明显耗时的函数,可以先简单的按照 发生ANR的组件+ANR触发的原因 两个关键信息进行聚合,先按照这种比较粗的粒度进行聚合,再进行分析,如果发现一定的规律后,可以在平台支持一些 特定规则的自定义聚合能力进行数据聚合。
平台体验
这个小节讲的是,在平台展示上,如何展现现有的这些数据、信息,更好的发现定位问题。对于每一个ANR日志来说,除了一些通用的埋点信息外(如userId、sessionId、deviceOs等),更重要的是对ANR相关上下文信息的展示。
问题列表
ANR日志经过聚合后,分成一个个ANR ISSUE,每个ISSUE都有单独的处理状态及趋势 ,以便进行问题治理及问题趋势的跟进
在应用整体上,也有相应维度的趋势图,可以观测目前线上ANR的情况。
问题详情
从问题列表中,点击每个问题时,开始进入问题分析处理的步骤,在聚合算法实现较为完备的情况下(在实际场景中,以ANR目前原因的多样性,较难达到),每个问题详情都是一个特定原因导致的ANR、或者是发生在某一个特定的应用组件上的ANR。
在问题详情页,同样可以展示该问题的发生趋势,以及问题处理状态等,但是更为重要的是 平台提供哪些关键元素信息来帮助开发者定位ANR问题。
日志详情
每个ANR ISSUE 都是很多 ANR Log的集合,即ISSUE 和 Log,是一个一对多的关系,在问题详情页,可以查看该问题下面上报的所有异常case(即日志),点击每个异常case时,可以查看该日志的详细信息。在日志详情部分,ANR平台将上个小节 解析的所以信息,包括主线程 Message回溯信息、method trace信息,各进程CPU使用率等,以更友好的可视化方式进行展示。
ANR 基础信息的展示
- ANR 触发的组件
- ANR触发的原因
- ANR 发生时的应用的前后台状态
- 应用运行时长
- …
ANR后一段时间各进程CPU的使用率信息
ANR 时各线程现场信息展示
Looper消息回溯信息的展示
Looper消息的回溯是可以从主线程处理Message的视角看消息的分发、处理情况。ANR发生的原因除了耗时函数外,也可能是由于主线程CPU的调度受到了瓶颈、或者是向Looper发送了过多消息导致等诸多原因。
logcat 信息
从logcat信息中,可以辅助验证、获取ANR发生前后的一些信息。
函数执行火焰图(Flame Chart)的展示
火焰图可以以全局的视角来看应用函数执行的情况,更直观的分析出可能导致性能瓶颈的调用栈。
使用ANR监控平台排查问题的思路
简单来说,通过现有的平台,可以从以下几个方面来分析ANR问题
- 从ANR的前后台状态、应用运行时长、进程cpu使用率、Message Trace 中Message 的cpuTime、wallTime的差值 大概对当前应用的cpu调度能力有一个评估。
- 从火焰图中 分析是否有明显耗时的业务代码
- 从 MessageTrace中看是否有明显异常的消息处理(单个消息处理过长、Message数过多 等情况)
- 从CPU使用率上,看 user和kernel的使用占比有无异常,如果kernel使用较高,则分析是否发生了大量的系统调用,如果user层的使用率较高,则说明在业务侧有较多繁重的任务执行
部分问题治理分享
本节是分析在ANR平台上线后,利用ANR平台定位到的一些ANR问题。
调用Kotlin Function 对象的 toString 函数调用导致ANR
在排查ANR日志时,发现有多个ANR 日志中,Kotlin 的KfunctionImpl.toString都比较耗时,火焰图case如下
于是在线下对Kotlin的该函数进行了测试,在demo中参照火焰图中的业务代码进行了场景还原
结果发现,一个简单的toString调用,在demo中耗时达到了 118ms。经过一系列的变量测试发现,在引入kotlin后,并且 引入了 kotlin-reflection库的情况下, 调用 Kotlin Function对象的toString函数时 耗时非常严重。因此在业务代码中要注意,不要对kotlin的 函数对象 直接或者间接的触发toString操作。
主线程执行View.getDrawingCache进行bitmap转换
从队列中,发现大量的消息已被阻塞,并且在前序的Mesage处理记录中,发现有耗时900ms的消息处理。
转到对应的 method trace中发现,卡顿主要是由于在主线程对Webview获取drawingCache导致的。
在线下进行测试时,可能在正常情况下该函数的耗时并没有这么耗时,因此暂不能确定该问题是如何引起的,但是从代码实现的角度,也不应该在主线程执行类似的耗时操作,因此可以将该操作移入异步线程执行。
SP 等待任务造成的 ANR 问题
在ANR平台上线后,发现目前收集的用例中 有大量数据是由于 系 统组件在 onPause、onStop等相关流程需要等待所有异步的 SP写入操作完成造成的
得物目前的业务都已使用 MMKV 替换SP,主要使用SP的场景都是接入的一些三方SDK,而这些sdk一般不需要对这部分数据在组件跳转间 对“异步任务数据写入完成”的强保证,因此得物对这部分系统的机制进行了HOOK,在这部分HOOK功能上线后,ANR率有了明显下降。
后续优化迭代方向
从目前ANR平台上线后 问题的治理情况上来看,大部分容易解决的问题都是从 火焰图上分析出明显耗时的业务代码从而解析的,在实际的线上场景中,目前还是发现有一些特定的case以现有上报的日志信息无法明确给出发生ANR的根本原因。后面会根据每种case,加强其他方面的监控能力,来协助定位问题。
端侧
其他上下文信息收集的增强
比如在线上发现,某些ANR问题 耗时函数卡在一个简单的 File.exist函数调用上,针对此类问题,后续可能需要结合native层 对file io操作的一些监控、hook来协助定位问题。
端上 Method Trace 性能优化
目前得物APP在端上进行Method Trace的方案是 在子线程定时通过主线程的Thread对象 调用getStackTrace进行采集,虽然该函数的执行是在异步线程,但是在虚拟机底层在非当前线程获取某个线程的堆栈时,会先suspend对应的线程,获取完成后再resume。 因此如果对主线程抓起堆栈的频率较为频繁的话,势必会影响主线程正常函数的执行。
我们的MethodTrace的要求是应该尽量保证其只产生极小的运行时开销,避免为了监控反而影响应用性能,虽然目前的这套采样方案,控制的最小采样间隔为50s,对性能影响不大。在facebook 的profilo开源库中有一套 异步非阻塞获取线程堆栈的方案,不过其目前只开源了适配了 Android 9及以下的版本。字节跳动的西瓜视频 分享了另外一种 Java Method Trace ,不过该方案也是需要suspend thread, 也是一个可以参考的方向。
平台测
MessageTrace 体验优化
目前Looper Message Trace 功能和 Method Trace(函数执行火焰图)功能是两个比较独立的功能,当我们在Message Trace 功能 中发现某个消息比较耗时,正常情况下是希望能够直接看到该消息具体做了什么操作,因此在平台测可以做的一个优化是, 当点击Message时,根据这个Message的处理的开始结束时间,自动截取对应时间的函数堆栈片段,进行信息展示。