前言
本文将整理腾讯GT各个性能测试项的测试方法,目的是为了帮助移动性能专项测试同学快速过一遍腾讯GT各个性能数据是如何获取的。
一.GT性能测试方案之CPU测试
1.简要流程
-
初始化cpu的数据
-
提供了两种方法获取CPU数据 getCpuUsage: 整机的CPU使用水平,主要用于实时刷新GT上的CPU数据。通过读取/proc/stat的数据,将每一个核的cpu使用跟闲置数据提取。使用率永远是增量式计算。计算方法为100*(cpu忙时增量-cpu整体增量),从计算方法来看,可能会导致负数出现。 getProcessCpuUsage:计算进程的CPU使用率,主要通过"/proc/" + pid + "/stat"来计算,在这里回京过一系列计算,拿到进程的CPU时间片
2.代码流程
-
cpu数据初始化 经过初始化,让CPU整体使用跟进程的CPU占用都为0
public CpuUtils() { initCpuData();}private void initCpuData() { pCpu = o_pCpu = 0.0; aCpu = o_aCpu = 0.0;}
-
通过不同的调用栈,来监控不同的CPU数据
整体CPU使用率:由于getCpuUsage是通过后台线程不断刷新来实现的,因此,o_cpu/o_idle数据不断在实时更新RandomAccessFile reader = null;try {reader = new RandomAccessFile("/proc/stat", "r");String load;load = reader.readLine();String[] toks = load.split(" ");double c_idle = Double.parseDouble(toks[5]);double c_cpu = Double.parseDouble(toks[2]) + Double.parseDouble(toks[3]) + Double.parseDouble(toks[4]) + Double.parseDouble(toks[6]) + Double.parseDouble(toks[8]) + Double.parseDouble(toks[7]);if (0 != ((c_cpu + c_idle) - (o_cpu + o_idle))) { // double value = (100.00 * ((c_cpu - o_cpu) ) / ((c_cpu + // c_idle) - (o_cpu + o_idle))); usage = DoubleUtils.div((100.00 * ((c_cpu - o_cpu))), ((c_cpu + c_idle) - (o_cpu + o_idle)), 2); // Log.d("CPU", "usage: " + usage); if (usage < 0) { usage = 0; } else if (usage > 100) { usage = 100; } // BigDecimal b = new BigDecimal(Double.toString(value)); // usage = b.setScale(2, // BigDecimal.ROUND_HALF_UP).doubleValue(); // Log.d("CPU", "usage: " + usage);}o_cpu = c_cpu;o_idle = c_idle;} catch (IOException e) {e.printStackTrace();} finally {FileUtil.closeRandomAccessFile(reader);}
-
进程的CPU使用时间片获取
public String getProcessCpuUsage(int pid) { String result = ""; String[] result1 = null; String[] result2 = null; if (pid >= 0) { result1 = getProcessCpuAction(pid); if (null != result1) { pCpu = Double.parseDouble(result1[1]) + Double.parseDouble(result1[2]); } result2 = getCpuAction(); if (null != result2) { aCpu = 0.0; for (int i = 2; i < result2.length; i++) { aCpu += Double.parseDouble(result2[i]); } } double usage = 0.0; if ((aCpu - o_aCpu) != 0) { usage = DoubleUtils.div(((pCpu - o_pCpu) * 100.00), (aCpu - o_aCpu), 2); if (usage < 0) { usage = 0; } else if (usage > 100) { usage = 100; } } o_pCpu = pCpu; o_aCpu = aCpu; result = String.valueOf(usage) + "%"; } p_jif = pCpu; return result;}
二.GT性能测试方案之内存测试
1.简要流程
内存测试主要通过handleMessage来触发,根据msg参数来决定任务执行
-
内存数据获取:仅仅简单通过dumpsys meminfo来获取内存数据,然后通过解析来得到pss_Native/naticeHeapSize/naticeAllocated/pss_OtherDev/pss_graphics/pss_gl/pss_UnKnown/pss_total数据
-
dumpHeap:通过am dumpheap 来获取heap文件
-
GC:直接kill -10 $pid 来完成GC
2.测试方法
-
内存数据获取
public static MemInfo getMemInfo(String packageName){ MemInfo result = null; String resultString = null; try { resultString = runCMD("dumpsys meminfo " + packageName); } catch (Exception e) { e.printStackTrace(); return MemInfo.EMPTY; } if(Env.API < 14) { result = parseMemInfoFrom2x(resultString); } else if (Env.API < 19) { result = parseMemInfoFrom4x(resultString); } else { result = parseMemInfoFrom44(resultString); } return result;}
-
dumpHeap
private void dumpHeap() { String pid = String.valueOf(ProcessUtils .getProcessPID(AUTManager.pkn.toString())); if (!pid.equals("-1")) { boolean isSucess = true; ProcessBuilder pb = null; String sFolder = Env.S_ROOT_DUMP_FOLDER + AUTManager.pkn.toString() + "/"; File folder = new File(sFolder); if (!folder.exists()) { folder.mkdirs(); } String cmd = "am dumpheap " + pid + " "// 命令 + Env.S_ROOT_DUMP_FOLDER + AUTManager.pkn.toString() + "/"// 输出路径 + "dump_" + pid + "_" + GTUtils.getSaveDate() + ".hprof"; // 输出文件名 pb = new ProcessBuilder("su", "-c", cmd); Process exec = null; pb.redirectErrorStream(true); try { exec = pb.start(); InputStream is = exec.getInputStream(); BufferedReader reader = new BufferedReader( new InputStreamReader(is)); while ((reader.readLine()) != null) { isSucess = false; } } catch (Exception e) { e.printStackTrace(); isSucess = false; } // 至此命令算是执行成功 if (isSucess) { handler.sendEmptyMessage(6); } } else { Log.d("dump error", "pid not found!"); }}
-
GC
private void gc() { String pid = String.valueOf(ProcessUtils .getProcessPID(AUTManager.pkn.toString())); if (!pid.equals("-1")) { boolean isSucess = true; ProcessBuilder pb = null; String cmd = "kill -10 " + pid; pb = new ProcessBuilder("su", "-c", cmd); Process exec = null; pb.redirectErrorStream(true); try { exec = pb.start(); InputStream is = exec.getInputStream(); BufferedReader reader = new BufferedReader( new InputStreamReader(is)); while ((reader.readLine()) != null) { isSucess = false; } } catch (Exception e) { e.printStackTrace(); isSucess = false; } // 至此命令算是执行成功 if (isSucess) { handler.sendEmptyMessage(5); } } else { Log.d("gc error", "pid not found!"); }}
三.GT性能测试方案之内存填充
1.简要流程
内存填充,主要是通过调用系统的malloc来分配内存。内存释放,则是通过系统free来释放。
2. 代码流程
-
内存分配以及释放的函数
const int BASE_SIZE = 1024*1024; // 1Mint fill(int blockNum){ int memSize = blockNum * BASE_SIZE; p = (char *)malloc(memSize); int i; for (i = 0; i < memSize; i++) { p[i] = 0; } return 0;}int freeMem(){ free(p); return 0;}
-
加载com_tencent_wstt_gt_api_utils_MemFillTool.c,并提供度应对操作接口
public class MemFillTool { public MemFillTool() { } public static MemFillTool instance = null; public static MemFillTool getInstance() { if (instance == null) { System.loadLibrary("mem_fill_tool"); instance = new MemFillTool(); } return instance; } // 填充xxxMB内存 public native int fillMem(int blockNum); // 释放刚才填充的内存 public native int freeMem();}
四.GT性能测试方案之帧率测试
1.简要流程
FPS数据收集是一个定时任务(4.3后1s一次),通过异步线程中不断获取FPS数据来刷新到前端页面。而广播模式调用,则直接从缓存的field中获取数据即可。
在这里GT获取fps数据,也是通过采用surfaceflinger来获取,但我感觉好像是有问题的。因为,一般surfaceflinger数据获取的命令是adb shell dumpsys SurfaceFlinger --latency <window name>
,在这里直接定义了把"service call SurfaceFlinger 1013"字符串写到流里,没看明白这个操作跟帧率获取有什么关系。刚去了解了下,Runtime.getRuntime()
原来执行多条命令时后续只要拿到process
的DataOutputStream
对象,继续writeBytes
就可以保证是在同一个上下文中执行多条命令了。
2.代码流程
-
判断当前root状态,如果没有root直接返回,避免消耗系统资源
if (! GTFrameUtils.isHasSu()){ return;}
-
计算一个周期内的帧率数据,由于比较简单(除了在1中surfaceflinger数据存疑外),直接把核心代码发出来
startTime = System.nanoTime();if (testCount == 0) { try { lastFrameNum = getFrameNum(); } catch (IOException e) { e.printStackTrace(); }}int currentFrameNum = 0;try { currentFrameNum = getFrameNum();} catch (IOException e) { e.printStackTrace();}int FPS = currentFrameNum - lastFrameNum;if (realCostTime > 0.0F) { int fpsResult = (int) (FPS * 1000 / realCostTime); defaultClient.setOutPara("FPS", fpsResult);}lastFrameNum = currentFrameNum;testCount += 1;
-
帧率获取的部分也发一下。另外感谢@codeskyblue 的指点,
service call SurfaceFlinger 1013
这个命令是获取系统的总的刷新帧率(返回的是16进制)
public static synchronized int getFrameNum() throws IOException { String frameNumString = ""; String getFps40 = "service call SurfaceFlinger 1013"; if (process == null) { process = Runtime.getRuntime().exec("su"); os = new DataOutputStream(process.getOutputStream()); ir = new BufferedReader( new InputStreamReader(process.getInputStream())); } os.writeBytes(getFps40 + "\n"); os.flush(); String str = ""; int index1 = 0; int index2 = 0; while ((str = ir.readLine()) != null) { if (str.indexOf("(") != -1) { index1 = str.indexOf("("); index2 = str.indexOf(" "); frameNumString = str.substring(index1 + 1, index2); break; } } int frameNum; if (!frameNumString.equals("")) { frameNum = Integer.parseInt(frameNumString, 16); } else { frameNum = 0; } return frameNum;}
五.GT性能测试方案之流畅度测试
1.简要流程
腾讯的流畅度测试比较简单粗暴,测试方式是通过初始化choreographer日志级别,生成Choreographer日志来得到当前操作的丢帧。通过一系列计算后来计算流畅度。
2.测试方法
-
执行
setprop debug.choreographer.skipwarning 1
View.OnClickListener button_write_property = new View.OnClickListener() { @Override public void onClick(View v) { String cmd = "setprop debug.choreographer.skipwarning 1"; ProcessBuilder execBuilder = new ProcessBuilder("su", "-c", cmd); execBuilder.redirectErrorStream(true); try { execBuilder.start(); } catch (IOException e) { e.printStackTrace(); } } };
-
执行
getprop debug.choreographer.skipwarning
判断,为1则可以进行测试
View.OnClickListener button_check_status = new View.OnClickListener() { @Override public void onClick(View v) { String cmd = "getprop debug.choreographer.skipwarning"; ProcessBuilder execBuilder = new ProcessBuilder("sh", "-c", cmd); execBuilder.redirectErrorStream(true); try { TextView textview = (TextView) findViewById(R.id.textviewInformation); Process p = execBuilder.start(); InputStream is = p.getInputStream(); InputStreamReader isr = new InputStreamReader(is); BufferedReader br = new BufferedReader(isr); Boolean flag = false; String line; while ((line = br.readLine()) != null) { if (line.compareTo("1") == 0) { flag = true; break; } } if (flag) { textview.setText("OK"); } else { textview.setText("NOT OK"); } } catch (IOException e) { e.printStackTrace(); } } };
-
执行
adb logcat -v time -s Choreographer:I *:S
-
过滤获取当前pid丢帧值
protected void onHandleIntent(Intent intent) { try { String str = intent.getStringExtra("pid"); int pid = Integer.parseInt(str); List<String> args = new ArrayList<String>(Arrays.asList("logcat", "-v", "time", "Choreographer:I", "*:S")); dumpLogcatProcess = RuntimeHelper.exec(args); reader = new BufferedReader(new InputStreamReader(dumpLogcatProcess.getInputStream()), 8192); String line; while ((line = reader.readLine()) != null && !killed) { // filter "The application may be doing too much work on its main thread." if (!line.contains("uch work on its main t")) { continue; } int pID = LogLine.newLogLine(line, false).getProcessId(); if (pID != pid){ continue; } line = line.substring(50, line.length() - 71); Integer value = Integer.parseInt(line.trim()); SMServiceHelper.getInstance().dataQueue.offer(value); } } catch (IOException e) { Log.e(TAG, e.toString() + "unexpected exception"); } finally { killProcess(); } }
-
数据处理得到sm值 腾讯这边的处理方案是:当丢帧<60时,流畅度SM =60-frame; 当丢帧frame>60时,流畅度SM = 60-frame%60。不过这种处理方式是有问题的。在这里要先说下流畅度计算的原理:
-
VSync机制可以通过其Loop来了解当前App最高绘制能力,固定每隔16.6ms执行一次,这样最高的刷新的帧率就控制在60FPS以内,Choreographer日志可以打印当前丢帧数,因此通过计算,得到当前APP的流畅度。
-
而计算这样来计算可能会更加准确(个人看法,欢迎讨论): SM= 60-丢帧frame/每两行同一线程的丢帧时间差(单位:s),如果只关心UI线程,那就只需要统计UI线程即可。
-
while (true) { if (pause) { break; } int x = count.getAndSet(0); // 卡顿大于60时,要将之前几次SM计数做修正 if (x > 60) { int n = x / 60; int v = x % 60; TagTimeEntry tte = OpPerfBridge.getProfilerData(key); int len = tte.getRecordSize(); // 补偿参数 int p = n; //Math.min(len, n); /* * n > len是刚启动测试的情况,日志中的亡灵作祟,这种情况不做补偿; * 并且本次也记为60。本逻辑在两次测试间会清理数据的情况生效。 */ if (n > len) { globalClient.setOutPara(key, 60);// globalClient.setOutPara(SFKey, 0); } else { for (int i = 0; i < p; i++) { TimeEntry te = tte.getRecord(len - 1 - i); te.reduce = 0; } globalClient.setOutPara(key, v);// globalClient.setOutPara(SFKey, 60 - v); } } else { int sm = 60 - x; globalClient.setOutPara(key, sm);// globalClient.setOutPara(SFKey, x); }
六.GT性能测试方案之流量测试
1.简要流程
流量测试有三种方案,默认采用方案1
-
通过读取
"/proc/uid_stat/" + uid + "/tcp_snd"
获取发送跟接收流量 -
直接调用android的
api:TrafficStats.getUidTxBytes(uid)
来获取流量数据(该方法号称是获取到指定 uid 发送流量的总和,但实测情况是只有 tcp 层的流量) -
第三种方案居然空在那里,那实际上只有两种方案
2.代码流程
-
初始化流量
public void initProcessNetValue(String pName) { p_t_base = getOutOctets(pName); p_r_base = getInOctets(pName); p_t_add = 0; p_r_add = 0;}
其中getOutOctets/getInOctets具体对应什么方法,需要看设备是不是支持uid流量数据获取
-
获取增加的流量
public String getProcessNetValue(String pName) { StringBuffer sb = new StringBuffer(); java.text.DecimalFormat df = new java.text.DecimalFormat("#.##"); p_t_cur = getOutOctets(pName); p_r_cur = getInOctets(pName); p_t_add = (p_t_cur - p_t_base) / B2K; p_r_add = (p_r_cur - p_r_base) / B2K; sb.append("t"); sb.append(df.format(p_t_add)); sb.append("KB|r"); sb.append(df.format(p_r_add)); sb.append("KB"); return sb.toString();}
-
矫正处理
// modify on 20120616 过滤有的手机进程流量偶尔输出负数的情况if ((nowT != lastT || nowR != lastR) && nowT >= 0 && nowR >= 0) { OpPerfBridge.addHistory(op, value, new long[]{(long) nowT, (long) nowR});}return value;
七.GT性能测试方案之电量测试
1.简单流程
-
关注指标:
电量测试关注的指标有四个: 电流,电压,电量跟温度。 -
数据获取方式:
通过ReadPowerTimerTask
任务去set关注的电量指标,当update方法调用时,才把数据set进去。电量数据调用的系统命令/sys/class/power_supply/battery/uevent
-
具体流程:
-
接收
"com.tencent.wstt.gt.plugin.battery.startTest"
广播后,update各个指标 -
注册跟设置出参,并设置刷新频率跟初始化屏幕电量
-
开启定时任务
ReadPowerTimerTask
,这个任务的作用就是去获取/sys/class/power_supply/battery/uevent
下的电量数据并解析,最后设置出参。刷新频率默认是250ms一次 -
最后stop时,销毁异步定时任务
-
2.代码流程
整个生命周期如下,当BATTERY_START_TEST
行为被捕获时,开始执行电量测试
String action = intent.getAction();if (action == null) return;if (action.equals(BATTERY_START_TEST)) { int refreshRate = intent.getIntExtra("refreshRate", 250); int brightness = intent.getIntExtra("brightness", 100); boolean updateI = intent.getBooleanExtra("I", true); GTBatteryEngine.getInstance().updateI(updateI); boolean updateU = intent.getBooleanExtra("U", false); GTBatteryEngine.getInstance().updateU(updateU); boolean updateT = intent.getBooleanExtra("T", false); GTBatteryEngine.getInstance().updateT(updateT); boolean updateP = intent.getBooleanExtra("P", false); GTBatteryEngine.getInstance().updateP(updateP); GTBatteryEngine.getInstance().doStart(refreshRate, brightness);} else if (action.equals(BATTERY_END_TEST)) { GTBatteryEngine.getInstance().doStop();}
-
update操作很简单,只是注册跟set出参
public void updateI(boolean isChecked){ if (isChecked) { globalClient.registerOutPara(GTBatteryEngine.OPI, "I"); globalClient.setOutparaMonitor(GTBatteryEngine.OPI, true); } else { globalClient.unregisterOutPara(GTBatteryEngine.OPI); } state_cb_I = isChecked; GTPref.getGTPref().edit().putBoolean(GTBatteryEngine.KEY_I, isChecked).commit(); for (BatteryPluginListener listener : listeners) { listener.onUpdateI(isChecked); }}
-
初始化操作略过,这里展示异步任务,处理流程直接看下面的关键代码即可,方法在
GTBatteryEngine.java
中
timer = new Timer(true);timer.schedule(new ReadPowerTimerTask(), refreshRate, refreshRate);@Overridepublic void run() {
BufferedReader br = null;
try {
FileReader fr = new FileReader(f);
br = new BufferedReader(fr);
String line = "";
while((line = br.readLine()) != null){
int found = 0;
if (line.startsWith("POWER_SUPPLY_VOLTAGE_NOW="))
{
U = line.substring(line.lastIndexOf("=") + 1);
// since 2.1.1 从μV转成mV
long volt = Long.parseLong(U) / 1000;
globalClient.setOutPara(OPU, volt + "mV");
OutPara op = globalClient.getOutPara(OPU);
if (null != op)
{
OpPerfBridge.addHistory(op, U, volt);
}
found++;
}
if (line.startsWith("POWER_SUPPLY_CURRENT_NOW="))
{
I = line.substring(line.lastIndexOf("=") + 1);
// since 2.1.1 从μA转成mA since 2.2.4 华为本身就是mA
long current = Long.parseLong(I);
if (isHuawei)
{
current = -current;
}
else if (isLGg3)
{
current = current >> 1; // 经验值估算LG g3的数据除以2后比较接近真实
}
else
{
current = current / 1000;
}
globalClient.setOutPara(OPI, current + "mA");
OutPara op = globalClient.getOutPara(OPI);
if (null != op)
{
OpPerfBridge.addHistory(op, I, current);
}
found++;
}
if (line.startsWith("POWER_SUPPLY_CAPACITY="))
{
String lastBattery = POW;
POW = line.substring(line.lastIndexOf("=") + 1);
if (! lastBattery.equals(POW)) // 电池百分比变化了
{
if (startBattry != -1)
{
lastBatteryChangeTime = (System.currentTimeMillis() - startBattry)/1000 + "s";
String tempValue = POW + "% | -1% time:" + lastBatteryChangeTime;
globalClient.setOutPara(OPPow, tempValue);
GTLog.logI(LOG_TAG, tempValue);
// 将电量加入历史记录
OutPara op = globalClient.getOutPara(OPPow);
if (null != op)
{
OpPerfBridge.addHistory(op, tempValue, Long.parseLong(POW));
}
}
startBattry = System.currentTimeMillis();
}
globalClient.setOutPara(OPPow, POW + "% | -1% time:" + lastBatteryChangeTime);
found++;
}
if (line.startsWith("POWER_SUPPLY_TEMP="))
{
TEMP = line.substring(line.lastIndexOf("=") + 1);
int iTemp = Integer.parseInt(TEMP);
iTemp = iTemp/10;
if (iTemp > -273)
{
TEMP = iTemp + "℃";
}
globalClient.setOutPara(OPTemp, TEMP);
OutPara op = globalClient.getOutPara(OPTemp);
if (null != op && iTemp != INT_TEMP)
{
OpPerfBridge.addHistory(op, TEMP, iTemp);
GTLog.logI(LOG_TAG, TEMP);
INT_TEMP = iTemp;
}
found++;
}
if (found >= 4)
{
return;
}
}
} catch (Exception e) {
doStop();
}
finally
{
FileUtil.closeReader(br);
}}