移动性能测试 FPS 计算方法的比较

fenfenzhong · 发布于 2016年04月15日 · 最后由 carl 回复于 2017年06月26日 · 2822 次阅读
本帖已被设为精华帖!

忙于发版,又有些天没有上社区贡献帖子了,表示很惭愧,但是突然加了个类似git contribution的timeline,让我觉得很紧张。。。这么零星感觉不太能忍

引言

之前写过两篇帖子,是Android UI呈现系列的,里面介绍了一些UI测试方法和UI组成的基本概念,比如window,frame,垂直同步这些,都被加了精,还推了微信,这里再次感谢社区。
理解这些UI结构对测试,或者开发,甚至设计师都是非常有好处的,抛开所有业务,UI是用户最大的痛点,每帧画面的显示,jank都最直接的影响用户体验,UI系列的帖子二最后我给出了一个FPS的报表,帖子一里最前面也有说参考了一个库:
https://github.com/ChromiumWebApps/chromium/blob/master/build/android/pylib/perf/surface_stats_collector.py
但并没有把具体实现给出来,怎么拿到数据,回复下面有挺多人关心,今天就给大家介绍一下我的摸索路径

0x00

知道要统计FPS,于是上社区找找有没有前辈已经做过,google一下市面上用的方法,发现社区提到了那个库,市面上的查询结果涉及到几个关键字:gfxinfo,systrace
这个过程中,得去了解surface,surfaceFlinger等等概念,不然进行不下去
首先不要问为什么,先去读那个库的代码,读懂,多读几遍
读完就会发现几个事情:

  1. 社区里的帖子可能比较老了,现在这个库的代码有变化,类的构造方法,或者里面有些方法的参数已经变了
  2. 执行adb shell dumpsys SurfaceFlinger --latency + <window名> ,完整会输出128行信息,但第一行代表refresh_period,所以它其实只表示127帧的渲染情况
  3. 这127帧的渲染数据中,介绍是说三列代表不同的意义,但事实上在有的手机上第一列和第三列是一样的(不重要,反正我们用到的是第二列,也就是垂直脉冲到来时的时间戳)
  4. 库里面的一些限制条件,比如pause_threshold,pending_fence_timestamp,有什么用
  5. 在了解垂直同步机制之后,就会想,只要时间间隔差出现大于刷新周期的情况的话,就是一次jank,加起来就是总的jank_count,按照Android VSync机制,既然中间一列代表该帧开始加载时垂直脉冲到来的时间,【每2帧之间的时间间隔】必然是refresh_period的整数倍,应该只要 【每2帧之间的时间间隔】 大于 refresh_period 就代表有jank啊,为什么还要调用_GetNormalizedDeltas函数,用【每2帧之间的时间间隔差】来计算,这个值有什么意义?
  6. 最后fps的计算其实跟jank没有关系,因为计算方法是 int(round((frame_count - 1) / seconds)), 'fps'),即总帧数除以总时间

0x01

在把这个库通过改造,集成到自己的工具中时(经过一个time_interval就执行一次命令),因为有上面那些疑问,所以稍作了修改,但最核心的zip比较没变
得出来的一些序列如下(格式同 帧总数-掉帧数-FPS ):

