热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

基于机器学习的启动耗时自动化测试方案

使用OpenCV与sklearn实现基于机器学习的启动+首


阅读本文大约需要4.5分钟。

背景

当一个应用的用户越来越多,业务越来越复杂,性能问题就会突显,特别是在低端机上的用户感受尤为明显,甚至会影响到应用的用户活跃度、停留时长等重要指标,提升应用在中低端机上的性能迫在眉睫。如何来对研发同学的优化做出合理的评测我们需要思考下面两点:


  • 要避免“运动式”性能优化, 有不少团队在投入了大量时间和精力对应用进行专项治理之后,由于缺少常态化的管控和治理手段,最终导致性能震荡式波动恶化;

  • 线上的埋点日志数据不能完全反应用户对应用的真实体验和感受;


而影响用户体验最重要的一个指标就是启动耗时(启动+首屏),特别是应用拉新的时候,关于如何测量启动耗时,一般有两个方向:一是通过技术埋点,但基于技术埋点记录数据很难衡量用户真实体感(线上统计数据好?真实体感却差?),而且也无法基于技术埋点获取竞品数据;另一个是通过录屏分帧测试,但是人工录屏逐帧分析会有人为感知误差(结束位边界认知不一致),而且人工性能专项测试持续交付ROI不高,比如录制10次,抽取关键帧取平均值,差不多要花费将近一个小时,采样次数越多,耗时越久。由于最近一段时间在看机器学习的书,所以在想能不能拿这个案例来实践一下。



在此之前我也调研了一下业内已有的类似方案:有通过OCR文字识别的、也有通过图像对比的,其中图像对比的方案如果是整图对比,视频启动过程中的广告、首页海报是变化的,这样无法准确识别;另外如果是部分对比,那么应用完整启动后第一屏不完全展示的地方,每次不一定在同一处,于是我参考了各种方案后,结合自己的想法,就把整个方案实现了一遍,接下来详细介绍一下此方案。


整体流程

阶段一主要是采集数据,将视频转换为图片,生成训练数据和测试数据



阶段二主要是训练模型



阶段三主要是通过训练好的模型进行预测并计算启动时间



环境准备

由于整个方案我是通过Python实现的,所以本地需要安装好Python环境,这里我使用的是Mac电脑所以默认带的Python环境,但如果要用到Python3需要自己升级,另外要安装pip工具:



brew install pip3



安装scikit-learn,一个简单的机器学习框架,以及依赖的科学计算软件包numpy和算法库scipy:



pip3 install scikit-learn

pip3 install numpy

pip3 install scipy



图片处理库OpenCV和imutils:



pip3 install opencv-contrib-python

pip3 install imutils



对视频文件进行分帧处理的ffmpeg:



brew install ffmpeg



安装airtest框架(网易的一个跨平台的UI自动化框架):



pip3 install -U airtest



安装poco框架(网易的一个跨平台的UI自动化框架)



pip3 install pocoui



注意:需要将Android手机开发者选项中的触摸反馈开关打开,这样就可以准确识别出点击应用icon的时刻。




阶段一

首次安装

由于应用第一次安装会有各种权限弹框,为了避免影响测试准确性,我们需要把第一次安装时候的弹框点掉,然后杀掉应用重新启动计算冷启动时间。


另外要模拟用户真实体感,首先要模拟用户真实的点击应用启动的过程,这时候不能通过adb直接唤起应用,我是通过poco框架来实现点击桌面应用icon的。



poco = AndroidUiautomationPoco()

poco.device.wake()

poco(text='应用名字').click()

poco(text='下一步').click()

poco(text='允许').click()

poco(text='允许').click()

poco(text='允许').click()

os.system("adb shell am force-stop {}".format(package_name))



启动录屏

用adb命令开启录屏服务,—time-limit 20 表示录屏20秒,一般情况下20秒启动加首页基本能完成,如果是在低端机上可以适当延长时间。


录屏通过单独线程启动。



subprocess.Popen("adb shell screenrecord  --time-limit 20 /sdcard/sample.mp4", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)



启动应用

测试前对被测应用进行安装,然后在点击完权限弹框后,杀掉进程重新点击桌面icon启动应用。



os.system("adb install -r {}".format(apk_path))

poco(text="应用名字").click()



等录屏结束后杀掉进程,然后重复上面的启动过程,根据采样率决定重复几次。



os.system("adb shell am force-stop {}".format(package_name))



视频分帧

将录制好的视频从手机中拉取到本地,然后通过ffmpeg进行分帧处理。



os.system("adb pull /sdcard/sample.mp4 {}".format(video_local_path))

os.system("ffmpeg -i {} -r 60 {}%d.jpeg".format(video_local_path, test_path))

-r 指定抽取的帧率,即从视频中每秒钟抽取图片的数量。60代表每秒抽取60帧。



提取训练集和测试集数据

我们一般把数据按照80%和20%的比例分为训练集和测试集,这里我们可以录制10组数据,把其中8组作为训练集,2组作为测试集。


阶段二

人工标注训练集数据

由于我们是通过图片分类算法来对启动各个阶段进行识别的,所以首先要定义启动的阶段都有哪些,这里我分为5个阶段:


  • 0_desk:桌面阶段

  • 1_start:点击icon图标的阶段

  • 2_splash:闪屏页出现的阶段

  • 3_loading:首页加载的阶段

  • 4_stable:首页渲染稳定的阶段


这五个阶段的图片如下:



由于应用还会有广告页、业务弹框、首页动态变化等,这些暂时先忽略,不影响整体的测试思路。


特征提取与描述子生成

这里选择SIFT特征,SIFT特征具有缩、旋转、光照不变性,同时对图像几何变形有一定程度的鲁棒性,使用Python OpenCV扩展模块中的SIFT特征提取接口,就可以提取图像的SIFT特征点与描述子。


词袋生成

词袋生成,是基于描述子数据的基础上,生成一系列的向量数据,最常见就是首先通过K-Means实现对描述子数据的聚类分析,一般会分成100个聚类、得到每个聚类的中心数据,就生成了100个词袋,根据每个描述子到这些聚类中心的距离,决定了它属于哪个聚类,这样就生成了它的直方图表示数据。


SVM分类训练与模型生成

使用SVM进行数据的分类训练,得到输出模型,这里通过sklearn的线性SVM实现了分类模型的训练与导出。



import cv2

import imutils

import numpy as np

import os

from sklearn.svm import LinearSVC

from sklearn.externals import joblib

from scipy.cluster.vq import *

from sklearn.preprocessing import StandardScaler


# Get the training classes names and store them in a list

train_path = "dataset/train/"

training_names = os.listdir(train_path)


# Get all the path to the images and save them in a list

# image_paths and the corresponding label in image_paths

image_paths = []

image_classes = []

class_id = 0

for training_name in training_names:

    dir = os.path.join(train_path, training_name)

    class_path = imutils.imlist(dir)

    image_paths += class_path

    image_classes += [class_id] * len(class_path)

    class_id += 1


# 创建SIFT特征提取器

sift = cv2.xfeatures2d.SIFT_create()


# 特征提取与描述子生成

des_list = []


for image_path in image_paths:

    im = cv2.imread(image_path)

    im = cv2.resize(im, (300, 300))

    kpts = sift.detect(im)

    kpts, des = sift.compute(im, kpts)

    des_list.append((image_path, des))

    print("image file path : ", image_path)


# 描述子向量

descriptors = des_list[0][1]

for image_path, descriptor in des_list[1:]:

    descriptors = np.vstack((descriptors, descriptor))


# 100 聚类 K-Means

k = 100

voc, variance = kmeans(descriptors, k, 1)


# 生成特征直方图

im_features = np.zeros((len(image_paths), k), "float32")

for i in range(len(image_paths)):

    words, distance = vq(des_list[i][1], voc)

    for w in words:

        im_features[i][w] += 1


# 实现动词词频与出现频率统计

nbr_occurences = np.sum((im_features > 0) * 1, axis=0)

idf = np.array(np.log((1.0 * len(image_paths) + 1) / (1.0 * nbr_occurences + 1)), 'float32')


# 尺度化

stdSlr = StandardScaler().fit(im_features)

im_features = stdSlr.transform(im_features)


# Train the Linear SVM

clf = LinearSVC()

clf.fit(im_features, np.array(image_classes))


# Save the SVM

print("training and save model...")

joblib.dump((clf, training_names, stdSlr, k, voc), "startup.pkl", compress=3)



预测验证

加载预先训练好的模型,使用模型在测试集上进行数据预测,测试结果表明,对于启动阶段的图像分类可以获得比较好的效果。


下面是预测方法的代码实现:



import cv2 as cv
import numpy as np
from imutils import paths
from scipy.cluster.vq import *
from sklearn.externals import joblib


def predict_image(image_path, pkl):
    # Load the classifier, class names, scaler, number of clusters and vocabulary
    clf, classes_names, stdSlr, k, voc = joblib.load("eleme.pkl")
    # Create feature extraction and keypoint detector objects
    sift = cv.xfeatures2d.SIFT_create()
    # List where all the descriptors are stored
    des_list = []
    im = cv.imread(image_path, cv.IMREAD_GRAYSCALE)
    im = cv.resize(im, (300, 300))
    kpts = sift.detect(im)
    kpts, des = sift.compute(im, kpts)
    des_list.append((image_path, des))


    descriptors = des_list[0][1]
    for image_path, descriptor in des_list[0:]:
        descriptors = np.vstack((descriptors, descriptor))


    test_features = np.zeros((1, k), "float32")
    words, distance = vq(des_list[0][1], voc)
    for w in words:
        test_features[0][w] += 1


    # Perform Tf-Idf vectorization
    nbr_occurences = np.sum((test_features > 0) * 1, axis=0)
    idf = np.array(np.log((1.0 + 1) / (1.0 * nbr_occurences + 1)), 'float32')


    # Scale the features
    test_features = stdSlr.transform(test_features)


    # Perform the predictions
    predictiOns= [classes_names[i] for i in clf.predict(test_features)]
    return predictions



阶段三

采集新的启动视频

和阶段一采用的方式一样。


用模型进行预测

和阶段二预测验证的做法一样。


计算启动时间

根据预测结果,确定点击应用icon阶段的图片和首页渲染稳定之后的图片,获取两个图片直接的帧数差值,如果前面以60帧抽取图片,那么总耗时 = 帧数差值 * 1/60,具体计算这部分的代码实现如下:



from airtest.core.api import *
from dingtalkchatbot.chatbot import DingtalkChatbot
from poco.drivers.android.uiautomation import AndroidUiautomationPoco

webhook = 'https://oapi.dingtalk.com/robot/send?access_token='
robot = DingtalkChatbot(webhook)

