cover_image

【Hummer Flutter引擎】高可用流畅度方案

叶然 U4内核技术 2022年06月02日 08:39

背景

随着越来越多的业务在深入使用flutter技术且迭代频繁,flutter在线上的实际表现受到了广泛的关注。在此背景下,一套完善的高可用监控体系变得尤为重要,它不仅要能准确地定位线上的性能和稳定性问题,还要能提供一些辅助信息帮助开发同学去定位问题,从而提升用户使用感受。这篇文章将介绍hummer在基于集团原有的flutter高可用方案的基础上在流畅度相关问题的探索和优化。

行业方案对比

既然想做探索和优化那咱们就要知道,现有的高可用做到了什么地步。以下的一些数据是通过GMTC的分享获得。


帧率FPS

卡顿JANK

卡顿率STUTTER

高可用旧方案

60*掉帧率

ui或raster耗时跟16.66ms对比

腾讯

未知

电影帧

卡顿时长/测试时长

字节

60*掉帧率

掉帧数大于7帧为严重卡顿

未知

美团

onReportTiming

未知

未知


指标探索与优化

Hummer的高可用方案,将采用FPS帧率,JANK卡顿,卡顿率STUTTER来综合的监控流畅度问题,在指标的建设过程中,采用笨办法去摸索和测试行业内的现有方案,发掘方案的不足,探寻各个指标的底层原理,致力于将指标监控做的准确、通用,可以和不同监控方案的指标相比较。

图片

帧率fps

现有方案

旧方案和字节都采用了60*掉帧率的方式来计算fps,旧方案的掉帧是通过选取当前帧的ui耗时和raster耗时的较大值和16.66ms去对比来判断掉帧。在逻辑上说不通,当前帧的ui和raster不是并行的。这样计算原理上就不准确,除此之外这种计算方式偏离了fps的定义,在和其他高可用计算出的fps数据没有可比性。最后这个方案还有个问题,就是无法适应高刷新率的场景。


美团的方式是通过onReportTiming 返回的FrameTiming的list的数量来反映fps,方法简单便捷,不需要做任何的改动,但是这个方法的问题是onReportTiming的返回是延迟的,如果想要将fps和其他的实时信息匹配会存在困难。


fps计算原理

测试了一遍现有的方案后,发现都有问题,难道就没有什么准确的方案吗?在求助了测试组的同学后,答案是“adb shell dumpsys SurfaceFlinger”,虽然这是一个线下方案,但是不妨碍我们搞明白计算fps的奥义。


Android系统中App将需要显示的内容绘制在BufferQueue的Buffer中,Buffer本身可以通过dequeue获取。将内容填充到Buffer以后,通过queue将Buffer还回到BufferQueue,然后SurfaceFlinger需要合成时,需要调用acquire 从BufferQueue中取buffer,同理使用release去归还。

图片

在上面的流程中,存在一个问题就是CPU和GPU是异步的,当app通过queue 归还Buffer的时候,此时仅仅是CPU完成了,GPU是否完成CPU侧并不知道。所以我们在CPU侧执行一系列的OpenGL的函数调用,实际上,只是把命令放在command buffer里,GPU什么时候开始执行CPU不知道,除非使用glFinish()去同步,但是这样会导致CPU和GPU无法并行。


 所以在android的图形系统中引入了一套同步框架,让CPU和GPU可以异步执行,但是也不会出现BufferQueue的Buffer被同时读写的情况。这个就是fence,当一个buffer正在被写或者读的时候,它就会被fence起来,当操作完成后fence解除,然后surfaceFlinger可以获取这个解除fence的Buffer去显示,这样就可以保证对Buffer的操作是安全的。


每一个Buffer在被surfaceFlinger读取完后,会release fence,此时表示这个Buffer又可以被新的数据写入,“adb shell dumpsys SurfaceFlinger”指令会dump每一帧的这个时刻, 然后根据时间戳的数量和时间戳的间隔就可以推算出fps。


