APM 页面加载耗时校准
APM 的全称叫做 Application Performance Monitor,属于应用性能监控部分。在手淘的 APM 中有一项特殊的数据,叫做页面可视耗时,不同于业界常规的技术视角阐述数据,我们更倾向于从用户体验交互进行阐述。
以 Activity 实现的页面为例,在页面加载过程中,用户关心的是从点击开始到最后页面完全呈现,直至用户可以进行交互的一个过程,而不是单纯 Activity 的 Lifecycle 耗时,更不是技术视角的一个阶段数据。
更逼近用户体验的数据是我们在页面加载上的追求。接下来我将从淘宝 APM 如何从用户角度实现可视计算的历史进行介绍,并深入详解淘宝对页面加载耗时校准所做一些努力与改变。
页面可视耗时可以拆解成起点与终点,起点争议相对较少,使用的是 Activity onCreate 时间、 Fragment onFragmentPreAttached 时间以及页面跳转时间,下面主要针对可视终点进行讨论:
▐ 为什么要引入可视计算?
业务埋点带来了标准的不统一
每一个页面的实现都是不一样的,每一个业务方对自己的可视终点有不一样的定义。有的人认为页面第一张图片出来了就是可视,有些人认为页面数据请求回来了就是可视,有些人认为第一帧上屏就是可视,一千个人心中有一千个哈姆雷特,这也导致埋出来的数据往往千差万别,甚至会出现肉眼看见更慢的页面数据更好的情况。
框架复杂,在现有基础上改动成本有些过高
当前淘宝首页会有 N 多个模版,并且模版间还存在组合关系。淘宝首页是根据模版下发进行渲染数据,如果说对模版改造有些不太现实。同时,现阶段的淘宝,业界中能见到的跨端框架,在淘宝都能看到影子,让所有框架改造一个统一的可视标准,让所有业务方手动打入可视时间有一些不切实际。
业务与框架侧都不具有全局视角,导致埋点不准确
往往业务方将数据请求回来交给容器渲染时就认为可视了,可却忽略了容器处理数据需要时间,数据上屏需要时间,容器将图片 URL 交给图片库进行加载亦需要时间。对于业务方而言,上诉耗时,对他是透明并不感知的,对于框架而言,只是进行一次渲染,并不知是页面加载还是刷新页面。
基于上诉问题,我们将解决方案放在业务无入侵、统一可视终点标准上,并最终提出了 8060 算法。
▐ 可视算法初步实现
8060 可视算法规则很简单,主要是将屏幕范围内的 View,对 X、Y 轴进行投影,当覆盖 X 轴长度的 80%、Y 轴的 60% 就认为是可视。
如果多线程对 UI 进行写操作,那么会导致 UI 的状态被破坏;但是如果是多线程读操作呢,多线程读 UI 的状态并不会破坏 UI 的状态,只会导致一个问题,即在读的过程中读到一个不稳定的状态,导致程序异常,比如说 NPE,APM 在尝试异步 UI 可视算法的起初,就遇到了 NPE 的一些问题。不过这到不是一个很严重的问题,因为当你读到了一个不稳定的状态时,恰恰说明 UI 的状态还不稳定,意味着 UI 在变化。抛弃本次计算,重新开始计算,直到有一次完整的页面计算。这个算法其实来源于读写锁的设计思路,如果读不会导致数据变化,那么在 UI 线程在读操作的时候异步线程也可以同时的读 UI,并不会对 UI 的状态导致破坏,程序依旧能正常运行。异步线程只需要保护好自己的运行状态,能正确处理一些异常即可。
▐ 可视算法初步演变
针对 View 不再是无差别计算,我们有了更细的规则:
View 在全部或者部分在屏幕范围内,且 Visibility 必须为 View.VISIBLE
只针对 View 进行计算,ViewGroup 不在计算范围之列,且不是 ViewStub
如果是 ImageView,Drawable 必须为 BitmapDrawable、NinePatchDrawable、AnimationDrawable、PictureDrawable
如果是 TextView,必须要有文字
如果是 EditText,判断是否已经聚焦,如果聚焦,整个页面直接加载完成
其他 View 默认加载完成
经过 APM 前期的发展,基础的页面加载耗时已经有了,但是准确性上还有待提高。
▐ 算法的迭代
将所有的合格有效 View 对 XY 轴分别进行投影,同时将有无效标记的 View 对 XY 轴分别进行投影,当合格的投影减去无效 View 投影,分别覆盖 Y 轴的 80%,X 轴的 60%,那么页面加载完成。
现在投影算法已经是 APM 里面的推荐算法。
▐ 支持 View 打标
计算时候,会识别有效元素和无效元素的标记,这个标记是什么?在手淘中,哪些又做了这些标记呢?
页面元素铺满屏幕不等于加载完成。我们发现很多页面为了追求更好的用户体验,会生成鱼骨图、页面打底图、图片打底图,同时页面为了追求更为完美的展示效果,自定义 View 也是页面上的常规操作。为了支持各种各样的 case,APM 采用 View tag 的方式进行解决,APM 中提供三个 tag 进行选择:
/** * 当前状态是无效的View,但是仅仅表示当前状态,有可能变成有效,例如 ImageView */ String APM_VIEW_VALID = "valid_view"; /** * 当前状态是有效的View */ String APM_VIEW_INVALID = "invalid_view"; /** * 需要完全忽略的无用 View,这个 View 完全是计算的噪点,例如鱼骨图 */ String APM_VIEW_IGNORE = "ignore_view";在手淘中,需要打标的主要包括:鱼骨图、自定义图片库、自定义View(继承于 ImageView、TextView 不需要打标)。而这一部分需要打标的 View 出现频次较低,改动较少,故而在成本较低情况下,可以大大拉高准确度。
这个变动毫无疑问是巨大的,也给 APM 在与业务低耦合的情况下,带来了更大的灵活性,例如,详情打开后会有一个全局遮罩的鱼骨图,对鱼骨图打标后,然后配合图片库打标,详情的页面加载准确性大幅提升
通过线下测试可以发现,详情的鱼骨图遮罩时间较长,在遮罩过程中会进行页面加载,如果不对鱼骨图进行打标,那么 APM 计算出来的加载时长与用户体感加载时长差距较大。下图是详情页面校准前后的线上数据,我们可以发现 7 月到 8 月线上数据有一个非常明显的数据跃迁:
通过给 View 打上是否合法的 tag,除了给 APM 带来了灵活性,也给业务带来了更大的自主性。对于高度复杂的业务来说,依旧存在自定义页面的结束时间的诉求,但是这种自定义结束时间并不是直接给 APM 塞一个时间戳,而是在自身页面加载逻辑完成之时,给 View 一个标记,标记此 View 完成了加载。例如,对于直播来说,他们需要的页面加载耗时,并不是直播页面的首帧,而是等待指定的直播小组件加载完成才算完成。如此高度定制的需求只需要做两步就可以完成:
在创建 View 树的时候,在根 View 上打上非法标记
当直播小组件加载完成,将根 View 上的非法标记改成合法标记
这是线上数据,APM 在校准上线后,发现更为符合业务方诉求与预期:
▐ 设置页面加载阈值
是否任意一个页面都适合同一加载阈值呢?答案当然不是的。
页面的布局、机型的适配直接影响着页面加载的比例,所以页面加载的上限在不同的页面中,也应该是不同的。在线下测试中发现,有些页面加载在某些手机上勉强了达到 80% 的阈值,有些手机上一直达不到,造成大量数据计算不成功。
基于以上考虑,APM 提供了设置单个页面加载阈值的口子,通过配合校准的各种改造,页面的加载时长准确性大幅度提高,同时 Android 的计算成功率也飙升。
errorCode = 0 表示计算成功,由 12% 上升到 78%
▐ 自定义页面根 View
APM 计算的根结点默认是页面上的 DecorView,往下遍历的根结点是否是合理的呢?存在修改的可能吗?是存在的,自定义页面根 View 可以更细粒度的控制页面。
对于页面的理解一定是 Activity 或者 Fragment 吗?不是的,我们既然讨论的时候用户体感加载时长,那么我们应该更多的从用户视角去考虑这件事情。例如,逛逛页面上有两个 tab,基于用户角度,我们更愿意理解成两个页面,推荐页面和关注页面。
以逛逛关注页面为例,点击关注 tab 时候创建了页面,整个关注页面也只是整个页面的一部分,下图为校准后的效果:
在 APM 中提出了 Page (页面)的概念,每一个 Page 有一个对应的 pageRootView,使用 pageRootView 来进行页面加载比率计算。
当我们仔细去观察页面的 View 树结构时,还发现自定义页面根结点带来了更大的灵活性,对于异常 case 也有了更多的处理手段,这里举一个我们在校准过程中遇到的例子,逛逛首页加载校准:
逛逛的首页是一个 Fragment,按道理说可以直接进行度量,但是在校准的时候发现:逛逛首页(推荐)有一个全局背景图。当尝试使用打标解决的时候才发现,逛逛业务定义了自身框架的 DSL,整个页面使用的是 DSL 进行编写(类 RN 原理),所有 ImageView 都是相同的 View,并没有任何特殊性,在端侧根本不感知这个背景图。这就意味着打无效标的办法不能用了。
那么还有其他低成本的办法吗?当然有的,查看逛逛首页的布局,发现全局背景图在整棵 View 树非常靠上的位置,与真正有效的 View 节点并没有关联,那么只需要将有效的子 View 树的根结点作为计算的起点就可以了。
注:在原始 APM 计算逻辑中,是使用整棵 View 树的根节点来进行向下遍历计算的
最后的方案:更换逛逛首页的页面根结点(pageRootView)。
下面是逛逛首页的校准效果,如果没有进行校准,那么当背景图出现的时候就是页面加载的结束点,而且二刷也没完。通过打标解决了二刷问题,通过修正页面根 View 解决背景图问题,由于页面 ImageView 存在动画,所以加载完成后会有一个渐显的动画,当前 APM 认为这个渐显动画不影响可视点:
在 H5 等场景下,WebView 的进度条并不一定代表真实的加载进度,导致 APM 的页面加载无痕算法在 H5 场景下准度很差。为此,我们引入 JSTracker (前端框架层)进行计算,与此同时,在 Android 中,UC 内核自主计算的可视时间也被引入其中。
UC 内核计算的可视时间原理又是什么呢?在页面加载的过程中,记录所有的渲染帧,在页面加载结束之后,回溯检查每一帧,图片渲染面积首次达到最大值的那一帧记为可视时间,而 JSTracker 计算原理类似,不同的是,JSTracker 是在前端框架层进行计算。
▐ 前端自定义终点
除了 JsTracker/T2 的页面加载终点之外,还有没有其他办法来体现自身业务的自定义页面加载终点呢?有的。APM 与前端框架定下了此规范。
对于前端 H5 来说,如果需要自定义页面终点,首先需要通过一种方式告诉容器,自身是需要自定义终点的,告诉的方式就是:增加 APM 规定的 HEADER 标签。
容器读取到了这个 HEADER 标签,表明当前 H5 页面遵守规范,需要自定义终点,将会通过 APM 提供的 JSBridge 提供页面加载的终点。此外,APM 也提供埋入参数和阶段数据的 JSBridge。
在大促会场场景下,使用此方案支持了性能优化结果产出,其产出的结果中,除了页面可视之外,还包括秒开率,系统耗时,H5 容器框架耗时,前端耗时等指标,结合 AB 产出对比数据结果,同时结合设备分级数据细化在不同手机等级下的数据。
▐ 挑战:多容器内嵌页面
在手淘中有各种各样的跨端容器框架,如 weex 等。存在一个页面上多种容器并存的情况,容器与容器之间,数据如何兼容,APM 提出了自身的仲裁方案。
在店铺一个页面中,有可能存在多种框架混合使用的情况,举个例子,可以用 WebView 实现一个广告推荐,可以用 Weex 渲染出整个页面。
如果每一个容器直接将自己的页面加载时间点通过一个接口直接打进来,APM 选取哪一个作为页面真正的加载结束时间戳呢?如果选取最小的时间戳,如果对应的 View 不是页面主要元素,那么这个值比体感加载时长小,如果选取最大的时间戳,就有可能偏慢。
其实在 Native 角度,每一个容器只是 View 树上的一个节点,APM 只需要关心这个子 View 是否加载完成,然后使用页面加载算法计算(8060算法),就可以知道整个页面是否加载完成。
那么问题就简化成如何知道这个子 View 是否加载完成。当前 APM 支持对 View 打标,当 View 没有加载完成的时候,就会打上没有加载完成的 tag,完成页面加载就会打上完成的 tag。由于容器知道自己的加载状态,就只需要在合适的时候,给自己的 View 打上合法的 tag 即可。
由于 APM 在遍历 View 树的时候,一旦发现 View 打上了 tag,就不再往下遍历,直接确定了当前 View 的状态,起到了数据仲裁的效果。
对于 APM 页面加载耗时校准而言,目前 APM 还只是向前走了一小步。在最新的 APM 自动化页面加载耗时计算中,剔除了对用户页面加载体验无效的元素,聚焦页面加载体验中的核心元素,既给了业务相对的自由度,又达到了一定的加载体感准确性。
大淘宝技术新春拜年
“虎虎虎”
纸质红包大派送
关注”淘系技术“回复"红包“即可获得领取方式
(2月28日18:00截止)