def calculate(package_name, apk_path, pkl, device_name, app_name, app_version):
    sample = 'sample/screen.mp4'
    test_path = "dataset/test/"
    if not os.path.isdir('sample/'):
        os.makedirs('sample/')
    if not os.path.isdir(test_path):
        os.makedirs(test_path)
    try:
        os.system("adb uninstall {}".format(package_name))


        os.system("adb install -r {}".format(apk_path))


        poco = AndroidUiautomationPoco()
        poco.device.wake()


        time.sleep(2)


        poco(text='应用名').click()
        poco(text='下一步').click()
        poco(text='允许').click()
        poco(text='允许').click()
        poco(text='允许').click()


        os.system("adb shell am force-stop {}".format(package_name))


        subprocess.Popen("adb shell screenrecord  --time-limit 20 /sdcard/sample.mp4", shell=True,
                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)


        poco(text="应用名").click()


        time.sleep(20)


        os.system("adb pull /sdcard/sample.mp4 {}".format(sample))
        os.system("adb uninstall {}".format(package_name))


        os.system("ffmpeg -i {} -r 60 {}%d.jpeg".format(sample, test_path))
        image_paths = []
        class_path = list(paths.list_images(test_path))
        image_paths += class_path
        start = []
        stable = []
        for image_path in image_paths:
            predictiOns= predict_image(image_path, pkl)
            if predictions[0] == '1_start':
                start += [str(image_path.split('/')[2]).split('.')[0]]
            elif predictions[0] == '4_stable':
                stable += [str(image_path.split('/')[2]).split('.')[0]]


        start_time = int(sorted(start)[0])
        stable_time = int(sorted(stable)[0])
        print("耗时:%.2f 秒" % ((stable_time - start_time) / 60))
        robot.send_text(
            msg="启动耗时自动化测试结果:\n被测设备:{}\n被测应用:{}\n被测版本:{}\n".format(device_name, app_name,
                                                                   app_version) + "启动耗时:%.2f 秒" % (
                        (stable_time - start_time) / 60),
            is_at_all=True)
    except:
        shutil.rmtree(test_path)
        if os.path.exists(sample):
            os.remove(sample)



if __name__ == "__main__":
    calculate("package_name", "app/app-release.apk", "startup.pkl", "小米MIX3", "应用名", "10.1.1")



持续集成

根据上面测试方法提供的参数,通过Jenkins配置任务,训练好模型,将以上三个阶段通过Python脚本的形式封装好,另外再配置好WebHook跟打包平台关联好,即可实现自动验证分析计算最新包的首屏加载耗时。


效果

通过人工录屏,然后用QuickTime分帧查看时间轴,计算出的首屏加载耗时跟这套方案得到的结果误差基本在100毫秒以内,但这个过程一次取数需要15分钟左右,而现在这套方案一次取数只需要3分钟左右,效率明显提升,还避免了不同人操作采集标准不一致的问题。


想要明白些道理,遇见些有趣的事 —— 离岛



