目前,许多团队的微信小程序都在自动化测试的探索,本文从自动化测试过程中产生的数据进一步分析,得出相关的性能指标,为后续的性能优化做数据支持。
一、自动化测试框架选择
对于微信小程序的测试框架的选择问题上,个人觉得仅仅在于是否使用FAutoTest,这个由腾讯TBS出品的测试框架。该框架对基于TBS内核应用的测试上是非常友好的,但该框架的优势仅仅停留在在早期的小程序版本,其内核能切换至Webview。通过实践发现,最新的小程序版本已经无法切换,因此嫡亲的FAutoTest框架已无明显优势。
转为选择业内比较流行的测试框架。目前,业内的测试框架大致分为两大类,一类是传统的自动化测试框架,如Appium、Macaca(阿里);另外一类是基于UI视觉的自动化测试框架,如SikuliX、Airtest(网易)。两者的实现差别如下表:

对于传统的测试框架,实现基本依赖于在客户端安装一个能执行选择或者交互行为的APP,例如在IOS端会安装XCUITest。而基于UI视觉的方案是基于STF框架中的工具,如视频流minicap,minitouch工具等,这些工具是用NDK开发的,只需要向客户端上传对应编译好的文件,后台运行即可。
从两者的实践对比结果如下:

由于传统的测试框架是基于CS框架,通信交换耗时相对较长。而基于UI视觉的底层框架是基于socket的,实时性较高,再结合与高效的图像diff算法,能快速响应。
二、采集监测框架设计

整体的采集监测框架分为采集部分和统计分析部分。在采集部分中,本方案使用了Appium作为自动化测试框架,结合minicap采集实时快照流数据。图中的小程序rpc server能直接将自动化脚本的运行环境与小程序的运行环境连接起来,能提供更多的native能力,如元素的选择等。在自动化脚本中,提供了minicap和小程序性能面板的控制,如在QA流程case中自动打开性能面板和导出traceLog文件等操作,以及集成一些性能case,如每个页面的滚动操作等。在整个QA流程case结束后,会产生快照和TraceLog数据并进行离线存储。在统计分析部分中,首先是对离线文件进行源数据处理,然后根据制定的性能指标进行计算,最后输出统计报表。
二、数据采集
在编写好测试脚本后,即可测试流程,除了正常的流程测试框架的辅助,如断言库等,与此同时我们还能获取到测试过程中的视频,以及针对微信小程序,我们还能导出其独有的TraceLog,这个在开发者工具中Trace选项卡中可以追踪展现。
2.1 数据类型
本方案中采集到的数据,主要分为以下两块:

2.1.1 快照数据
测试过程中的录屏是能直观反映测试效果,如首屏的填充速度等。这里的快照数据就是一帧视频帧,即一张快照图片。
录屏的方式目前有两种方案,一是通过原生的API,如调用原生的screencap方法。二是从minicap传送回来的stream数据中,实时保存成图片文件。前者的兼容性有问题,在部分华为手机上无法调用,因此推荐使用minicap的方法,将实时传送回来的流数据保存成图片文件。
在minicap的官方example源码中,提供了如何将stream转换成base64的图片数据的方法,有兴趣的可以自己查看。我们只需建立Socket连接实时接收stream流,并对其进行控制处理,包括开关和保存操作。
2.1.2 TraceLog数据
在小程序的开发文档里面已经介绍了,如何打开性能面板以及导出Trace文件。这里需要注意的是,只有Android手机才支持导出功能。
然而,对于自动化的需求来说,手动打开和导出是远远不够,我们要设计自动化的流程来支持。这里主要分为两个步骤:第一,打开性能面板以及在结束的时候导出trace文件;第二,从手机中拉去trace文件保存在本地并删除该文件(防止文件冗余错乱)。
对于第一步的流程case,是比较容易编写的,只需要找准元素位置即可。而且,打开开发版本的小程序是一劳永逸的,只需打开一次,之后无需再次操作。比较麻烦的时候第二个步骤,首先,我们需要定位trace文件在手机中的存储位置,除了暴力人肉查找的之外,我们可以通过hack进入源代码发现,这里简单介绍下,首先在开发者工具的console中数据openPlugin()
,执行后会打开对应的目录,找到WeappPlugin/trace目录,该目录就是对应了开发者工具的Trace选项卡的界面功能,搜索index.js文件,全局查找trace等关键字,就能发现到如下保存trace文件的路径:
tencent/MicroMsg/appbrand/trace
很显然该路径是个相对路径,需要获取系统存储路径来补全,最后通过mobile:shell执行adb命令来拉取和删除对应的trace文件,具体的appium代码片段如下:
const AdmZip = require('adm-zip')
const baseDir = await driver.execute('mobile:shell', {
command: 'echo',
args: ['$EXTERNAL_STORAGE']
})
const folder = `${baseDir.replace('\n', '')}/tencent/MicroMsg/appbrand/trace`
const folderBase64 = await driver.pullFolder(folder)
const buffer = Buffer.from(folderBase64, 'base64')
// 拉取的是压缩文件,需要解压缩const zip = new AdmZip(buffer)
for (const entry of zip.getEntries()) {
const decompressedData = zip.readFile(entry)
// 保存 fs.writeFileSync(./traceLog/${entry.entryName}, zip.readAsText(entry), 'utf8')
// 删除远端的文件 await driver.execute('mobile:shell', {
command: 'rm',
args: [${folder}/${entry.entryName}]
})
}
2.2 原数据分析
至此,我们能得到2份原始数据,分别是快照数据(1秒6张以上,不均等分布)和Trace数据文件。接下来,需要对原始数据做简单的处理和展示。
2.2.1 快照数据处理
对于快照数据的处理,主要有如下两个处理点:
A. 时间戳对准
我们将每一次完整的数据流保存成一张图片,图片的命名可以是保存的时间戳(timestamp),这也是方便后续的处理。但这里就存在一个问题,客户端的时间戳和本地的时间戳是存在误差的,为了统一我们应该以客户端的时间戳为准。因此需要在执行前,先将两端的时间戳误差计算出来,方法如下:
const phoneTsStr = await driver.execute('mobile:shell', {
command: 'date',
args: ["'+%s%N'"]
})
const phoneTs = parseInt(phoneTsStr.substr(0, 13), 10)
const localTs = +new Date()
const offsetTs = localTs - phoneTs
注意:有些系统上是date是不支持%N的,可以安装别的插件来处理。
B. 关键帧提取
由于整个测试过程会比较漫长,特别是使用appium的方案的时候,在小程序中的大部分的元素标示缺失,只能通过长长的xpath方法获取,这个检索过程是很耗时的。直接导致了保存下来的快照数据量会很大,且在静止屏等待的情况下,重复的图片会很多。在保存原始数据的情况下,为了直观展示UI变化点,需要对关键图片做提取,做法比较暴力,就是每一帧和前一帧做image-diff操作,如果结果超过设定的阈值,就认为是关键帧。diff的库有不少,这里推荐两款基于像素点对比的图像 Diff库:
uber-archive/image-diff
yahoo/blink-diff
为了加快diff操作,我们针对小程序的场景特殊性,做了一些优化。
由于小程序在case执行过程中是打开性能面板的状态,就会在右上角滞留一块黑色底的看板,这部分的位置和内容几乎不会变动,我们也不需要监测。此外,小程序的布局大部分是从上到下,从左到右的分布。基于此,我们将diff的区域裁剪至偏左侧的一长条(宽度可根据实际情况设定),可大幅提高处理速度。

至于阈值如何设定,需要根据内容的丰富程度和多次实验的结果来制定。
2.2.2 TraceLog数据处理
在小程序开发者工具中,我们能看到trace文件的展现结果,通过追踪,我们能找到WeappTraceFiles目录下的trace文件,但这个文件已经是处理过后,且有过删减的内容。因此我们在参考微信官方处理方案的基础上,解析出符合我们需求的内容。
原始的trace文件是由一条记录一行的保存,每行有4个模块(最后一个某些情况会缺失),且由逗号分隔。如下所示:
// 格式Name, Cat, Type, StartTime, EndTime, Args
// 例如ResourcePrepare, Native, X, 217, 602,
具体的解释如下表:

关于记录的顺序,并不是按照StartTime的大小从小到大排列,而是根据父子方法的调用时间为准,具体来看以下一个案例,该案例是小程序首次启动的时候,准备阶段的记录,结合官方提供的指标定义来看如下片段:
X5Prepare,Native,X,216,217,
// 资源准备耗时,包括下载耗时(初次打开的情况下)和准备耗时ResourcePrepare,Native,X,217,602,
ActivityCreate,Native,X,0,1579,
WebViewInit+PageFrameLoad,Native,X,828,2018,
pageframe,Native,X,2533,2983,
WAWebview,Native,X,1942,2203,
...
App.onLaunch,App,X,4413,4620,
...
App.onShow,App,X,4621,4652,
...
Page.onLoad,pages/index/index,X,6069,6184,
// 首次渲染耗时firstRender,pages/index/index,X,6077,6168,
Page.onShow,pages/index/index,X,6189,6191,
...
// 页面切换耗时PageLoad,Native,X,1241,6218,
...
// 启动耗时startupDone,Native,X,0,6250,%7B+%22appMd5%22%3A+%22016f8777cbb7c3397cb2d61955e5a317%22+%7D
需要对上述源数据的进行预处理,统计成父子调用关系,梳理调用时间点,具体的统计处理脚本可参考这里。
三、性能指标制定
通过上文对原始数据处理后,经过统计整理,我们可以拿到如下数据集:

3.1 小程序启动流程分析
在制定指标之前需要分析清楚小程序的启动运作流程。通过采集得到的快照数据与trace日志,再对比中各个调用栈的时间点,得出了下图的启动timeline。

通过官方文档可了解到,trace日志中的startupDone统计的就是性能面板上的启动耗时,而在启动方法的末尾,统一loading视图才刚刚开始。这个过程中,经历了页面切换,初次渲染等操作。ActivityCreate猜测是Android系统中Activity的创建过程,X5Prepare是启动微信浏览器的内核。ResourcePrepare是资源准备过程,如果是初次进入,准备中还会包含资源下载的过程。
WebViewInit+PageFrameLoad操作,以及紧紧跟随的是WAWebView和pageframe操作,从命名上看是对WebView和PageFrame的初始化(小程序之所以有原生般的体验,原因之一就是采用了PageFrame技术)。在启动过程中,会有2次这样的流程,第一次是为了加载loading视图,第二次才是首页视图的PageFrame准备。
3.1 通用性能指标定义
这部分的指标可以在小程序每个页面(通过trace日志中的firstRender记录进行分割)进行统计。前置条件是,每个页面的测试case都会首先执行1000px的来回滚动操作,为了获取滚动期间的FPS和内存变化值。
页面首次加载时间
page.onReady 与 page.onLoad 两个页面生命周期的startTime之差。
加载耗时/页面切换耗时
参考性能面板,由trace文件中的Native类别提供。
首次渲染耗时
从firstRender的Endtime与PageLoad的StartTime之差,是指首次渲染时View Thread和App Service Thread的总计执行时长。这里与性能面板上的首次渲染耗时(View Thread的渲染时长)有所不同。
初始化耗时
从trace日志中分析得出,从生命周期执行结束到展现首次渲染结果的时长
首字节到达时间
当前页面下第一个wx.request方法调用endTime 与 page.onLoad的startTime之差。请求数据到达越快,内容的呈现速度越快。
平均内存/最大内存
统计当前小程序页面内的内存的平均值和最大值。内存消耗过大,会在低配机型上引起闪退或者黑屏。
平均FPS/最低FPS
统计当前小程序页面内的FPS的平均值和最大值。人眼在30fps的情况下,可以保持连续画面,30以下就慢慢有卡顿了。
setData最大频率
统计当前小程序页面内的调用setData方法的频率(单位:秒)。在页面渲染过程中,如果setData频率过快,会影响渲染速度。
SpeedIndex
speedIndex表示一个页面渲染的速度(单位:毫秒)。首先要确定分析的记录时间段,1. 首页,从startupDone的EndTime开始到滚动触发前结束;2. 非首页,从页面切换开始(从当前页面的PageLoad方法的EndTime到滚动触发前结束)。统计这个时间段内,每一张快照的填充率,再通过如下公式计算出speedIndex值。

本文使用了speedline开源库,来计算speedIndex值。

这里引用一张官方的说明图(来源),图中是以秒为单位统计的,白色柱子表示页面每秒的填充率,已及对应的speedIndex值,可以看出,speedIndex值越小,表示内容的填充速度越快。
3.2 首屏(小程序启动)性能指标定义
在小程序首次启动的情况下,会有一些独有的数据,因此可以单独统计一些性能指标
资源加载耗时
包含了资源下载和加载耗时,如果是非首次打开则没有下载耗时
启动耗时
参考性能面板,由trace文件中的Native类别提供。
四、总结
本文主要介绍了小程序在自动化测试过程中,采集快照和trace日志的方法,并对采集到的数据进行筛选统计,分析得到相关的性能指标,为性能优化提供数据支持。
原文来自 知乎: https://zhuanlan.zhihu.com/p/69052314作者:charway原来微信支付软件架构是这样哒!!!
前面必读系列-this/apply/call考点
有这个!你还愁不会写正则吗?
看我如何把node接口耗时降低23%!
前端的你是时候跻身docker了!【简易入门】
高级前端工程师是怎样高效部署前端应用?
Webpack 4 如何帮你删除无效代码?Tree Shaking 终极优化指南
进阶高级前端工程师需要了解的数据结构和算法
【撩妹教程】如何教公司新来的女实习生小姐姐什么是闭包?
前端如何在繁忙的业务中提升自己【收藏系列】JavaScript知识图谱-入门培训,进阶巩固知识体系全链路日志如何实现?大厂的高性能小程序原来是这样弄的!感觉可能会改变未来几年的打包方式!应用秒开方案改进测试一下你离前端专家这个称号还有多远?