start collecting...
---------------------
current window:com.miui.home/com.miui.home.launcher.Launcher
surface fps info:0-0-0
---------------------
current window:activity.SplashActivity
surface fps info:31-2-17
---------------------
current window:MainActivity
surface fps info:127-13-38
---------------------
current window:MainActivity
surface fps info:127-6-55
---------------------
current window:MainActivity
surface fps info:127-8-46
---------------------
current window:MainActivity
surface fps info:127-7-44
---------------------
current window:MainActivity
surface fps info:127-7-46
---------------------
current window:MainActivity
surface fps info:127-6-43
---------------------
current window:MainActivity
surface fps info:127-8-48
---------------------
current window:MainActivity
surface fps info:127-9-45
---------------------
current window:MainActivity
surface fps info:127-9-48
---------------------
current window:MainActivity
surface fps info:127-7-41
---------------------
current window:MainActivity
surface fps info:127-5-32
---------------------
current window:MainActivity
surface fps info:127-7-39
---------------------
current window:MainActivity
surface fps info:127-7-39
---------------------
current window:MainActivity
surface fps info:127-7-39
---------------------
current window:MainActivity
surface fps info:127-7-39
---------------------
current window:MainActivity
surface fps info:127-7-39
---------------------
current window:MainActivity
surface fps info:127-7-39
---------------------
current window:MainActivity
surface fps info:127-8-29
---------------------
current window:MainActivity
surface fps info:127-11-32
---------------------
current window:MainActivity
surface fps info:127-12-36
---------------------
current window:MainActivity
surface fps info:127-7-39

这是一段连贯的日志,大家可以看到fps几乎没有上50的,更别说60这个标准了。
但并不是这个app就做的不好,我在这次操作过程中没有觉得卡顿,listview滑动顺畅,但是一个有图的地方,是之后再渲染出来的,我把开发者模式中的GPU渲染打开,在屏幕上以柱状图显示的时候,被测应用每帧渲染情况,只有两三根柱形图超过了16ms,所以总体来说应该是达标的
所以我又发现了如下几个问题:

  1. FPS统计应该是一个连续,流畅的过程,但这种方法如果我保持屏幕不滑动,它会一直输出上一次的信息,比如日志里的127-7-39这一部分
  2. 这种统计方式导致FPS偏小
  3. 在以往的测试数据中,竟然发现了127-0-45 这种数据,觉得这不合理,既然一帧都没有掉,为什么fps还是不能达到60,所以开始怀疑这种用法

0x02

有问题就得找另外的解决方法,上面也说了,除了SurfaceFlinger,还有gfxinfo,期间还问过了@monkey大神,他潇洒的回了我:gfxinfo
而且,Android开发者模式里,GPU呈现模式分析里,柱状图就是根据gfxinfo里的数据来绘制的
执行 adb shell dumpsys gfxinfo + package名,(可能为空,需滑动后才能看到渲染数据),得到的结果如下:

Draw    Process Execute
7.29    31.04   0.88
2.99    1.19    0.46
0.47    0.71    0.87
0.47    22.04   0.54
0.54    2.50    0.64
0.70    2.24    0.92
0.56    2.04    0.78
0.67    3.09    0.48
0.73    2.40    2.45
0.57    4.57    0.44
2.07    1.87    0.47
0.80    13.79   0.53
0.79    2.18    3.00
0.98    2.31    2.26
0.86    2.19    2.43
0.90    2.11    4.25
0.86    1.92    4.45
0.92    3.29    6.17
0.91    5.53    0.71

注意:一帧的渲染不应该超过16.67这个标准,垂直同步机制的应用,

  1. 没有超过的,也会当16.67来算,下一帧必须要等到垂直脉冲来了才开始渲染
  2. 超过了的,则会打乱原有的秩序,出现一次jank,但耗时会是垂直脉冲的整数倍,如果出现了一次jank,可能值耗了2个脉冲,也可能耗了十几个,不确定
  3. 执行gfx的命令,如果收集完整的话,会有128帧的数据,不同于SurfaceFlinger(只有127)

DrawProcessExecute 下面分别对应的时间,具体代表的意义可参加官网

  • 这128帧,其实是与时间没什么关系的,它仅仅代表最近128帧的渲染情况,它可能用了几秒,也可能用了十几秒,与时间无关
  • 因为与时间无关,所以计算时不能用 帧总数/总时间,总时间 ≠ 每帧具体时间相加(因为实际上是以垂直脉冲来计算的)

那我怎么算FPS呢?。。下面是我的代码和注释,贴给大家

