背景
當前閒魚在精益開發模式下,整個技術團隊面臨了諸多的能力落地和挑戰,尤其是效能方面的2-1-1的目標(2周需求交付週期,1周需求開發週期,1小時達到釋出標準),在這個大目標下,就必須把每個環節都做到極致。自動化的建設是決定CI(持續整合)成敗的關鍵能力,今天分享一下閒魚Android客戶端效能自動化環節的實踐。
面臨的問題
1 發現問題
工具缺失:
目前淘寶系,對於線上效能水位的監控有一套完善的體系,但是針對新功能的效能測試,每個業務團隊都有對應的效能專項小組,產出的工具都是根據自己業務特點的定製開發的,閒魚客戶端目前使用Flutter做為客戶端主開發語言,對於Flutter效能資料的獲取及UI自動化測試支撐工具目前是缺失的,同時業界對Flutter自動化和效能相關的實踐幾乎沒有;
測試工作量翻N倍(N=一個版本週期內的分支數):
原先的開發模式是功能測試整合測試一起進行的,所以效能測試只需要針對整合後的包進行測試即可,到現在轉變為泳道的開發模式,一個版本內會一般包含十幾個左右的泳道分支甚至更多,我們必須確保每個泳道的分支的效能是達標的,如果有效能問題需要第一時間反饋出來,如果遺留到整合階段,問題的排查(十幾個分支中篩查),問題的解決將會耗費大量的時間,效率很難得到大的提升;
2 分析問題
體系化解決,要讓每個泳道分支都得到有效測試覆蓋,測試件能夠自動化執行,持續反饋結果。
解決方案
綜合上述問題,梳理如下解決方案:
針對Flutter效能資料的獲取(比如,Flutter有自己的SurfaceView,原有Native計算FPS的方式無法直接使用)
針對Flutter UI自動化的實現(Flutter/Native UI混合棧的處理)
效能自動化指令碼 / 效能資料自動採集、上報 融入CI流程
效能問題的通知 / 報表展示 / 分析
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 $awkfike >>$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
}
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 效能自動化CI流程
4 效能資料包表
[FPS]
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]
[Memory]
成果展示
1 指定泳道分支效能監控
泳道分支出現了效能問題再報表上一目瞭然
2 效能專項支撐
Flutter商品詳情頁重構 14輪測試
客戶端圖片統一資源測試 4輪測試
總結
效能自動化只是整個CI流程中的一個環節,為了極致效率的大目標,閒魚質量團隊還產出了很多支撐工具,CI平臺,遍歷測試,AI錯誤識別,用例自動生成等等,後續也會分享給大家。
參考
https://testerhome.com/topics/2232
https://testerhome.com/topics/4775