完成方案后开源了,也没见有反馈的,今天公司内部做了分享,在此也把实现过程分享下。
Step1:了解 sysrrace 背景,依靠搜索
Step2:如何解决持续抓取?
Step1:首先的了解下 SurfaceFlinger 的绘制过程:VSYNC on Android N
DispSync-799 ( 683) [000] ...1 1080841.601945: tracing_mark_write: C | 683 | VSYNC-app|1
DispSync-799 ( 683) [000] ...1 1080841.602019: tracing_mark_write: C | 683 | VSYNC-sf|1
<...>-683 (-----) [000] ...1 1080841.602840: tracing_mark_write: C | 683 | StatusBar#0|1
<...>-683 (-----) [001] ...1 1080841.605342: tracing_mark_write: B | 683 | postFramebuffer
DispSync-799 ( 683) [000] ...1 1080841.618617: tracing_mark_write: C | 683 | VSYNC-app|0
DispSync-799 ( 683) [000] ...1 1080841.618664: tracing_mark_write: C | 683 | VSYNC-sf|0
Step2:处理逻辑:
Step3:帧率统计逻辑
“注:”部分是解释影响脚本执行,此文件是代码细节介绍
注:shell脚本指定执行的工具位置,也可以不指定执行命令调用
#!/system/bin/sh
注:判定依赖的busybox是否存在,存在则赋予bb全局变量方便使用
if [ -f /data/local/tmp/busybox ];then
bb="/data/local/tmp/busybox"
else
echo "No /data/local/tmp/busybox"
exit
fi
注:按一般命令工具方式设计,help文本
show_help() {
echo "
Usage: sh gfx.sh [ -t target_FPS ] [ -k KPI ] [ -T output_type ] [ -d delay_time ] [ -f output_frames ] [ -F output_folder ] [ -h ]
POSIX options | GNU long options
-t | --target The target FPS. Default: 60
-k | --KPI The one frame's kpi time for scoring. Default: 100ms
-T | --type The output type [0~4]. Default: 0
-d | --delay The delay time for checking output. Default: 1 (S)
-f | --frames The frames number to output resulr. Default: 20
-F | --folder The folder name for resulr csvs(/data/local/tmp/). Default: fps
-h | --help Display this help and exit
"
}
注:全局变量指定默认值
注:目标帧率,用于加权评价
target=60
注:目标卡顿评估线,生物概念认知:人眼有100ms buffer,100ms以上判定为卡顿,一些视觉敏感的人或经过训练的人识别卡段的能力会提高
KPI=100
注:输出类型0到4,0——只输出帧率统计;1——增加体验认知卡顿的单帧信息;2——大于vsync的卡顿帧信息输出;3——只输出1中的卡顿信息;4只输出2中的卡顿信息
type=0
注:查询是否输出帧率统计的间隔,单位秒,控制输出文本量
delay=1
注:控制帧率计算的1起步总帧数,默认大于20帧输出
frames=20
注:测试结果文件夹名,默认fps,在/data/local/tmp/下,此目录用户不可见,常用于脚本执行
folder=fps
注:模拟命令行工具传入参数的解析,shift是逐个参数右移删除
while :
do
case $1 in
-h | --help)
show_help
exit 0
;;
-t | --target)
shift
target=$1
shift
;;
-T | --type)
shift
type="$1"
shift
;;
-k | --KPI)
shift
KPI=$1
shift
;;
-d | --delay)
shift
delay="$1"
shift
;;
-f | --frames)
shift
frames="$1"
shift
;;
-F | --folder)
shift
folder="$1"
shift
;;
--) # End of all options
shift
break
;;
*) # no more options. Stop while loop
break
;;
esac
done
注:解析功能独立函数设计,以便其他脚本复用
#参数说明
## $1 = 目标帧率
## $2 = 评估体验卡顿的把控线
## $3 = 输出类型:
## 0) ~ 默认只输出帧率
## 1)~ 增加APP内容两帧间隔 >42ms 的单帧信息
## 2)~ 增加绘制间隔 >vsync间隔的单帧信息
## 3)~ 只输出APP内容两帧间隔 >42ms 的单帧信息
## 4)~ 只输出绘制间隔 >vsync间隔的单帧信息
## $4 = 检查输出结果的间隔
## $5 = 检查输出时,满足?帧以上输出条件的才输出
GFX(){
#如果systrace打开则关闭
if [ `cat /sys/kernel/debug/tracing/tracing_on` -eq 1 ];then
echo "atrace gfx stop"
atrace gfx --async_stop 1>/dev/null &
$bb sleep 3
kill $!
fi
#vsync 间隔获取
local sync=`dumpsys SurfaceFlinger --latency|$bb awk 'NR==1{r=$1/1000000;if(r<0)r=$1/1000;print r}'`
#当前选中的activity
local hasFocus=`dumpsys input|grep "hasFocus=true"|$bb awk '{print substr($4,1,length($4)-3)}'`
#提取当前显示activity的app
local app=`echo $hasFocus|$bb awk -F "/" '{print $1}'`
#开始输出systrace log: 1M 循环buffer 的gfx信息
echo "atrace gfx -b 1024 -c --async_start"
atrace gfx -b 1024 -c --async_start
#awk解析
注:-F 指定间隔符,这里是 |
$bb awk -F "|" \
注:外部变量传入
-v sync="$sync" \
-v OFS=, \
-v app="$app" \
-v activity="$hasFocus" \
-v target="$1" \
-v kpi="$2" \
-v type="$3" \
-v delay="$4" \
-v fames="$5" \
-v csv=$monitor/fps.csv \
-v csv1=$monitor/dropFrames.csv \
注:awk有三部分BEGIN{}{}END{},BEGIN是第一行之前的处理,END是最后一行之后。可以在BEGIN中将传入变量做进一步的处理
注:这里的实现是因为要在退出时把未输出的数据输出,在awk内部做的逐行读取处理,所以只有BEGIN;对于awk外部命令调用不支持的有gfx_no_system.sh
注:awk是单行执行或传入脚本文件,这里为了方便看在每行后加的“ \” 只代表每一行是连接的非换行,为了方便看去掉awk首尾单引号
BEGIN{ \
注:while("cat /sys/kernel/debug/tracing/trace_pipe"|getline){} 是awk的调用外部命令的逐行处理,getline是获取行能容函数,这么做的目的是kill掉cat进程后可以继续输出数组中的数据
while("cat /sys/kernel/debug/tracing/trace_pipe"|getline){ \
注:只处理四列的C和postFramebuffer所在行信息
if(NF==4||$3=="postFramebuffer"){ \
注:第三列是VSYNC的进行统计处理。VSYNC-sf和VSYNC-app是android P之后的,之前是VSYNC-sf-app未区分
if($3~/VSYNC-sf|VSYNC-app|VSYNC-sf-app/){ \
注:gsub是替换函数支持正则(目标字符串,替换为字符串,处理的字符串变量),多用于格式混乱不好处理的内容进行初步格式化
gsub(/.*.) |: tracing_mark_write: C/,"",$1); \
注:将替换后的字符串切割为函数,目的是提取时间的字符串,T[3]是需要的时间,shell的数组是从1起,并不是0
split($1,T," "); \
注:起始时间变量f,用于计算输出间隔处理输出判定。awk的变量无需初始化,默认空,变量转换也随意,字符串+0=0,a[1]=0, a就是数组,delete a直接删除
if(f==""){ \
f=T[3]; \
注:根于type的值判定输出状态logF是统计帧率对应fps.csv,logD是卡顿掉帧对应dropFrames.csv
logF=1; \
logD=0; \
if(type==1){ \
logD=1 \
}else{ \
if(type==2){ \
logD=2; \
}else{ \
if(type==3){ \
logF=0; \
logD=1 \
}else{ \
if(type==4){ \
logF=0; \
logD=2 \
} \
} \
} \
} \
}else{ \
注:post代表最近一个vsync间隔内是否包含postFramebuffer,awk是逐行处理上下文的判定需要个状态变量
if(post==1){ \
注:处理index(p)的surface名部分,即帧内容的归属,同样是之前行处理是传递下来的变量值
if(sv!=""){ \
SV=sv \
}else{ \
SV=activity \
}; \
p=TX","app",\""SV"\""; \
注:判定绘制间隔,超过则 D[p]+=1
t=(T[3]-VSYNC)*1000; \
if(t>sync){ \
d=1 \
}else{ \
d=0 \
}; \
注:计算体验卡顿,即相同窗口前后两帧的时间差
wt=(T[3]-V[p])*1000; \
注:只对间隔<500ms的数据进行帧率统计,大于的记录等待,从而可以提高帧率统计值的意义,避免等待影响帧率波动
if(wt<500){ \
注:掉帧卡顿的输出处理
if(d==1){ \
if(logD==2){ \
print VSYNC,T[3],t,$3,p,"\""info"\"">csv1 \
}else{ \
if(logD==1){ \
if(wt>=42)print V[p],T[3],wt,$3,p,"\""info"\"">csv1 \
} \
} \
}; \
注:帧率统计所需数组的记录,舍弃了≥500ms的
if(logF==1){ \
注:帧数
N[p]=N[p]+1; \
注:耗时总计
Time[p]=Time[p]+wt; \
注:最大卡顿
if(M[p]==""){ \
M[p]=wt \
}else{ \
if(M[p]<wt)M[p]=wt \
}; \
注:[100,500) 卡顿区间,基于人眼的100ms buffer
if(wt>=100){ \
A[p]=A[p]+1 \
}else{ \
注:[50,100) 卡顿区间,20帧是游戏卡顿可玩忍受底线,再低就是卡片交互类游戏了,不是即时交互类游戏
if(wt>=50){ \
B[p]=B[p]+1 \
}else{ \
注:[42,50) 卡顿区间,电影24帧录制,网络视频25帧播放,视频体验延迟区间
if(wt>=42)C[p]=C[p]+1 \
} \
} \
} \
}else{ \
注:记录等待的时间和次数
if(logF==1&&b[p]!=""){ \
Stop[p]=Stop[p]+1; \
StopT[p]=StopT[p]+wt \
} \
}; \
if(logF==1){ \
注:超过vsync间隔的掉帧记录,绘制性能问题,需减小绘制buffer或优化绘制的framework层面优化
if(d==1)D[p]=D[p]+1; \
注:更新index的起始时间
if(b[p]=="")b[p]=T[3]; \
V[p]=T[3]; \
注:判定检查间隔
if(T[3]-f>=delay){ \
注:判定帧数据数据间隔,有窗口帧率数据则处理
if(length(N)>0){ \
注:数组的逐个处理
for(i in N){ \
注:帧数大于配置的帧数则输出,如默认的20帧
if(N[i]>fames){ \
注:计算帧率
fps=sprintf("%.1f",N[i]*1000/Time[i]); \
注:处理小数点精度,避免歧义,因为vsync间有微秒级别浮动
if(fps>=target){ \
fps=int(fps); \
g=1 \
}else{ \
g=fps/target \
}; \
if(kpi<M[i])h=kpi/M[i];else h=1; \
注:流畅度加权计算评估
ss=sprintf("%.2f",g*60+h*20+(1-A[i]/N[i])*10+(1-B[i]/N[i])*7+(1-C[i]/N[i])*3); \
print b[i],V[i],fps+0,N[i],sprintf("%.3f",Time[i]/1000)+0,M[i],sprintf("%.3f",StopT[i]/1000)+0,Stop[i]+0,A[i]+0,B[i]+0,C[i]+0,D[i]+0,ss+0,i>>csv; \
注:对输出的数据进行初始化,下一轮统计
b[i]=V[i]; \
N[i]=""; \
Time[i]=""; \
M[i]=""; \
StopT[i]=""; \
Stop[i]=""; \
A[i]=""; \
B[i]=""; \
C[i]=""; \
D[i]="" \
} \
} \
}; \
注:更新判定间隔的起始时间
f=T[3]; \
} \
} \
};
}; \
注:记录最近一个VSYNC时间
VSYNC=T[3];
注:state、TX,post,sv,info,处理状态用于上下文判定,经过vsync的判定处理为1
state=1; \
TX=0; \
post=0; \
sv=""; \
info="" \
}else{ \
注:更新:state、TX,post,sv,info的内容为了vsync行处理时所用,就是上线文间靠变量实现内容信息的传递
if(NF==3){ \
state=0; \
post=1 \
}else{ \
if(state==1&&$3!~/HW_VSYNC_ON_0|HW_VSYNC_0|hasClientComposition|FrameMissed|FramebufferSurface/){ \
if(logD>0){ \
if(info="")info=$3;else info=info"\n"$3 \
}; \
if($3~/TX - /){ \
TX=1 \
}else{ \
注:提取更新app包名和activity名
l=split($3,Check,"."); \
if(l>2){ \
l=split($3,tmp,"/"); \
if(l>1){ \
sv=$3; \
l=split($3,Check," "); \
if(l==1){ \
app=tmp[1]; \
activity=$3 \
} \
}else{ \
sv=$3 \
} \
}else{ \
sv=$3 \
} \
} \
} \
} \
} \
} \
}; \
注:kill掉cat /sys/kernel/debug/tracing/trace_pipe进程后开始执行,把未输出的数组数据进行输出,避免丢弃,不支持system()调用的设备在结束时只好丢掉了这部分数据gfx_no_system.sh
if(length(N)>0){ \
for(i in N){ \
if(N[i]>0){ \
fps=sprintf("%.1f",N[i]*1000/Time[i]); \
if(fps>=target){ \
fps=int(fps); \
g=1 \
}else{ \
g=fps/target \
}; \
if(kpi<M[i])h=kpi/M[i];else h=1; \
ss=sprintf("%.2f",g*60+h*20+(1-A[i]/N[i])*10+(1-B[i]/N[i])*7+(1-C[i]/N[i])*3); \
print b[i],V[i],fps+0,N[i],sprintf("%.3f",Time[i]/1000)+0,M[i],sprintf("%.3f",StopT[i]/1000)+0,Stop[i]+0,A[i]+0,B[i]+0,C[i]+0,D[i]+0,ss+0,i>>csv \
} \
} \
} \
} & 注:&是后台进程执行,用于离线脱机,不阻塞执行
注:输出cat /sys/kernel/debug/tracing/trace_pipe的pid,停止是可直接kill此进程
#获取cat进程pid
local catPid=`$bb ps|$bb awk '$4=="cat"&&$5=="/sys/kernel/debug/tracing/trace_pipe"{print $1}'`
echo "cat Pid="$catPid
#等待 cat /sys/kernel/debug/tracing/trace_pipe进程退出,后子进程退出
注:等待子进程退出命令,wait,和位置相关,这里只等待函数内起的子进程
wait
#停止systrace
if [ `cat /sys/kernel/debug/tracing/tracing_on` -eq 1 ];then
echo "finish: atrace gfx stop"
atrace gfx --async_stop 1>/dev/null &
$bb sleep 3
kill $!
fi
}
#main
${testresult="/data/local/tmp/"} 2>/dev/null
monitor="$testresult/$folder"
if [ -d $monitor ];then
$bb rm -r $monitor
fi
mkdir -p $monitor
if [ $type -lt 3 ];then
echo "start time,end time,FPS,frames,Time(S),max time(ms),waiting time(S),wait times,A,B,C,D,score,TX,app,Surface" >$monitor/fps.csv
fi
if [ $type -gt 0 ];then
echo "start VSYNC,end VSYNC,used time(ms),VSYNC type,TX,app,Surface,info" >$monitor/dropFrames.csv
fi
注:函数的调用,函数话设计就是可按需集成到其他脚本的需求中
GFX $target $KPI $type $delay $fames
项目开源地址:(https://github.com/sandmanli/android_FPS_frome_GFX)