├── .gitignore ├── README.md ├── reference ├── 拼多多 │ └── t0_end.png └── 有车以后 │ └── t0_end.png ├── requirements.txt └── wxapp_boot_time.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | .vscode 4 | .pytest_cache 5 | 6 | *.pyc 7 | *.swp 8 | *.egg 9 | .idea 10 | .tgz 11 | 12 | data 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wxapp-boot-time 2 | 3 | 微信小程序启动时间的测量方案,借助Appium测试框架、ffmpeg视频处理、pyssim图片相似度判断等开源工具,整体误差在 20~50ms左右。 4 | 5 | 6 | ## 一、背景 7 | 小程序的启动耗时一直是我们非常关注的指标,目前这个指标的主要衡量标准是微信官方后台的报表,这个数据具有较高的参考意义,但是也有几个问题: 8 | 9 | - 只有正式版的数据 10 | - 指标的定义比较模糊,会出现 下载耗时 + 渲染耗时 != 总启动耗时的情况 11 | - 不够细化,只有平台、网络类型区分 12 | 13 | 简而言之它只是一个总体的参考,并不能提供很细化的优化建议。我们需要一个在**测试阶段**就能确定、**恒定测试标准**下的“准确”启动耗时,这样我们在发布前就知道启动时间是变好了还是差了,甚至能知道和竞品的耗时差距。 14 | 15 | 于是本方案应用而生,其实也是业内通用的方案,基于录屏+图片分析,开源出来希望能够帮助更多人。 16 | 17 | ![](https://xingzx.org/static/upload/201903/1552646140.png) 18 | 19 | ## 二、技术方案 20 | ### 2.1 网络限速 21 | 小程序启动过程会有一个网络下载过程,需要尽量排除网络的影响。这是保证测试数据稳定的大前提。 22 | 23 | 一种思路是在手机端限制网速,但是需要ROOT,操作麻烦很多。目前采用了更简单的方案: 24 | 25 | - 路由器端限制设备网速 100KB/s(找运维配置) 26 | - 凌晨5~6点网络低谷时执行测试 27 | - 连续测试5次,去除最大值最小值后取平均值 28 | 29 | ### 2.2 计算方法(本次开源内容) 30 | 在启动小程序(通过下拉菜单启动)时刻同步录屏,然后将视频逐帧分解为图片,最后再通过对比图片计算出启动时间。 31 | 32 | 详细处理流程参考代码实现,可视具体小程序调整相关的相似度因子。 33 | 34 | ### 2.3 持续集成 35 | 将脚本放到 jenkins 进行持续集成,数据汇总到后台生成报表(此部分未开源) 36 | 37 | ## 三、使用教程 38 | ### 3.1 安装[Appium](http://appium.io/)并启动服务 39 | 用于驱动手机自动化操作,建议在服务器端运行此服务,运行服务在Mac、Windows上测试通过 40 | 41 | 需要安装Android SDK、Java等环境,推荐安装最新稳定版 42 | 43 | ### 3.2 安装 [ffmpeg](https://www.ffmpeg.org/download.html) 命令 44 | 45 | Mac可通过 brew 安装,Windows需要下载安装包自行安装。 46 | 47 | ### 3.3 安装Python3及依赖 48 | 49 | 此脚本仅在 Python3 上测试通过,具体依赖列表参考 `requirements.txt` 50 | 51 | 推荐安装最新稳定版 52 | 53 | ### 3.4 修改配置运行服务 54 | 55 | #### 3.4.1 修改如下配置 56 | 57 | ```python 58 | # Appium 服务地址 59 | EXECUTOR = 'http://127.0.0.1:4723/wd/hub' 60 | 61 | # 被测设备信息 62 | ANDROID_CAPS = { 63 | 'platformVersion': '7.0', 64 | 'deviceName': '0915f911a8d02504', 65 | } 66 | 67 | # 被测小程序名 68 | WXAPP = "有车以后" 69 | 70 | ``` 71 | 72 | #### 3.4.2 放置小程序启动时间结束位置图片到对应位置 73 | 74 | 例如 `reference/有车以后/t0_end.png` ,可从录屏分解的图片中挑选一张作为结束位置 75 | 76 | 代码库里默认已经放置了 `有车以后` 和 `拼多多` 两个小程序的结束位置图片,可供参考 77 | 78 | #### 3.4.3 启动脚本 79 | 80 | 直接运行即可 `python wxapp_boot_time.py` 81 | 82 | ``` 83 | [2019-03-15 17:51:07,920] root:INFO: 0002.png 相似度:0.999981 84 | [2019-03-15 17:51:08,432] root:INFO: 0003.png 相似度:0.999973 85 | [2019-03-15 17:51:08,948] root:INFO: 0004.png 相似度:0.999970 86 | ... 87 | [2019-03-15 17:52:11,151] root:INFO: 0120.png 相似度:0.970193 88 | [2019-03-15 17:52:11,677] root:INFO: 0121.png 相似度:0.967654 89 | [2019-03-15 17:52:11,677] root:INFO: 开始位置:5,结束位置:121,本次启动耗时:2320毫秒 90 | ``` 91 | 92 | 93 | 94 | ## 四、微信交流群 95 | 请扫码加群,如二维码失效,可加管理员 `richshaw` 申请入群,备注`小程序测试` 96 | 97 | ![](https://raw.githubusercontent.com/richshaw2015/wxapp-appium/master/img/qrcode.jpg) 98 | 99 | ## 五、版权声明 100 | [有车以后](http://youcheyihou.com/)测试组荣誉出品,如果对您项目有帮忙,欢迎Star,开源声明 [The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause) 101 | -------------------------------------------------------------------------------- /reference/拼多多/t0_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richshaw2015/wxapp-boot-time/425101cdb3df5fea758d711219350905ecdbada1/reference/拼多多/t0_end.png -------------------------------------------------------------------------------- /reference/有车以后/t0_end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richshaw2015/wxapp-boot-time/425101cdb3df5fea758d711219350905ecdbada1/reference/有车以后/t0_end.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Appium-Python-Client 2 | pillow 3 | pyssim 4 | -------------------------------------------------------------------------------- /wxapp_boot_time.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from appium import webdriver 4 | import time 5 | import ssim 6 | import os 7 | import datetime 8 | import base64 9 | import logging 10 | 11 | logging.basicConfig( 12 | level=logging.INFO, 13 | format="[%(asctime)s] %(name)s:%(levelname)s: %(message)s" 14 | ) 15 | 16 | # Appium 服务地址 17 | EXECUTOR = 'http://192.168.0.244:4723/wd/hub' 18 | 19 | # Appium 所需的被测手机参数,需要根据实际情况修改 platformVersion、deviceName 20 | ANDROID_CAPS = { 21 | 'platformName': 'Android', 22 | 'automationName': 'UIAutomator2', 23 | 'appPackage': 'com.tencent.mm', 24 | 'appActivity': '.ui.LauncherUI', 25 | 'fullReset': False, 26 | 'noReset': True, 27 | 'newCommandTimeout': 120, 28 | 'platformVersion': '7.0', 29 | 'deviceName': '0915f911a8d02504', 30 | } 31 | 32 | # Appium 查找一个元素的最大等待时间 33 | IMPLICITLY_WAIT = 10 34 | 35 | # 被测小程序名 36 | WXAPP = "有车以后" 37 | # WXAPP = "拼多多" 38 | 39 | # 视频被切割的帧数,即1秒切割成多少张图片 40 | FPS = 50 41 | 42 | # 项目路径 43 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 44 | 45 | 46 | def calculate_boot_time(pngs_dir, fps, refer_end_pic): 47 | """ 48 | 通过一系列的截图文件,计算出启动时间 49 | :param pngs_dir: 截图所在目录 50 | :param fps: 帧数 51 | :param refer_end_pic: 结束位置参考图片 52 | :return: 启动时间 53 | """ 54 | # 找启动的开始(点击响应)、结束时间(渲染首页内容)点 55 | pngs = os.listdir(pngs_dir) 56 | pngs.sort() 57 | start_t, end_t, boot_time = 0, 0, 0 58 | 59 | # 找开始点,对比和第一张图的相似度 60 | refer_start_pic = os.path.join(pngs_dir, pngs[0]) 61 | for png in pngs[1:]: 62 | dest_png = os.path.join(pngs_dir, png) 63 | factor = ssim.compute_ssim(refer_start_pic, dest_png) 64 | logging.info("%s 相似度:%f" % (png, factor)) 65 | if factor < 0.9: 66 | start_t = int(png.split('.png')[0]) 67 | break 68 | 69 | if start_t > 0: 70 | # 继续找结束点,和灰度的连续匹配两次的最后位置 71 | third_f, second_f, first_f = 0, 0, 0 72 | for png in pngs[start_t:]: 73 | dest_png = os.path.join(pngs_dir, png) 74 | current_f = ssim.compute_ssim(refer_end_pic, dest_png) 75 | logging.info("%s 相似度:%f" % (png, current_f)) 76 | third_f = second_f 77 | second_f = first_f 78 | first_f = current_f 79 | # TODO 这个范围根据实际的业务场景自己确定 80 | if third_f > 0.96 and second_f > 0.96 and first_f < 0.96: 81 | end_t = int(png.split('.png')[0]) 82 | break 83 | 84 | # 有效性判断和时间计算 85 | if start_t == 0 or end_t == 0: 86 | logging.warning("没有找到开始或者结束图片") 87 | elif end_t == len(pngs): 88 | logging.warning("结束位置错误") 89 | else: 90 | boot_time = int((end_t - start_t) * 1000 / fps) 91 | logging.info("开始位置:%d,结束位置:%d,本次启动耗时:%d毫秒", start_t, end_t, boot_time) 92 | return boot_time 93 | 94 | 95 | def main(): 96 | """ 97 | 小程序启动时间的度量,点击后录屏,然后把视频切割成图片帧,最后通过图片分析计算 98 | 本项目仅做demo参考,未做完善的异常处理 99 | :param driver: 100 | :param cmdopt: 101 | :return: 102 | """ 103 | # 初始化 Appium 的 driver 对象 104 | driver = webdriver.Remote(EXECUTOR, ANDROID_CAPS) 105 | driver.implicitly_wait(IMPLICITLY_WAIT) 106 | 107 | time.sleep(6) 108 | # 进入下拉栏目,被测小程序需要出现在下拉栏里,建议收藏起来,以备日后持续测试 109 | width, height = driver.get_window_size()['width'], driver.get_window_size()['height'] 110 | driver.swipe(width * 0.5, height * 0.25, width * 0.5, height * 0.75, duration=800) 111 | time.sleep(2) 112 | 113 | # 本地保存目录,按天划分,一个场景可以有多个记录 114 | mp4_dir = os.path.join(BASE_DIR, 'data', '{:%Y-%m-%d}'.format(datetime.datetime.now()), WXAPP, 115 | '{:%H%M%S}'.format(datetime.datetime.now())) 116 | pngs_dir = os.path.join(mp4_dir, 'pngs') 117 | 118 | os.makedirs(pngs_dir, exist_ok=True) 119 | 120 | mp4 = os.path.join(mp4_dir, 'rec.mp4') 121 | png = os.path.join(pngs_dir, '%04d.png') 122 | 123 | # 录制屏幕 124 | driver.start_recording_screen() 125 | 126 | # 这里通过 xpath 定位小程序,放置命中其他版本,例如开发版、体验版 127 | xpath = "//android.widget.TextView[@text='%s']/../android.widget.FrameLayout[not(android.widget.TextView)]" \ 128 | % WXAPP 129 | driver.find_element_by_xpath(xpath).click() 130 | time.sleep(12) 131 | 132 | mp4_base64 = driver.stop_recording_screen() 133 | driver.quit() 134 | 135 | # 生成视频文件 136 | open(mp4, 'wb').write(base64.b64decode(mp4_base64)) 137 | os.system('ffmpeg -i %s -r %d -ss 00:00:01 -t 00:00:10 -s 1080x1920 %s' % (mp4, FPS, png)) 138 | 139 | # 找启动的开始(点击屏幕)、结束时间(渲染首页内容)点 140 | refer_end_pic = os.path.join(BASE_DIR, 'reference', WXAPP, 't0_end.png') 141 | boot_time = calculate_boot_time(pngs_dir, FPS, refer_end_pic=refer_end_pic) 142 | # TODO 这里可以设置一个符合预期的范围,例如 6000 > boot_time > 2000 143 | if boot_time > 0: 144 | # TODO 把这个时间上传到服务器上,进行数据统计和报表制作 145 | return boot_time 146 | else: 147 | raise ValueError 148 | 149 | 150 | if __name__ == '__main__': 151 | main() 152 | --------------------------------------------------------------------------------