字节跳动 iOS Heimdallr 卡死卡顿监控方案与优化之路

本文主要介绍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,当主线程阻塞的时间超过该阈值,则会触发全线程的抓栈,获取卡顿场景的堆栈信息。此后监听线程继续等待主线程直至主线程恢复,并计算卡顿的总时间,整合之前获取的堆栈信息,上报卡顿异常。需要说明的是,如果在抓栈之后主线程无法恢复,那么该异常不是卡顿,应交由卡死模块处理。


  1. 卡死(WatchDog)

与卡顿不同,卡死的阻塞是更长的,而且是无法恢复的。iOS系统会对App的主线程进行类似的监控,一旦发现了阻塞的情况,持续时间大于当前系统内允许的阈值(不同iOS版本和机型不同),就会强制杀死当前App进程,这个操作是没有任何通知的。因此我们需要做的就是在系统发现卡死并强杀之前,获取堆栈,并尽可能的评估出卡死持续的时间。

预先设定一个卡死的阈值T(默认是8s),这个阈值可以是相对保守的,并不是说超过了这个阈值就一定会被判定为卡死。在超过卡死阈值T的时候,获取全线程的堆栈,并保存至本地文件中。之后每隔一段时间(采样间隔,默认是1s),会进行一次采样。采样的目的不是为了获取新的堆栈,而是为了更新卡死持续的时间,将该信息保存至本地文件中。因此,采样的间隔越小逼近真实卡死时间按越精确。直至到某一个时间节点,系统把App杀死。当App下一次启动时,卡死模块会根据上一次启动中保留的本地文件信息,还原出卡死的堆栈、持续时间等信息,并上报卡死异常。

需要说明的是,很多人认为卡死一定是因为死锁、死循环这样的场景,导致程序永远也无法完成导致的。其实不然,在很多场景下,一个或多个耗时的操作,只要其耗时超过了系统的允许阈值,都会触发卡死。当应用启动过程中,没有在限定时间内完成初始化工作也会被系统杀死。所以,某些卡死可能是多个场景的不合理一起导致的,这也给卡死的问题定位提出了更高的要求。


四、问题与优化

理想是丰满的,现实是骨感的。看似”无懈可击“的监控方案,在线上却暴露出不同程度的问题。

  1. 卡顿监控优化

在卡顿监控中,我们认为超过了卡顿阈值时获取的堆栈一定是一个卡顿的场景,其实不然。在一些时候,获取的堆栈可能是他人的”背锅侠“。我们来看下面这个case。导致主线程卡顿的是4这个耗时操作,但是当我们设定阈值超时时,获取的堆栈却是没有任何性能问题的5。因此如果使用这种方式来进行卡顿的监控,一定会存在误报。而根据概率来讲,虽然上报的5是一个误报,但就线上的上报量来讲,4的数量一定是要大于5的。因此上报量级大的堆栈才应该是真正的耗时操作,是需要我们专注去解决的,而那些量级较小的堆栈则可能是误报。

image

那么是否能够通过一些技术手段,在控制性能开销的情况下,对卡顿场景捕捉的更加准确呢?一个比较好的思路就是采样策略。如下图,我们在原有的”常规模式“的基础上增加了”采样模式“。需要额外定义采样间隔、采样阈值。我们把卡顿阈值的等待过程,划分为以采样间隔为单位的粒度更细的时间节点。在每个时间节点进行主线程采样,对主线程进行堆栈的提取。由于仅对主线程进行堆栈提取,所以耗时较全线程抓栈要小很多。

获取了主线程堆栈后,通过提取顶层第一个自身调用来进行堆栈的聚合。如果某一个相同堆栈持续的时间超过了设定的采样阈值,例如图中的4,重复了3次,那么就会判定该场景一定是一个卡顿场景。那么此时就会进行全线程抓栈,而后面的卡顿阈值触发时则不再抓栈。\

结合主线程采样,我们可以更加精准的以函数级别监控卡顿场景,但是也需要付出采样带来的额外性能开销。为了将采样的开销降至最低,避免线上对低端设备造成二次性能劣化,卡顿监控支持采样功能的退火策略。当某一个卡顿场景被多次捕获时,为了避免再次将其捕获,造成不必要的性能浪费,会逐步增加采样间隔,直至将”采样模式“退化成”常规模式“。


在Slardar平台配置并开启采样功能后,可以通过sample_flag来过滤通过采样超时获取的卡顿异常。通过此方式获取的堆栈,大概率为卡顿场景,可以更加有针对性的去分析和解决。

  1. 卡死监控优化

相比卡顿,卡死的误报大多发生在后台(目前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导致的(大概率是dyldOC Runtime Lock造成的死锁)。一旦发生这种类型的卡死,其它所有线程的OC代码都会因此而阻塞,当然也包括监听线程,卡死监控此时就无法捕获这个异常。为了能够覆盖所有场景,我们把卡死、卡顿模块的所有逻辑进行了C/C++重构,解除了对OC调用的依赖,并且性能相比与OC实现进一步得到提升。

结语

HeimdallrANRWatchDog模块经过一段时间的迭代与优化,达到了一个全面、稳定、可靠的状态。这期间的一些优化思路借鉴了一些开源的APM框架,并结合使用方的实际需求进行不断改进。感谢所有使用方的反馈,帮助我们不断完善我们的功能与体验。后续我们会继续针对Watchdog场景增加防卡死功能,帮助接入方能够在无侵入式的情况下,解决通用场景的卡死问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容