

字节跳动 iOS Heimdallr 卡死卡顿监控方案与优化之路
source link: https://my.oschina.net/u/4180867/blog/5403712
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文主要介绍
Heimdallr
对卡死、卡顿异常的监控原理,并结合长时间的业务沉淀发现的问题进行不断迭代和优化,逐步实现全面、稳定、可靠的历程。
作者:字节跳动终端技术——白昆仑
卡死、卡顿作为目前iOS App的重要性能指标,不仅影响着用户体验,更关系到用户留存、DAU等重要产品数据。本文主要介绍Heimdallr
对卡死、卡顿异常的监控原理,并结合长时间的业务沉淀发现的问题进行不断迭代和优化,逐步实现全面、稳定、可靠的历程。
一、什么是卡死/卡顿?
卡顿,顾名思义就是在使用过程中出现了一段时间的阻塞,使得用户在这一段时间内无法进行操作,屏幕上的内容也没有任何的变化。Heimdallr
在监控指标上,根据阻塞时间的长短进行了3个等级的划分。
1、流畅性与丢帧:动画、滑动列表不流畅,一般为十几至几十毫秒的级别
2、卡顿:短时间操作无反应,恢复后能继续使用,从几百毫秒至几秒
3、卡死:长时间无反应,直至被系统杀死,通过线上收集数据,最少为5s
可以看到,根据严重性由小至大可将卡顿问题划分为流畅性与丢帧、卡顿、卡死三个不同的等级。卡死的严重程度与Crash是相当的,甚至更为严重。因为卡死不仅仅造成了类似于崩溃的闪退,更使得用户被迫等待了相当长的一段时间,更加损害用户的体验。由于监控方案上的差异,本文主要面向的是后两者卡顿和卡死的监控。
二、卡死/卡顿的原因
iOS开发中,由于UIKit是非线程安全的,因此一切与UI相关的操作都必须放在主线程执行,系统会每16ms(1/60帧)将UI的变化重新绘制,渲染至屏幕上。如果UI刷新的间隔能小于16ms,那么用户是不会感到卡顿的。但是如果在主线程进行了一些耗时的操作,阻碍了UI的刷新,那么就会产生卡顿,甚至是卡死。主线程对于任务的处理是基于Runloop
机制,如下图所示。Runloop支持外部注册通知回调,提供了
1、RunloopEntry
2、RunloopBeforeTimers
3、RunloopBeforeSources
4、RunloopBeforeWaiting
5、RunloopAfterWaiting
6、RunloopExit
6个时机的事件回调,其流转关系如下图所示。Runloop
在没有任务需要处理的时候就会进入至休眠状态,直至有信号将其唤醒,其又会去处理新的任务。
在日常编码中,UIEvent
事件、Timer
事件、dispatch
主线程任务都是在Runloop
的循环机制的驱动下完成的。一旦我们在主线程中的任何一个环节进行了一个耗时的操作,或者因为锁的使用不当造成了与其它线程的死锁,主线程就会因为无法执行Core - Animation
的回调而造成界面无法刷新。而用户的交互又依赖于UIEvent
的传递和响应,该流程也必须在主线程中完成。所以说主线程的阻塞会导致UI和交互的双双阻塞,这也是导致卡死、卡顿的根本原因。
三、监控方案
既然问题的根本在于主线程Runloop
的阻塞,那么我们就要通过技术手段监测主线程Runloop
的运行状态。为了能够实时获取主线程Runloop
的状态,首先对主线程注册上面提到的几个事件回调,在触发事件回调时,利用signal
机制将其运行状态传递给另一个正在监听的子线程(后面称之为监听线程)。监听线程对于信号的处理可以是多样的,它可以设置等待signal
的超时时间,如果超过了设定的阈值,这说明主线程可能正在经历阻塞。通过监听线程,我们可以完整地了解到主线程Runloop
循环的周期,目前处于哪个阶段,耗时了多久等等。根据这些必要的信息,就可以采取对应的策略进行异常的捕获和处理,后面会单独就卡顿、卡死分别进行说明。
目前大多数APM工具都是采用监听Runloop
的方式进行卡顿的捕获,这也是性能、准确性表现最好的一种方案。由于RunloopBeforeTimers
的和RunloopBeforeSources
是紧邻的两个事件回调,Heimdallr
为了降低Runloop
频繁事件回调造成的性能损失,去除了对RunloopBeforeTimers
的监听。
1. 卡顿(ANR)
卡顿监控的特点在于主线程的阻塞是暂时的、能够恢复的,因此我们要获取卡顿持续的时间,用来评估卡顿问题的严重性。我们预先设定一个卡顿时间的阈值T,当主线程阻塞的时间超过该阈值,则会触发全线程的抓栈,获取卡顿场景的堆栈信息。此后监听线程继续等待主线程直至主线程恢复,并计算卡顿的总时间,整合之前获取的堆栈信息,上报卡顿异常。
需要说明的是,如果在抓栈之后主线程无法恢复,那么该异常不是卡顿,应交由卡死模块处理。
2. 卡死(WatchDog)
与卡顿不同,卡死的阻塞是更长的,而且是无法恢复的。iOS系统会对App的主线程进行类似的监控,一旦发现了阻塞的情况,持续时间大于当前系统内允许的阈值(不同iOS版本和机型不同),就会强制杀死当前App进程,这个操作是没有任何通知的。因此我们需要做的就是在系统发现卡死并强杀之前,获取堆栈,并尽可能的评估出卡死持续的时间。
预先设定一个卡死的阈值T(默认是8s),这个阈值可以是相对保守的,并不是说超过了这个阈值就一定会被判定为卡死。在超过卡死阈值T的时候,获取全线程的堆栈,并保存至本地文件中。之后每隔一段时间(采样间隔,默认是1s),会进行一次采样。采样的目的不是为了获取新的堆栈,而是为了更新卡死持续的时间,将该信息保存至本地文件中。因此,采样的间隔越小逼近真实卡死时间按越精确。直至到某一个时间节点,系统把App杀死。当App下一次启动时,卡死模块会根据上一次启动中保留的本地文件信息,还原出卡死的堆栈、持续时间等信息,并上报卡死异常。
需要说明的是,很多人认为卡死一定是因为死锁、死循环这样的场景,导致程序永远也无法完成导致的。其实不然,在很多场景下,一个或多个耗时的操作,只要其耗时超过了系统的允许阈值,都会触发卡死。当应用启动过程中,没有在限定时间内完成初始化工作也会被系统杀死。所以,某些卡死可能是多个场景的不合理一起导致的,这也给卡死的问题定位提出了更高的要求。
四、问题与优化
理想是丰满的,现实是骨感的。看似”无懈可击“的监控方案,在线上却暴露出不同程度的问题。
1. 卡顿监控优化
在卡顿监控中,我们认为超过了卡顿阈值时获取的堆栈一定是一个卡顿的场景,其实不然。在一些时候,获取的堆栈可能是他人的”背锅侠“。我们来看下面这个case。导致主线程卡顿的是4这个耗时操作,但是当我们设定阈值超时时,获取的堆栈却是没有任何性能问题的5。因此如果使用这种方式来进行卡顿的监控,一定会存在误报。而根据概率来讲,虽然上报的5是一个误报,但就线上的上报量来讲,4的数量一定是要大于5的。因此上报量级大的堆栈才应该是真正的耗时操作,是需要我们专注去解决的,而那些量级较小的堆栈则可能是误报。
那么是否能够通过一些技术手段,在控制性能开销的情况下,对卡顿场景捕捉的更加准确呢?一个比较好的思路就是采样策略。如下图,我们在原有的”常规模式“的基础上增加了”采样模式“。需要额外定义采样间隔、采样阈值。我们把卡顿阈值的等待过程,划分为以采样间隔为单位的粒度更细的时间节点。在每个时间节点进行主线程采样,对主线程进行堆栈的提取。由于仅对主线程进行堆栈提取,所以耗时较全线程抓栈要小很多。
获取了主线程堆栈后,通过提取顶层第一个自身调用来进行堆栈的聚合。如果某一个相同堆栈持续的时间超过了设定的采样阈值,例如图中的4,重复了3次,那么就会判定该场景一定是一个卡顿场景。那么此时就会进行全线程抓栈,而后面的卡顿阈值触发时则不再抓栈。
结合主线程采样,我们可以更加精准的以函数级别监控卡顿场景,但是也需要付出采样带来的额外性能开销。为了将采样的开销降至最低,避免线上对低端设备造成二次性能劣化,卡顿监控支持采样功能的退火策略。当某一个卡顿场景被多次捕获时,为了避免再次将其捕获,造成不必要的性能浪费,会逐步增加采样间隔,直至将”采样模式“退化成”常规模式“。
在Slardar平台配置并开启采样功能后,可以通过sample_flag
来过滤通过采样超时获取的卡顿异常。通过此方式获取的堆栈,大概率为卡顿场景,可以更加有针对性的去分析和解决。
2. 卡死监控优化
相比卡顿,卡死的误报大多发生在后台(目前Heimdallr
提供后台卡死过滤,如果对后台卡死不关心的业务方可以自行打开)。因为后台场景的限制,当前App的线程优先级更低,而且随时存在被系统挂起的可能,这给我们进行卡死时间的判定带来了很多问题。
上面的Case描述的是一个卡死的误报场景,因为在后台的原因线程的优先级较低,因此1、2、3任务执行的时间要比前台更久,更加容易超过我们的卡死阈值。而后,因为iOS系统的策略问题,后台应用被挂起(suspend),直至某一个时间点因为内存紧张,将整个应用杀死。但请注意,这个流程属于App正常的生命周期范畴,并不是WatchDog
。而按照我们之前的策略,这将会被判定为卡死。由于我们无法监听到suspend事件,所以这种场景目前还无法排除误报。
还有一种误触发卡死的case是,suspend发生在8s阈值前,在长时间的挂起后,应用被resume,此时8s的超时被触发。但是实际上,我们的App只有在8s中的很少一部分时间在running,大部分时间都是被挂起,所以不应该触发卡死判定。归根结底是卡死计时的准确性问题。
为了解决上面的问题,对计时策略进行了改进。相比于直接进行8s的等待,我们将时间细分为8个1s。如果在这段时间内App被挂起,等到恢复时也不会直接超过8s的阈值,而仅仅会造成最多1s的误差。
此外,上面也提到过,卡死有的时候可能是多个耗时场景累计导致的。为了能够跟踪主线程的变化,在抓栈之后的采样阶段,对主线程进行堆栈采样,并将其一起上报。结合采样中获取的主线程堆栈,我们可以得到一个主线程堆栈变化的时间线,能够更加准确的帮助定位问题所在。(时间线功能在Heimdallr 0.7.15之后支持)
最后,我们发现部分卡死场景是由于OC Runtime Lock
导致的(大概率是dyld
和OC Runtime Lock
造成的死锁)。一旦发生这种类型的卡死,其它所有线程的OC代码都会因此而阻塞,当然也包括监听线程,卡死监控此时就无法捕获这个异常。为了能够覆盖所有场景,我们把卡死、卡顿模块的所有逻辑进行了C/C++重构,解除了对OC调用的依赖,并且性能相比与OC实现进一步得到提升。
Heimdallr
的ANR
、WatchDog
模块经过一段时间的迭代与优化,达到了一个全面、稳定、可靠的状态。这期间的一些优化思路借鉴了一些开源的APM框架,并结合使用方的实际需求进行不断改进。感谢所有使用方的反馈,帮助我们不断完善我们的功能与体验。后续我们会继续针对Watchdog场景增加防卡死功能,帮助接入方能够在无侵入式的情况下,解决通用场景的卡死问题。
🔥 火山引擎 APMPlus 应用性能监控是火山引擎应用开发套件 MARS 下的性能监控产品。我们通过先进的数据采集与监控技术,为企业提供全链路的应用性能监控服务,助力企业提升异常问题排查与解决的效率。
目前我们面向中小企业特别推出「APMPlus 应用性能监控企业助力行动」,为中小企业提供应用性能监控免费资源包。现在申请,有机会获得60天免费性能监控服务,最高可享6000万条事件量。
👉 点击这里,立即申请
Recommend
-
57
要监控网页的卡顿,我们必须从 FPS 说起。 FPS 是来自视频或者游戏里的概念,即是每秒的帧数,代表视频或者游戏的流畅度,俗话说,就是“不卡”。 那在前端开发领域,网页的 FPS 是什么呢? 什么是网页的 FPS?...
-
35
XXPerformanceMonitor是一个Swift版轻量卡顿监控工具,支持主线程和子线程,一句代码即可轻松集成,开源在蜗牛的Github,可以结合代码来阅读本文。 曾几何时跟项目大佬有过这样的对话 大佬:最近有用户反馈用起来卡卡的,不太流畅,有找到原
-
19
背景 使用Flutter技术构建的应用,一直以高性能高流畅度著称。但是随着应用复杂度越来越高,Flutter会出现一些页面流畅度明显低于Native的情况,甚至可能发生一些卡顿...
-
11
这回因为需要查看同事的前端React项目,所以从版本控制仓库下载后准备打开。使用的依旧是地表最强IDE,IntelliJ IDEA。 但是打开以后,进行npm install以后IDEA整个卡死了!本篇文章就来记录下如何解决IDEA打开Vue/React/Node项目卡顿卡死的问题。...
-
15
改善页面性能 - 如何监控卡顿和响应延迟背景为了提供给前端者监控页面性能的能力,W3C 定义了一系列相关 API,在这里统称为 Performance API。目前使用较多的应该是 PerformanceTiming,但是除了该 API,新的 W3C 草案及 WICG 提案定...
-
11
iOS 稳定性问题治理:卡死崩溃监控原理及最佳实践 不同于...
-
8
卡顿 & ANR 在各 APP 中都是非常影响用户体验的问题,关于其的分析和治理一直也是个老生常谈的话题。过去调查卡顿 & ANR 问题主要依赖上报的堆栈和 traceInfo 文件,通过这些信息还原问题的现场情况。但是在实践过程中发现,现有监控机制下堆栈的抓取时...
-
7
应用性能前端监控,字节跳动这些年经验都在这了 作者:字节前端技术-单是昊
-
5
字节跳动 DanceCC 工具链系列之Xcode LLDB耗时监控统计方案 ...
-
4
1. 讲故事 今天本来想写一篇 非托管泄露 的生产事故分析,但想着昨天就上了一篇非托管文章,连着写也没什么意思,换个口味吧,刚好前些天有位朋友也找到我,说他们的拍摄监控软件卡死了,让我帮忙分析下为什么会卡死,听到这种...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK