天天看點

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

作者:閑魚技術-燈陽

1.背景

目前閑魚在精益開發模式下,整個技術團隊面臨了諸多的能力落地和挑戰,尤其是效能方面的2-1-1的目标(2周需求傳遞周期,1周需求開發周期,1小時達到釋出标準),具體可見

閑魚工程師是如何建構持續內建流水線,讓研發效率翻倍的

,在這個大目标下,就必須把每個環節都做到極緻。自動化的建設是決定CI成敗的關鍵能力,今天分享一下閑魚Android用戶端性能自動化環節的實踐。

2.面臨的問題

2.1 主要是兩個方面的問題

  • 工具缺失:

目前淘寶系,對于線上性能水位的監控有一套完善的體系,但是針對新功能的性能測試,每個業務團隊都有對應的性能專項小組,産出的工具都是根據自己業務特點的定制開發的,閑魚用戶端目前使用Flutter做為用戶端主開發語言,對于Flutter性能資料的擷取及UI自動化測試支撐工具目前是缺失的,同時業界對Flutter自動化和性能相關的實踐幾乎沒有;

  • 測試工作量翻N倍(N=一個版本周期内的分支數):

原先的開發模式是功能測試內建測試一起進行的,是以性能測試隻需要針對內建後的包進行測試即可,到現在轉變為泳道的開發模式,一個版本内會一般包含十幾個左右的泳道分支甚至更多,我們必須確定每個泳道的分支的性能是達标的,如果有性能問題需要第一時間回報出來,如果遺留到內建階段,問題的排查(十幾個分支中篩查),問題的解決将會耗費大量的時間,效率很難得到大的提升;

2.2 問題思考

體系化解決,要讓每個泳道分支都得到有效測試覆寫,測試件能夠自動化執行,持續回報結果

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

3. 解決方案

綜合上述問題,梳理如下解決方案:

  • 針對Flutter性能資料的擷取(比如,Flutter有自己的SurfaceView,原有Native計算FPS的方式無法直接使用)
  • 針對Flutter UI自動化的實作(Flutter/Native UI混合棧的處理)
  • 性能自動化腳本 / 性能資料自動采集、上報 融入CI流程
  • 性能問題的通知 / 報表展示 / 分析

3.1 性能資料

[FPS]

解析處理 adb shell dumpsys SurfaceFlinger --latency 的資料,詳細請見文末參考連結(該方式相容Flutter及Native的解決方案,已在Android4.x-9.x驗證可行),處理SurfaceFlinger核心代碼如下:

dumpsys SurfaceFlinger --latency-clear
#echo "dumpsys SurfaceFlinger..."
if [[ $isflutter = 0 ]];then
  window=`dumpsys window windows | grep mCurrent | $bb awk '{print $3}'|$bb tr -d '}'` # Get the current window
  echo $window
fi
if [[ $isflutter = 1 ]];then
  window=`dumpsys SurfaceFlinger --list |grep '^SurfaceView'|$bb awk 'NR==1{print $0}'`
if [ -z "$window" ]; then 
  window="SurfaceView"
fi
echo $window
fi
$bb usleep $sleep_t
dumpsys SurfaceFlinger --latency "$window"|$bb awk -v time=$uptime -v target=$target -v kpi=$KPI '{if(NR==1){r=$1/1000000;if(r<0)r=$1/1000;b=0;n=0;w=1}else{if(n>0&&$0=="")O=1;if(NF==3&&$2!=0&&$2!=9223372036854775807){x=($3-$1)/1000000/r;if(b==0){b=$2;n=1;d=0;D=0;if(x<=1)C=r;if(x>1){d+=1;C=int(x)*r;if(x%1>0)C+=r};if(x>2)D+=1;m=r;o=0}else{c=($2-b)/1000000;if(c>1000){O=1}else{n+=1;if(c>=r){C+=c;if(c>kpi)o+=1;if(c>=m)m=c;if(x>1)d+=1;if(x>2)D+=1;b=$2}else{C+=r;b=sprintf("%.0f",b+r*1000000)}}};if(n==1)s=sprintf("%.3f",$2/1000000000)};if(n>0&&O==1){O=0;if(n==1)t=sprintf("%.3f",s+C/1000);else t=sprintf("%.3f",b/1000000000);T=strftime("%F %T",time+t)"."sprintf("%.0f",(time+t)%1*1000);f=sprintf("%.2f",n*1000/C);m=sprintf("%.0f",m);g=f/target;if(g>1)g=1;h=kpi/m;if(h>1)h=1;e=sprintf("%.2f",g*60+h*20+(1-o/n)*20);print s","t","T","f+0","n","d","D","m","o","e","w;n=0;if($0==""){b=0;w+=1}else{b=$2;n=1;d=0;D=0;if(x<=1)C=r;if(x>1){d+=1;C=int(x)*r;if(x%1>0)C+=r};if(x>2)D+=1;m=r;o=0}}}}' >>$file           

[CPU]

使用的是top的指令擷取(該方式擷取性能資料時,資料收集帶來的損耗最少)

export bb="/data/local/tmp/busybox"
$bb top -b -n 1|$bb awk 'NR==4{print NF-1}'           

[記憶體]

解析 dumpsys meminfo $package 拿到 Java Heap,Java Heap Average,Java Heap Peak,Native Heap,Native Heap Average,Native Heap Peak,Graphics,Unknown,Pss 資料

do_statistics() {
    ((COUNT+=1))
    isExist="$(echo $OUTPUT | grep "Dalvik Heap")" 
    if [[ ! -n $isExist ]] ; then
        old_dumpsys=true
    else
        old_dumpsys=false
    fi
    if [[ $old_dumpsys = true ]] ; then
        java_heap="$(echo "$OUTPUT" | grep "Dalvik" | $bb awk '{print $6}' | $bb tr -d '\r')"
    else
        java_heap="$(echo "$OUTPUT" | grep "Dalvik Heap[^:]" | $bb awk '{print $8}' | $bb tr -d '\r')"
    fi
    echo "1."$JAVA_HEAP_TOTAL "2."$java_heap "3."$JAVA_HEAP_TOTAL
    ((JAVA_HEAP_TOTAL+=java_heap))
    ((JAVA_HEAP_AVG=JAVA_HEAP_TOTAL/COUNT))
    if [[ $java_heap -gt $JAVA_HEAP_PEAK ]] ; then
        JAVA_HEAP_PEAK=$java_heap
    fi
    if [[ $old_dumpsys = true ]] ; then
        native_heap="$(echo "$OUTPUT" | grep "Native" | $bb awk '{print $6}' | $bb tr -d '\r')"
    else
        native_heap="$(echo "$OUTPUT" | grep "Native Heap[^:]" | $bb awk '{print $8}' | $bb tr -d '\r' | $bb tr -d '\n')"
    fi
    ((NATIVE_HEAP_TOTAL+=native_heap))
    ((NATIVE_HEAP_AVG=NATIVE_HEAP_TOTAL/COUNT))
    if [[ $native_heap -gt $NATIVE_HEAP_PEAK ]] ; then
        NATIVE_HEAP_PEAK=$native_heap
    fi
    g_Str="Graphics"
    if [[ $OUTPUT == *$g_Str* ]] ; then
        echo "Found Graphics..."
        Graphics="$(echo "$OUTPUT" | grep "Graphics" | $bb awk '{print $2}' | $bb tr -d '\r')"
    else
        echo "Not Found Graphics..."
        Graphics=0
    fi
    Unknown="$(echo "$OUTPUT" | grep "Unknown" | $bb awk '{print $2}' | $bb tr -d '\r')"
    total="$(echo "$OUTPUT" | grep "TOTAL"|$bb head -1| $bb awk '{print $2}' | $bb tr -d '\r')"
}           

[流量]

通過 dumpsys package packages 解析出目前待測試包來擷取流量資訊

uid="$(dumpsys package packages|$bb grep -E "Package |userId"|$bb awk -v OFS=" " '{if($1=="Package"){P=substr($2,2,length($2)-2)}else{if(substr($1,1,6)=="userId")print P,substr($1,8,length($1)-7)}}'|grep $package|$bb awk '{print $2}')"
echo "Net:"$uid
initreceive=`$bb awk -v OFS=" " 'NR>1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print i,wr[i]/1000,wt[i]/1000,"wifi"};for(i in rr){print i,rr[i]/1000,rt[i]/1000,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid|$bb awk '{print $2}'`
inittransmit=`$bb awk -v OFS=" " 'NR>1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print i,wr[i]/1000,wt[i]/1000,"wifi"};for(i in rr){print i,rr[i]/1000,rt[i]/1000,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid|$bb awk '{print $3}'`

echo "initnetarray"$initreceive","$inittransmit
getnet(){
    local data_t=`date +%Y/%m/%d" "%H:%M:%S`
    netdetail=`$bb awk -v OFS=, -v initreceive=$initreceive -v inittransmit=$inittransmit -v datat="$data_t" 'NR>1{if($2=="wlan0"){wr[$4]+=$6;wt[$4]+=$8}else{if($2=="rmnet0"){rr[$4]+=$6;rt[$4]+=$8}}}END{for(i in wr){print datat,i,wr[i]/1000-initreceive,wt[i]/1000-inittransmit,"wifi"};for(i in rr){print datat,i,rr[i]/1000-initreceive,rt[i]/1000-inittransmit,"data"}}' /proc/net/xt_qtaguid/stats | grep $uid`
    echo $netdetail>>$filenet
}           

3.2 性能自動化腳本

  • 基于Appium的自動化用例,這個技術業界已經有非常多的實踐了,這裡我不再累述,如果不了解的同學,可以到Appium官網 http://appium.io
  • Flutter和Native頁面切換使用App内的Schema跳轉
  • Flutter頁面的文本輸入等互動性較強的場景使用基于Flutter架構帶的Integration Test來操作

An integration test

Generally, an integration test runs on a real device or an OS emulator, such as iOS Simulator or Android Emulator. The app under test is typically isolated from the test driver code to avoid skewing the results.

Flutter的UI自動化及Flutter/Native混合頁面的處理在測試上的應用後續單獨開文章介紹,原理相關可以先參考

千人千面錄制回放技術

3.3 性能自動化CI流程

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

3.4 性能資料報表

FPS相關

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考
  • Framediff: 繪制幀的開始時間和結束時間差
  • FPS: 每秒展示的幀數
  • Frames: 一個重新整理周期内所有的幀
  • jank: 一幀開始繪制到結束超過16.67ms 就記一次jank,jank非零代表硬體繪制掉幀,和螢幕硬體性能及相關驅 動性能有關
  • jank2: 一幀開始繪制到結束超過33.34ms 就記一次jank2
  • MFS: 在一個重新整理周期内單幀最大耗時(每兩行垂直同步的時間差代表兩幀繪制的幀間隔)
  • OKT: 在一個重新整理周期内,幀耗時超過16.67ms的次數
  • SS: 流暢度,通過FPS,MFS,OKT計算出來,流暢度 = 實際幀率比目标幀率比值60【目标幀率越高越好】 + 目标時間和兩幀時間差比值20【兩幀時間差越低越好】 + (1-超過16ms次數/幀數)*20【次數越少越好】

CPU

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

Memory

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

4. 成果展示

4.1 指定泳道分支性能監控

泳道分支出現了性能問題再報表上一目了然

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

4.2 性能專項支撐

1、Flutter商品詳情頁重構 14輪測試

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

2、用戶端圖檔統一資源測試 4輪測試

能用機器完成的,千萬别堆工作量|持續內建中的性能自動化測試1.背景2.面臨的問題3. 解決方案4. 成果展示5. 總結6. 參考

5. 總結

性能自動化隻是整個CI流程中的一個環節,為了極緻效率的大目标,閑魚品質團隊還産出了很多支撐工具,CI平台,周遊測試,AI錯誤識别,用例自動生成等等,後續也會分享給大家。

6. 參考

https://testerhome.com/topics/2232 https://testerhome.com/topics/4775