当一个应用的用户愈来愈多,业务愈来愈复杂,性能问题就会突显,特别是在低端机上的用户感觉尤其明显,甚至会影响到应用的用户活跃度、停留时长等重要指标,提高应用在中低端机上的性能迫在眉睫。如何来对研发同窗的优化作出合理的评测咱们须要思考下面两点:python
而影响用户体验最重要的一个指标就是启动耗时,特别是拉新的时候,关于如何测量启动耗时,通常有两个方向:一是经过技术埋点,但基于技术埋点记录数据很难衡量用户真实体感(线上统计数据好?真实体感却差?),并且也没法基于技术埋点获取竞品数据;另外一个是经过录屏分帧测试,可是人工录屏逐帧分析会有人为感知偏差(结束位边界认知不一致),并且人工性能专项测试持续交付ROI不高,好比录制10次,抽取关键帧取平均值,差很少要花费半个多小时,采样次数越多,耗时越久。因为最近一段时间在看机器学习的书,因此在想能不能拿这个案例来实践一下。android
在此以前我也调研了一下业内已有的相似方案:有经过OCR文字识别的、也有经过图像对比的,其中图像对比的方案若是是整图对比,视频启动过程当中的广告、首页海报是变化的,这样没法准确识别;另外若是是部分对比,那么app完整启动后第一屏不彻底展现的地方,每次不必定在同一处,因而我参考了各类方案后,结合本身的想法,就把整个方案实现了一遍,接下来详细介绍一下此方案。web
因为整个方案我是经过Python实现的,因此本地须要安装好Python环境,这里我使用的是Mac电脑因此默认带的Python环境,但若是要用到Python3须要本身升级,另外要安装pip工具:算法
brew install pip3
复制代码
安装scikit-learn,一个简单的机器学习框架,以及依赖的科学计算软件包numpy和算法库scipy:shell
pip3 install scikit-learn
pip3 install numpy
pip3 install scipy
复制代码
图片处理库OpenCV和imutils:api
pip3 install opencv-contrib-python
pip3 install imutils
复制代码
对视频文件进行分帧处理的ffmpeg:性能优化
brew install ffmpeg
复制代码
安装airtest框架(网易的一个跨平台的UI自动化框架):bash
pip3 install -U airtest
复制代码
安装poco框架(网易的一个跨平台的UI自动化框架):app
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个阶段:
这五个阶段的图片以下:
这里选择SIFT特征,SIFT特征具备放缩、旋转、光照不变性,同时兼有对几何畸变,图像几何变形的必定程度的鲁棒性,使用Python OpenCV扩展模块中的SIFT特征提取接口,就能够提取图像的SIFT特征点与描述子。
词袋生成,是基于描述子数据的基础上,生成一系列的向量数据,最多见就是首先经过K-Means实现对描述子数据的聚类分析,通常会分红100个聚类、获得每一个聚类的中心数据,就生成了100 词袋,根据每一个描述子到这些聚类中心的距离,决定了它属于哪一个聚类,这样就生成了它的直方图表示数据。
使用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
复制代码
和阶段1采用的方式同样。
和阶段2测试模型的作法同样。
根据预测结果,肯定点击应用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分钟左右,效率明显提高,还避免了不一样人操做采集标准不一致的问题。