results = run_unblock_command(fps_command)
frames = [x for x in results.split('\n') if validator(x)]
frame_count = len(frames)
jank_count = 0
vsync_overtime = 0
for frame in frames:
    time_block = re.split(r'\s+',frame.strip())
    if len(time_block) == 3:
        try:
            render_time = float(time_block[0]) + float(time_block[1]) + float(time_block[2])
        except Exception, e:
            render_time = 0

    '''
    当渲染时间大于16.67,按照垂直同步机制,该帧就已经渲染超时
    那么,如果它正好是16.67的整数倍,比如66.68,则它花费了4个垂直同步脉冲,减去本身需要一个,则超时3个
    如果它不是16.67的整数倍,比如67,那么它花费的垂直同步脉冲应向上取整,即5个,减去本身需要一个,即超时4个,可直接算向下取整

    最后的计算方法思路:
    执行一次命令,总共收集到了m帧(理想情况下m=128),但是这m帧里面有些帧渲染超过了16.67毫秒,算一次jank,一旦jank,
    需要用掉额外的垂直同步脉冲。其他的就算没有超过16.67,也按一个脉冲时间来算(理想情况下,一个脉冲就可以渲染完一帧)

    所以FPS的算法可以变为:
    m / (m + 额外的垂直同步脉冲) * 60
    '''
    if render_time > 16.67:
        jank_count += 1
        if render_time % 16.67 == 0 :
            vsync_overtime += int(render_time / 16.67) - 1
        else:
            vsync_overtime += int(render_time / 16.67)

fps = int(frame_count * 60 / (frame_count + vsync_overtime))

return (frame_count,jank_count,fps)

既然没有时间概念,我就用 实际的帧数/本来应该在这个时间内渲染完成的帧数 ,得到这个比例再乘以 60 这个标准
我把两种方法收集到的数据放在一起,如下:

start collecting...
---------------------
current window:com.miui.home/com.miui.home.launcher.Launcher
gfx fps info:0-0-0
surface fps info:0-0-0
---------------------
current window:activity.SplashActivity
gfx fps info:30-3-47
surface fps info:30-0-15
---------------------
current window:MainActivity
gfx fps info:128-5-57
surface fps info:127-13-46
---------------------
current window:MainActivity
gfx fps info:128-4-58
surface fps info:127-7-52
---------------------
current window:MainActivity
gfx fps info:128-5-57
surface fps info:127-8-52
---------------------
current window:MainActivity
gfx fps info:128-7-56
surface fps info:127-9-48
---------------------
current window:MainActivity
gfx fps info:128-1-59
surface fps info:127-7-54
---------------------
current window:MainActivity
gfx fps info:128-7-56
surface fps info:127-11-43
---------------------
current window:MainActivity
gfx fps info:128-6-56
surface fps info:127-10-48
---------------------
current window:MainActivity
gfx fps info:128-9-55
surface fps info:127-16-48
---------------------
current window:MainActivity
gfx fps info:128-3-58
surface fps info:127-4-57
---------------------
current window:MainActivity
gfx fps info:128-6-57
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------
current window:MainActivity
gfx fps info:0-0-0
surface fps info:127-11-28
---------------------

从上面的日志又可以看出几个问题:

  1. 用gfx统计的确实FPS会比较高,比较接近真机上柱状图的展示
  2. gfx方法比SurfaceFlinger的要稳定
  3. gfx如果一段时间不滑动,就会没有数据,我这里返回的是0-0-0,SurfaceFlinger则会一直保持输出

0x03

经过多次的试验,问题排查,决定用gfxinfo收集FPS,当然这是为了数据存储需要,如果是开发工程师,最有效直观的测试一段代码有没有影响FPS,打开GPU呈现模式,在屏幕上以柱状图显示就好了(btw,真机和模拟器上差别也非常大,为了还原用户体验最好用真机)

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 35 条回复
982

我自己是写了个shel脚本分析dumpsys SurfaceFlinger --latency + 获取的数据,其中遇到两个坑:
1、第二列垂直同步时间会刷9223372036854775807
2、存在相同帧重复利用的情况,同一帧先后间隔被用两次。比如微信红包弹出效果,就频繁出现重用的场景。

