字节跳动 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,当主线程阻塞的时间超过该阈值,则会触发全线程的抓栈,获取卡顿场景的堆栈信息。此后监听线程继续等待主线程直至主线程恢复,并计算卡顿的总时间,整合之前获取的堆栈信息,上报卡顿异常。
需要说明的是,如果在抓栈之后主线程无法恢复,那么该异常不是卡顿,应交由卡死模块处理。
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万条事件量。
点击这里,立即申请
字节跳动 iOS Heimdallr 卡死卡顿监控方案与优化之路的更多相关文章
- 【转】iOS实时卡顿监控
转自http://www.tanhao.me/code/151113.html/ 在移动设备上开发软件,性能一直是我们最为关心的话题之一,我们作为程序员除了需要努力提高代码质量之外,及时发现和监控软件 ...
- iOS应用千万级架构:性能优化与卡顿监控
CPU和GPU 在屏幕成像的过程中,CPU和GPU起着至关重要的作用 CPU(Central Processing Unit,中央处理器) 对象的创建和销毁.对象属性的调整.布局计算.文本的计算和排版 ...
- 解决页面使用overflow: scroll,overflow-y:hidden在iOS上滑动卡顿的问题
解决页面使用overflow: scroll,overflow-y:hidden在iOS上滑动卡顿的问题 div{ width: 100%; overflow-y: hidden; -webkit-o ...
- 记 页面使用overflow-scroll在iOS上滑动卡顿的问题
页面使用overflow-scroll在iOS上滑动卡顿的问题 因在做一个滑动的list列表,为某个div使用了overflow: scroll属性. 结果在手机上测试时,ios手机有明显的滑动卡顿问 ...
- (转)公有云vr客户端tcp连接数太多造成 系统卡顿问题 [bittorrent tracker优化] -公有云常见网络问题及思路
在公有云服务器 发现使用tcp(http)的tracker连接数太多 用户太多会造成windows系统卡顿 特此发表一下修改配置和路由器的方法 解决卡顿问题 解决方法1(参考内容): 修改 /etc/ ...
- 解决微信小程序ios端滚动卡顿的问题
方案1:直接使用微信小程序提供的 “scroll-view " 组件. <scroll-view scroll-y style="height: 100%;"> ...
- 想让安卓app不再卡顿?看这篇文章就够了
欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由likunhuang发表于云+社区专栏 实现背景 应用的使用流畅度,是衡量用户体验的重要标准之一.Android 由于机型配置和系统的 ...
- 面试官:你的App卡顿过吗?你是如何监控的?
一.故事开始 面试官:平时开发中有遇到卡顿问题吗?你一般是如何监控的? 来面试的小伙:额...没有遇到过卡顿问题,我平时写的代码质量比较高,不会出现卡顿. 面试官:... 这回答似乎没啥问题,但是如果 ...
- ios添加-webkit-overflow-scrolling依然卡顿
项目由vue-cli2创建 在overflow: auto区域内滑动ios手机出现卡顿,搜索资料后添加-webkit-overflow-scrolling: touch ios bug: 1.滑动区域 ...
随机推荐
- 【LeetCode】1128. Number of Equivalent Domino Pairs 等价多米诺骨牌对的数量(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 字典统计 代码 复杂度分析 日期 题目地址:http ...
- 【LeetCode】887. Super Egg Drop 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 参考资料 日期 题目地址:https://leetc ...
- 第四十五个知识点:描述一些对抗RSA侧信道攻击的基础防御方法
第四十五个知识点:描述一些对抗RSA侧信道攻击的基础防御方法 原文地址:http://bristolcrypto.blogspot.com/2015/08/52-things-number-45-de ...
- Wiener Filtering
目录 基本 滤波的推导 特别的情况 特别的例子 Signals, Systems and Inference, Chapter 11: Wiener Filtering (mit.edu) 基本 在图 ...
- uniapp上传图片后台返回url后放入image中图片不显示问题
原因:后台返回路径问题后台返回地址斜杠为 \ 我们需要的是 / 如下后台返回的地址导致转义了 修改后可以显示
- CS5213demoboard设计电路|DMI转VGA带II2S音频输出转接线|CS5213方案
CS5213是台湾CAPSTONE瑞奇达推出的一款HDMI(高清多媒体接口)到VGA转换芯片. CS5213设计HDMI转VGA带II2S转接线产品特性: ◇将完整的HDMI信号转换为VGA输出◇支持 ...
- MySQL 数据操作与查询笔记 • 【第1章 MySQL数据库基础】
全部章节 >>>> 本章目录 1.1 数据库简介 1.1.1 数据和数据库定义 1.1.2 数据库发展阶段 1.1.3 数据库系统组成 1.1.4 关系型数据库 1.2 M ...
- 关于linux的一点好奇心(一):linux启动过程
一直很好奇,操作系统是如何工作的?我们知道平时编程,是如何让代码跑起来的,但那些都是比较高层次的东西.越往后,你会越觉得,像是空中楼阁,或者说只是有人帮你铺平了许多道理,而你却对此一无所知. 1. 操 ...
- Pycharm的界面修改与基本设置
Pycharm的一些基本设置 设置字体大小与字体样式 首先我们打开Pycharm,点击左上角File,找到sitting 点击后就能进入设置界面 xdm,我将介绍两种更改字体的设置 方法一 设置界面找 ...
- Solr单机安装Version5.5.2
Solr安装单机模式,基于Solr的安装版本为5.5.2. 安装规划 IP/机器名 安装软件 运行进程 zdh-9 solr jar 安装用户 solr/zdh1234 hadoop useradd ...