hummer fps 方案

准确性优化

那么来到线上,我们如何模拟线下的方法去准确地计算fps数据呢?首先我们来看看线上相对于线下的限制在哪里。


图片


如上图我们可以看到帧的一生从业务代码到引擎到系统,帧是否真正上屏,最为准确的检测地方在系统侧,但是要在线上获取系统侧的数据,实属麻烦。虽然如此还是有替代的方法,由于flutter的渲染管线深度只为2,如果在ui线程调用handleBeginFrame的时候去统计帧的数量,和最后surfaceFlinger统计帧的数量理论上最差的情况只有3帧的差距。字节的方案会在raster线程去监控,但是这样做的就是需求修改引擎,不是一个通用的方案,而且当帧率要匹配一些场景数据时很多数据引擎中也没有。

因此选择在handleBeginFrame的时候模拟线下的方法来计算fps最为合理,且实际测量中和surfaceFlinger的差距都维持在1 到 2fps以内。


准确性测试

测试手机:谷歌pixel3

测试场景1

视频加载失败,请刷新页面再试

刷新

图片


灰色:高可用旧方案

蓝色:dump surfaceFlinger

橙色:高可用新方案


测试场景2

视频加载失败,请刷新页面再试

刷新

图片

灰色:高可用旧方案    

蓝色:dump surfaceFlinger    

橙色:高可用新方案


分类和场景优化

现在已经有个较为准确的计算方式,但是仅有一个准确方法还是不能做好线上监控,在实际灰度中发现,搜集上来的怪异fps数据量巨大,无法分析。主要的原因在于线上操作环境复杂,产生fps的数据场景各种各样,在线下测试之所以觉得fps有用,是因为我们线下测试时已经知道测试场景是什么,会在特定的场景下去考虑这个fps的意义。因此fps的监控增加如下的优化。

优化方法

说明

按照刷新率分类

监控主流刷新率下的fps数据 60hz 80hz 90hz 120hz 144hz(支持动态刷新率监控)

场景分类

每个fps的产生都增加场景信息(滑动,动画,等其他场景),方便分类观察,在一次灰度中,场景分类还帮助开发同学发现,一个不应该出现动画场景的业务竟然有动画fps数据出现的问题,修正此类问题可以减少app的耗电问题。

慢速拖拽判定

剔除用户在慢速滑动场景的fps数据

用户交互干扰过滤

过滤掉惯性滚动中夹杂了触屏操作的数据

以时间段为单位计算fps

以时间段为单位去计算fps,减少线上异常数据量

如上的优化和分类的核心思路是尽量的模拟平时线下测试时的场景,排除线上无意义的场景产生的fps数据,通过统计和分类让fps的数据可以像线下一样可以参考。

卡顿jank

现有方案

旧方案,在计算fps的时候已经阐述,是选取当前帧的ui耗时和raster耗时的较大值和16.66ms去对比去判断卡顿。


字节的高可用方案在判定普通卡顿时的思路跟高可用旧方案类似也是通过判定掉帧来判定卡顿,对于掉帧超过7帧的卡顿判定为大卡顿。


腾讯线下的测试工具perfdog采用了电影帧卡顿的概念:

电影帧卡顿:

  1. 当前帧耗时>前三帧平均耗时 2 倍。 

  2. 当前帧耗时>两帧电影帧耗时 (1000ms/24*2=84ms)。

  3. 两个条件同时满足

从上面的公式可以看出,电影帧的卡顿更关帧耗时的突变,同时关注电影帧是因为腾讯的游戏场景对帧率的要求不如app的那么高。

hummer的卡顿方案

那么线上该如何判定卡顿,线上判定卡顿更多是为了发现用户的体感卡顿,从而改善用户的使用感受。