马上要用上机械臂来测试性能响应和帧率,脚本方案被我搁置一旁了,后续有时间我也拿出来写写。
我写的shell脚本是直接手机端后台取数据,既可以实时输出到控制台查看,也可以后台自动记录为csv,后续csv转换出图展示。

209

FPS的的高低和界面卡顿并不是成正比,android上的FPS计算是依赖于用户交互,并不是说FPS低界面就一定不流畅,所以我个人认为用FPS去判定界面的流程性是不太严禁的,建议直接计算每一个VSYNC的丢帧率,即1s当中去计算(丢帧数/60),掉帧率越低说明界面越流畅。

5493

#1楼 @sandman 请问一下,你说的同一帧先后间隔被用两次,是它的vsync时间戳一样,但出现过两次吗?这种情况我还真没碰见过

5493

#2楼 @vigossjjj 我觉得FPS的高低和界面是否卡顿就是成正比的,只不过Android系统60的FPS标准是比较高的,而人眼只要FPS大于等于30就基本分辨不出是一帧帧图片还是连续的动画了。jank 数量确实不能反映FPS,因为一次jank可能延搁的vsync数不一定,但 jankness 还是比较有说服力的

5493

#2楼 @vigossjjj 所以我最后采用的是 m / (m + 额外的垂直同步脉冲) * 60, 这种方法其实就是(1-jankness)* 60 的换算

209

#5楼 @fenfenzhong 你可以用我说的方法尝试一下,“FPS不一定和界面卡顿成正比”这个我是论证过的,你可以再验证一下

5493

#6楼 @vigossjjj 恩,好的,我会再去试试

982

#3楼 @fenfenzhong 是的,第二列同步时间相同,出现两次,中间还间隔几帧。会导致依次做减法出现负数。
我也是在监控红包弹出框时发现的,你试下红包的弹出效果,高概率出现。就是抖动的动效会有相同帧被复用的情况。

982

#2楼 @vigossjjj 我认为这是综合评定的,我自己设计评价规则时,定了个规则。
满足帧率kpi占50%,满足kpi的两帧间隔帧数/总帧数占40%,帧间隔kpi/最大帧间隔占10%
流畅度得分(%)=(FPS/目标FPS)*50+(KPI/最大帧间隔)*10+(1-超kpi帧数/总帧数)*40。

丢帧率代表持续卡顿,最大帧间隔代表最大卡顿持续时长。

8617

楼主知道iOS怎么测试WEBVIEW 的 FPS吗?

1181

如果某个场景可以稳定在39~41帧之间,效果要比50~60帧变动的场景好

5493

#12楼 @sziitash 39~41 这么精确的值?还是你的意思是稳定的低fps比跳跃的高fps,页面效果更好?

982

我的看法:帧率只是一方面,稳定帧率不代表不卡,丢帧比例和最大单帧渲染时间也很重要。
如果帧率稳定,所有帧的渲染时间差距不大,自然可以算流畅,因为没有忽快忽慢引起用户关注。
如果丢帧比例少,且丢帧时渲染时间远超正常渲染时间,就会很明显被用户察觉到差异。

1181

#13楼 @fenfenzhong 对,我就是这个意思。稳定的低fps比跳跃的高fps效果更好。
当然太低也不行,貌似24帧是个底线。

96

这个版本是不是不能获取游戏的FPS,有没有什么好办法可以获取到游戏的FPS?

5493

#14楼 @sandman 我明白你的意思了,你是说如果在一段时间内,某一帧相对其他帧渲染时间有明显的增加,这种“单帧突出”的问题会更影响流畅性,即突出的差异比普遍的稳定更能被用户感知

