基於機器學習的啓動耗時自動化測試方案

背景

當一個應用的用戶愈來愈多,業務愈來愈複雜,性能問題就會突顯,特別是在低端機上的用戶感覺尤其明顯,甚至會影響到應用的用戶活躍度、停留時長等重要指標,提高應用在中低端機上的性能迫在眉睫。如何來對研發同窗的優化作出合理的評測咱們須要思考下面兩點: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個階段:

  • 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
複製代碼

階段三

採集新的啓動視頻

和階段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分鐘左右,效率明顯提高,還避免了不一樣人操做採集標準不一致的問題。

相關文章
相關標籤/搜索