背景
目前可以监控应用fps及jank数据的产品在用的有perfdog和itest;但perfdog已经开始收费,而itest无法监控到(3D)游戏的数据;
因此需要自研一款可以满足业务需求的jank数据检测工具。
业务需求
- 数据监控范围:fps数据、jank数据、bigjank数据
- 展示:图表形式实时展示数据、每秒截图在图表上显示
- 操作:低门槛图形化操作
- 实时性:数据秒级检测
- 准确性:以perfdog为参考,数据尽量靠拢
- 监控对象类型:普通应用+游戏
实现过程
帧数据获取方案选型
Choreographer.FrameCallback获取数据
原理
在 Android 系统中,实现绘制的类叫Choreographer;一次屏幕刷新完成后,将产生 VSync 信号并通知 Choreographer。
Choreographer 收到通知依次处理 Input、Animation、Draw,这三个过程都是通过 FrameCallback 回调的方式完成的。
而 FrameCallback#doFrame(long frameTimeNanos) 方法中可以得到 VSync 到来的时间戳,这样就能得到连续两帧开始渲染之间的间隔,将该值近似作为上一帧的渲染耗时。
实现 FrameCallback 接口,并通过 Choreographer#postFrameCallback() 方法将其跟 Input、Animation、Draw 这些回调一起塞入主线程的消息队列,就能源源不断的获取每一帧的渲染时间戳,每一个 VSync 的时间戳代表一帧,这样可以得到某段时间内渲染完成的帧数,二者相除即可得到帧率。
优点
- 系统函数的方式获取值,起源于 Facebook 在 DroidCon 的分享,数据准确没有争议。
- 基于该原理有已实现的开源工具fpsview
缺点
- 该方案需要集成在项目源码内使用,存在使用门槛。
- 因业务需求,部分指标需要靠拢perfdog,需要对fpsviewer进行二次开发。
- 该方案为移动端方案,难以实现截图展示。
gfxinfo获取数据
原理
通过adb命令
adb shell dumpsys gfxinfo < PACKAGE_NAME >
可以获取到android系统的最近127帧信息(各系统不同可能会有差异),类似如下样式:
* 结果样式:
* XXXX/XXXX.AAA/VVVV (visibility=0) 对应Activity
* Draw Prepare Process Execute
* 7.31 5.07 6.63 0.99
* 50.00 21.64 44.89 6.06
* 50.00 10.52 6.58 2.79
* 20.21 2.36 6.78 2.51
* 33.46 0.62 13.44 1.24
* 10.30 0.21 6.07 1.55
* 50.00 11.42 10.51 3.61
* 0.84 7.79 15.48 32.71
* 7.56 0.80 11.23 1.56
* 46.13 2.58 7.29 1.16
* 50.00 3.66 12.06 1.49
* 12.26 0.31 5.29 0.84
* 2.97 1.14 8.17 1.62
* 6.26 0.84 9.47 2.72
* ......
*/
Draw: 表示在Java中创建显示列表部分中,OnDraw()方法占用的时间;
Prepare: 准备时间;
Process: 表示渲染引擎执行显示列表所花的时间,view越多,时间就越长;
Execute: 表示把一帧数据发送到屏幕上排版显示实际花费的时间,其实是实际显示帧数据的后台缓存区与前台缓冲区交换后并将前台缓冲区的内容显示到屏幕上的时间。
将上面的四个时间加起来就是绘制一帧所需要的时间。
优点
- Google官方接口,数据准确可信
- 通过shell命令的形式获取数据,可扩展性强
缺点
- 不支持surface view数据的获取
SurfaceFlinger获取数据
原理
通过adb命令
adb shell dumpsys SurfaceFlinger --latency < VIEW_NAME >
可以获取到android系统的最近127帧信息(各系统不同可能会有差异),类似如下样式:
53476438728 53483331194 53476438728
53774334579 53783331182 53774334579
53804473320 53833331180 53804473320
53821433876 53849997846 53821433876
54482172942 54499997820 54482172942
62828275267 62849997486 62828275267
77744212604 77749996890 77744212604
137676463526 137683327826 137676463526
197665365491 197683325426 197665365491
257656215141 257666656360 257656215141
317667889815 317666653960 317667889815
377658368227 377666651560 377658368227
437659404105 437666649160 437659404105
497680028798 497683313426 497680028798
557661828695 557666644360 557661828695
617669142813 617683308626 617669142813
677664261743 677683306226 677664261743
...
第一列t1: when the app started to draw (开始绘制图像的瞬时时间);
第二列t2: the vsync immediately preceding SF submitting the frame to the h/w (VSYNC信令将软件SF帧传递给硬件HW之前的垂直同步时间),也就是对应上面所说的软件Vsync;
第三列t3: timestamp immediately after SF submitted that frame to the h/w (SF将帧传递给HW的瞬时时间,及完成绘制的瞬时时间)。
将第i行和第i-1行t2相减,即可得到第i帧的绘制耗时。
优点
- 可获取view数据类型多,兼容游戏和视频场景
- 通过shell命令的形式获取数据,可扩展性强
缺点
- 多view时,需手动汇总数据
- 数据统计口径和Google的gfxinfo存在差异,理论上gfxinfo的数据更正式
选型结论:SurfaceFlinger作为数据获取方案
综上各方案原理和优缺点,集合业务本身的应用场景(普通应用+游戏),符合全部业务需求的方案只有SurfaceFlinger;
以下工具开发实现过程将基于SurfaceFlinger方案描述。
帧数据加工处理
帧数据获取和处理流程
由于通过SurfaceFlinger方案获取的数据,如果在屏幕不动时,将始终获取到最后一次画面的值;因此,需要每间隔一段时间,执行一次clear操作,整体流程如下:
adb shell dumpsys SurfaceFlinger --latency-clear 执行数据清零 ------>
等待一秒,等待新数据生成 ------>
adb shell dumpsys SurfaceFlinger --latency < VIEW_NAME > 执行获取数据 ------>
计算数据,传输至前端展示 ------>
循环以上..
fps数据计算
每次执行dumpsys SurfaceFlinger --latency < VIEW_NAME > 后,计算汇总出一个fps;
计算规则为:
frame的总数N:127行中的非0行数
绘制的时间T:设t=当前行t2 - 上一行的t2,求出所有行的和∑t
fps=N/T
jank、bigjank数据计算
出于业务方要求,jank和bigjank采用腾讯PerfDog的计算方式:
PerfDog Jank计算方法:
同时满足两条件,则认为是一次卡顿Jank.
①Display FrameTime>前三帧平均耗时2倍。
②Display FrameTime>两帧电影帧耗时 (1000ms/24*2≈83.33ms)。
同时满足两条件,则认为是一次严重卡顿BigJank.
①Display FrameTime >前三帧平均耗时2倍。
②Display FrameTime >三帧电影帧耗时(1000ms/24*3=125ms)。
多view数据聚合处理
在实际的数据获取过程中,发现一个应用会同时存在多个surfaceview和view;因此,如果只针对topactivity获取数据,并不能保证一定存在数据;在实际开发中,通过同时获取所有surfaceview&view的数据后进行数据聚合处理来规避这种问题。
具体的方式是:
- 通过adb shell dumpsys SurfaceFlinger | grep {packname}获取当前所有view添加进列表
- 循环该列表,获取所有view的帧数据
- 对每个view的帧数据进行处理,提取出fps、jank、bigjank数据
- 根据展示所有问题的原则,将所有view的jank和bigjank数据相加进行最终展示;将各view的fps相加(为0则不处理)取平均值后进行展示
数据展示实现
前端展示方案
整体前端展示以独立前端项目的形式,采用react实现,ui组件使用ant design,其中图表组件使用ant charts。
后端数据获取&处理方案
整体后端数据获取&处理以独立后端项目的形式,采用Django实现。
截图方案
由于原始的adb截图速度太慢,不满足需求,因此截图方案采用hook系统的函数实现;采用java编写,以jar包的形式放在sdcard目录下,通过
export CLASSPATH=/sdcard/shot.jar;exec app_process /sdcard Shot /sdcard/{shot_name}
的形式调用。
数据传输方案
数据传输分为两个方案:
一次性的数据沟通,如:获取设备信息、获取应用app列表、开始/结束测试等,使用http协议进行;
连续的数据传输,如:监控数据传输,出于性能考量,使用websocket进行。
项目本地化方案
由于Django+React的形式使用起来较为麻烦,需要各自进行服务部署;因此这里采用本地化包装的形式,具体流程如下:
- 将react项目build后,产物放置于Django项目下;这样前端项目可以免部署使用了;
- 将Django项目采用pyinstaller打包成可独立执行的windows程序;这样可以使用命令行在任一windows机器上无需部署直接启动Django;
- 采用pyqt编写项目启动器作为项目的启动入口,使用pyqt的web组件自动加载后端网址,实现一键启动。
项目展示
工具初始化界面
工具选择应用界面
工具录制和展示数据界面
引用文档
Android屏幕刷新机制
Android屏幕刷新机制—VSync、Choreographer 全面理解
fpsviewer—实时显示fps,监控Android卡顿的可视化工具
android帧的绘制过程以及fps的获取
Android UI性能测试——使用 Gfxinfo 衡量性能
shell 脚本通过 dumpsys SurfaceFlinger --latency 数据计算 FPS 和评价流畅度
PerfDog客户端> Jank卡顿及stutter卡顿率说明
App性能测试揭秘(Android篇)