我和你的想法吻合,觉得不能以掉帧数来计算,所以我的计算方法是:如果某一帧超时了,先计算它到底占用了多少个脉冲,不是仅仅+1(这个1代表掉帧数),比如占用了10个,其实应该+10(这个10代表脉冲数)。我最后算的不是掉帧数,而是计算 (在一段时间内已渲染的帧数 / 这段时间内应该渲染的帧数) 这个比例(其实就等于 1 - 丢帧比例),然后用这个比例去乘以60。

这种方法,我认为“不以掉帧数来计算”这个方向是对的,但依然有问题,比如128帧中有127帧都正常,只有1帧超时,它用掉了额外的10个脉冲,这时我的方法计算出来的FPS应为 : 128 / (128 + 10) * 60 = 56,这个值也算很高了,这个结果很容易让人以为它是正常的,从而忽略掉了那个“单帧突出”的问题。到这里我也明白了 @vigossjjj 说的 “FPS的的高低和界面卡顿并不是成正比”了

多谢二位 @sandman @vigossjjj

更科学的计算方法应该是时刻检测在某些区间内的丢帧比例以及最大单帧渲染时间,去发现“突出”的问题,但这个怎么应用到代码中,我还需要再研究一下

982

#17楼 @fenfenzhong
我的方案是实时监控和后台长时间监控并存,观察数据变化。下图是监控的数据图展示,每段采样数据信息都有,单帧渲染最大时间也有,时间戳可以对应log时间点

6859

#18楼 @sandman 浮云大牛,开个贴子给大伙follow下呗。

8703

其实流畅度和FPS应该是两个概念

5493

#20楼 @r551 不能完全区分开来,是会互相影响的,要结合场景,观察某个时间区间内FPS值的变化

2863

楼主有联系方式吗 留个QQ勾搭一下~!

2405

(112, 65, 27), (8, 5, 26)
我用你的算法出来这样的值,感觉不像是正常的啊@fenfenzhong

5493

#23楼 @finelucky 为什么不正常呢?你说一下你的理解?

114

棒~最近我也在看UI流畅度这一块。
我觉得FPS只是在评定UI流畅过程中的一个参考值。
这跟用户操作快慢、场景转换快慢、请求响应快慢都有关。
所以FPS确实跟是否流程是不成正比的。
可以把界面绘制速度也考虑进来,综合去考察卡顿。

PS:之前看android的源码,其实开发者模式里的GPU呈现模式,就是不断地执行gfx的command,然后把得到的数据相加然后展示,然后继续执行,继续展示。所以LZ用gfx的方式确实更加统一一点。

96

楼主,刚试了你的算法,有个疑问,我停在一个页面,然后不断执行命令,发现gfx总会有数据,不为0;但是如果我滑动一下,然后再静止,gfx就是你说的没有数据了,不知道为啥前者会一直有数据呢?

4937

楼主的代码可以share吗?:)

110

为啥不直接在 5+ 手机上用 gfxinfo 呢?

118

#29楼 @lihuazhang 额。为啥一定要5+,之前也可以啊。。= =

110

#30楼 @monkey 以前没有统计信息

118

#31楼 @lihuazhang 统计啥信息。。。你说终端上的数据?也有的。。不过5的确多了很多信息

96

针对游戏项目做了下试验 :
1、使用gfxinfo 只有在调用android原生组件时才能获取到数据,其余时间根本获取不到。
2、使用dumpsys SurfaceFlinger --latency 跟gfxinfo出现同样情况
3、dumpsys使用 SurfaceFlinger --latency SurfaceView 才能打印出数据(在Android N版本上不能获取数据)

96

#17楼 @fenfenzhong 楼主有新发现了吗?目前帧率用什么方法测试了?

—— 来自TesterHome官方 安卓客户端

2562
110Lihuazhang 回复

我想请问一下这个图片中的Total frames,Janky frames都代表啥,我想计算fps,哪个方法最可靠啊?为毛一堆方法比较之后结果是用真机看GPU
谢谢

6504 lose android 端取 cpu,fps,men,wifi/gprs 流量等值 中提及了此贴 07月27日 17:52
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册