出于视觉惯性和体感的考虑,选择使用腾讯的方案更为合理,但是腾讯的方案从算法的角度来说,对于滑动场景的卡顿判定,也过于宽松。如果采用严格的卡顿判定,即一帧的耗时超过两个vsycn周期(考虑有double buffer 或者triple buffer的缓冲)就为卡顿,可能会和体感卡顿关联性不强,综合考虑下,还是采用腾讯的方案,更为关注体感或者是严重的卡顿,至于卡顿判定过于宽松的问题,可以通过调整卡顿判定的阈值进行调整或者根据开发同学的需求可以根据不同场景的分类动态的切换监控方式。


帧耗时的计算原理

选好了方案,那么下面的问题就是如何计算出准确的帧耗时,因为无论哪种方法都是基于帧耗时来做判断。这里阐述下hummer计算帧耗时的方式。

图片

在hummer引擎里可以获取每一帧在整个渲染周期的中关键时间点,分别为vsync时间点,ui线程的开始结束时间点和raster线程的开始结束时间点,此处的raster结束时间点表示渲染任务在flutter侧已经结束,后面会交给系统的窗口合成器,因为系统的窗口合成器是严格按照vsync去调度,无论raster_end在vsync_2和3中间的哪个位置都会在Vsync_3上屏,所以我们认为这个时间点就是display_time,前后帧的display_time的差值就是每一帧耗时,通过对每一帧的耗时做一定的分析来判定是否有卡顿发生。


此外,使用当前帧raster_end和vsync的差值或者前后帧的raster_end的差值来判定卡顿都有做过尝试和测试,但在实际的测试中都出现了令人不满意的误差。

hummer方案的优势

  1. 准确性高:虽然不如在系统侧获取上屏时间来判定卡顿的准确但是实际测试中差异很小,并且这种方案是在应用内判定卡顿的最准方式,且由于没有系统侧double buffer或者triple buffer的缓冲,引擎内的检测还可以发现一些可能被掩盖的潜在问题。

  2. 停顿和卡顿的区分准确:hummer引擎中可以准确知道帧耗时的的各个时间点,可以准确的区分卡顿和停顿。Perfdog等线下卡顿判定方案都会去使用dump surfaceFlinger的方法去计算每一帧间隔,通过间隔来判断。这样的方法有个缺陷就是无法分清楚当两帧间隔过大时,是卡顿了还是停顿了,因为给出的时间戳都是上屏的时间戳没有给出每一帧开始渲染的时间点。在实际的测试过程中发现,当有间断的刷帧时,Perfdog都会把间断停顿的刷帧全部错误的判定为卡顿。

  3. 混合开发中可以排除native侧的干扰:如果在native和flutter的混合页面,当native内容以较高的帧率刷新,而flutter内容以较低的帧率刷新,perfdog这类检测工具是无法判定到卡顿,因为他们是从Android系统侧获取数据,而hummer的方案是从引擎侧获取到数据,更为关注flutter内发生的问题。

  4. 丰富的卡顿信息辅助定位问题:在引擎中判定卡顿可以深度获取各种卡顿信息去辅助定位卡顿问题。

定位卡顿

在准确判定卡顿后,如何提供详细的卡顿信息去定位卡顿问题就变得尤为重要。

旧版高可用卡顿信息

hummer方案

图片

图片

从上表可以看出高可用的旧方案在提供卡顿信息的相对较为简单,只能告诉业务存在卡顿无法定位问题,在跟业务同学沟通之后,新的方案会在卡顿之后会上报详细的卡顿信息。

  1. 卡顿时间:单次卡顿的时长,可以衡量卡顿的严重性

  2. 不卡顿时长:距离上次卡顿的时长,可以反映这个页面的卡顿连续性

  3. ui耗时和raster耗时:卡顿是这两个线程各自的耗时

  4. 卡顿率:卡顿率 = 卡顿时长/不卡顿时长

  5. 电影帧:本次卡顿的时长等于多少个电影帧,方便开发同学筛选不同程度的卡顿

  6. 卡顿trace:在发现卡顿后,为了准确分析卡顿问题,从app中获取到卡顿帧的trace信息,提供给业务同学分析,方便线下定位问题。

