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

fenfenzhong · 2016年04月15日 · 最后由 morou 回复于 2024年04月08日 · 7212 次阅读
本帖已被设为精华帖!

忙于发版,又有些天没有上社区贡献帖子了,表示很惭愧,但是突然加了个类似 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,真机和模拟器上差别也非常大,为了还原用户体验最好用真机)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

多谢二位 @sandman @vigossjjj

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

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

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

匿名 #20 · 2016年04月22日

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

恒温 回复

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

测试小书童 android 端取 cpu,fps,men,wifi/gprs 流量等值 中提及了此贴 07月27日 17:52
testWalker 回复

请问,后来你这个问题解决了嘛?

File "D:\Python\Python36\lib\site-packages\decorator.py", line 207, in create
name, rest = obj.strip().split('(', 1)
ValueError: not enough values to unpack (expected 2, got 1)
请教下各位大神 运行代码,这是哪里出问题了

花开 Android FPS 方法探讨 中提及了此贴 10月25日 02:20

这种方法,没法去掉重复帧吧?


既然有这两个值,那不是每秒获取一次,然后计算差值就可以了吗?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册