忙于发版,又有些天没有上社区贡献帖子了,表示很惭愧,但是突然加了个类似 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 下面分别对应的时间,具体代表的意义可参加官网

那我怎么算 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,真机和模拟器上差别也非常大,为了还原用户体验最好用真机)


↙↙↙阅读原文可查看相关链接,并与作者交流