卡顿trace

卡顿堆栈与卡顿trace的抉择:最初在讨论如何提供卡顿的辅助信息时,主要有两个方案,卡顿堆栈和卡顿trace,卡顿堆栈存在准确性欠佳的问题,因为肯定是先有卡顿发生才能判定为卡顿,当判定为卡顿时再去抓堆栈,堆栈已经发生了变化,如果采用按一定频率抓堆栈的方式,性能影响肯定较大。不过卡顿堆栈的优势就是细密度很高每个函数都会记录,不像卡顿trace一样需要打点。卡顿trace(时间段信息)相对以卡顿堆栈(时间点信息)是一个二维的信息,记录一段时间内的调用,能在判定为卡顿后把之前卡顿信息准确捕捉,并且flutter 原本的trace方案在性能上存在很大的优化空间,trace打点灵活还可以根据开发同学的需求人为的增加或减少打点,所以最终选择了卡顿trace。

hummer的卡顿trace基于flutter原本的trace方案做了改造,同时结合Google的perfetto去展示最后的trace信息,下面简单的介绍一下卡顿trace的流程:

图片

  1. 在flutter的engine和framework层,实时地截取trace信息并缓存,当发现卡顿时上报当前的trace信息到高可用插件

  2. 高可用插件做信息整合,然后上报到平台

  3. 平台会做卡顿trace的解析,利用Google的perfecto UI去呈现卡顿trace


在性能损耗上,hummer的卡顿trace,相比官方没有频繁的同步锁和文件落盘,只是一些单纯的内存读写操作,且只缓存两帧的trace信息,对内存的负担也很小。下图便是根据线上实际灰度等到的卡顿信息。图片

上图可以看出,卡顿问题出现在了图中的红线部分,开发同学可以根据对应业务的代码,在分析调用函数后,定位到相应的函数,例如上图中StoreConnector<xxxx>,可以根据这个函数,熟悉代码的业务同学可以直接定位到下图。从而发现问题是出现在获取stream的snapshot的过程中,然后开发同学就可以对红框中的代码进行优化或做近一步的验证。

图片

性能损耗

卡顿trace的开启对app一定存在性能的损耗,目前较为关注的性能指标就是首屏和帧率,首屏目前不属于流畅度关注的范围,所以卡顿trace在首屏的时候会自动停止记录。而针对fps的性能损耗,用实际的业务在线下实验室做了测试。

图片图片


上面中蓝色开启卡顿trace,绿色关闭卡顿trace,两个图片是针对不同的实际业务的实验室测试结果,可以看出性能损耗极低,在fps上无法检测出来。

卡顿率stutter

hummer的高可用增加了卡顿率这个指标,卡顿率是一种从时间占比的角度去衡量流畅度的指标,相对于fps和卡顿,它的通用性更强,剔除了各种环境因素,例如刷新率,场景等。不同的业务可以通过卡顿率直接横向对比。卡顿率 = 卡顿的总时长/测试总时长。

四、结尾

UC 内核团队,专注渲染引擎 & 虚拟机技术十数年。作为阿里巴巴集团经济体共建 Flutter 的重要参与方,积极拥抱社区,力求给业务带来最大化的价值提升。Hummer 是我们深度定制优化的 Flutter 引擎,融合了团队在 Web 引擎上的多年技术沉淀。欢迎从事相关技术研究或基于 Flutter 构建应用的同学提出宝贵的意见或建议。


五、参考引用

腾讯perfdog:https://perfdog.qq.com/article_detailid=10162&issue_id=0&plat_id=1

fps计算原理:https://juejin.cn/post/6844904182898524168

字节GMTC分享:https://www.modb.pro/doc/39562

美团GMTC分享:https://www.modb.pro/doc/39606

微信扫一扫
关注该公众号

继续滑动看下一个
U4内核技术
向上滑动看下一个