推荐阅读
  • 本文详细介绍了 InfluxDB、collectd 和 Grafana 的安装与配置流程。首先,按照启动顺序依次安装并配置 InfluxDB、collectd 和 Grafana。InfluxDB 作为时序数据库,用于存储时间序列数据;collectd 负责数据的采集与传输;Grafana 则用于数据的可视化展示。文中提供了 collectd 的官方文档链接,便于用户参考和进一步了解其配置选项。通过本指南,读者可以轻松搭建一个高效的数据监控系统。 ... [详细]
  • 在Windows系统中安装TensorFlow GPU版的详细指南与常见问题解决
    在Windows系统中安装TensorFlow GPU版是许多深度学习初学者面临的挑战。本文详细介绍了安装过程中的每一个步骤,并针对常见的问题提供了有效的解决方案。通过本文的指导,读者可以顺利地完成安装并避免常见的陷阱。 ... [详细]
  • Python错误重试让多少开发者头疼?高效解决方案出炉
    ### 优化后的摘要在处理 Python 开发中的错误重试问题时,许多开发者常常感到困扰。为了应对这一挑战,`tenacity` 库提供了一种高效的解决方案。首先,通过 `pip install tenacity` 安装该库。使用时,可以通过简单的规则配置重试策略。例如,可以设置多个重试条件,使用 `|`(或)和 `&`(与)操作符组合不同的参数,从而实现灵活的错误重试机制。此外,`tenacity` 还支持自定义等待时间、重试次数和异常处理,为开发者提供了强大的工具来提高代码的健壮性和可靠性。 ... [详细]
  • Android 构建基础流程详解
    Android 构建基础流程详解 ... [详细]
  • 在对WordPress Duplicator插件0.4.4版本的安全评估中,发现其存在跨站脚本(XSS)攻击漏洞。此漏洞可能被利用进行恶意操作,建议用户及时更新至最新版本以确保系统安全。测试方法仅限于安全研究和教学目的,使用时需自行承担风险。漏洞编号:HTB23162。 ... [详细]
  • 本文介绍了一种自定义的Android圆形进度条视图,支持在进度条上显示数字,并在圆心位置展示文字内容。通过自定义绘图和组件组合的方式实现,详细展示了自定义View的开发流程和关键技术点。示例代码和效果展示将在文章末尾提供。 ... [详细]
  • 在Conda环境中高效配置并安装PyTorch和TensorFlow GPU版的方法如下:首先,创建一个新的Conda环境以避免与基础环境发生冲突,例如使用 `conda create -n pytorch_gpu python=3.7` 命令。接着,激活该环境,确保所有依赖项都正确安装。此外,建议在安装过程中指定CUDA版本,以确保与GPU兼容性。通过这些步骤,可以确保PyTorch和TensorFlow GPU版的顺利安装和运行。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • MySQL数据库安装图文教程
    本文详细介绍了MySQL数据库的安装步骤。首先,用户需要打开已下载的MySQL安装文件,例如 `mysql-5.5.40-win32.msi`,并双击运行。接下来,在安装向导中选择安装类型,通常推荐选择“典型”安装选项,以确保大多数常用功能都能被正确安装。此外,文章还提供了详细的图文说明,帮助用户顺利完成整个安装过程,确保数据库系统能够稳定运行。 ... [详细]
  • 通过使用CIFAR-10数据集,本文详细介绍了如何快速掌握Mixup数据增强技术,并展示了该方法在图像分类任务中的显著效果。实验结果表明,Mixup能够有效提高模型的泛化能力和分类精度,为图像识别领域的研究提供了有价值的参考。 ... [详细]
  • 浏览器作为我们日常不可或缺的软件工具,其背后的运作机制却鲜为人知。本文将深入探讨浏览器内核及其版本的演变历程,帮助读者更好地理解这一关键技术组件,揭示其内部运作的奥秘。 ... [详细]
  • 在机器学习领域,深入探讨了概率论与数理统计的基础知识,特别是这些理论在数据挖掘中的应用。文章重点分析了偏差(Bias)与方差(Variance)之间的平衡问题,强调了方差反映了不同训练模型之间的差异,例如在K折交叉验证中,不同模型之间的性能差异显著。此外,还讨论了如何通过优化模型选择和参数调整来有效控制这一平衡,以提高模型的泛化能力。 ... [详细]
  • 在Cisco IOS XR系统中,存在提供服务的服务器和使用这些服务的客户端。本文深入探讨了进程与线程状态转换机制,分析了其在系统性能优化中的关键作用,并提出了改进措施,以提高系统的响应速度和资源利用率。通过详细研究状态转换的各个环节,本文为开发人员和系统管理员提供了实用的指导,旨在提升整体系统效率和稳定性。 ... [详细]
  • Python 程序转换为 EXE 文件:详细解析 .py 脚本打包成独立可执行文件的方法与技巧
    在开发了几个简单的爬虫 Python 程序后,我决定将其封装成独立的可执行文件以便于分发和使用。为了实现这一目标,首先需要解决的是如何将 Python 脚本转换为 EXE 文件。在这个过程中,我选择了 Qt 作为 GUI 框架,因为之前对此并不熟悉,希望通过这个项目进一步学习和掌握 Qt 的基本用法。本文将详细介绍从 .py 脚本到 EXE 文件的整个过程,包括所需工具、具体步骤以及常见问题的解决方案。 ... [详细]
  • 在处理 XML 数据时,如果需要解析 `` 标签的内容,可以采用 Pull 解析方法。Pull 解析是一种高效的 XML 解析方式,适用于流式数据处理。具体实现中,可以通过 Java 的 `XmlPullParser` 或其他类似的库来逐步读取和解析 XML 文档中的 `` 元素。这样不仅能够提高解析效率,还能减少内存占用。本文将详细介绍如何使用 Pull 解析方法来提取 `` 标签的内容,并提供一个示例代码,帮助开发者快速解决问题。 ... [详细]
author-avatar
太阳神神神_890
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有