监控模块解析
概述
solopi的监控部分主要在工程目录src的shared下,部分对性能要求较高的监控指标采用c语言收集,利用jni技术提供调用接口。
整体框架解耦性较高,其基础性能数据监控代码在display目录下。
调用链解析
displayable接口作为基础的性能数据监控接口,被具体的性能监控实现类继承实现,具体的文件在目录display\items\下,共有6个文件,实现了对电量、cpu数据、fps数据、内存数据等的监控。
每个displayable实现类由注解DisplayItem记录属性,由DisplayItemInfo解释和使用。
displayable实现类由DisplayProvider类进行服务包装,统一对外提供运行入口和持续收集能力。
具体的实现方式是,DisplayProvider提供了一个对外的启动入口,startDisplay(name)方法,传入的参数是displayable实现类的TAG属性,该属性记录了实现类的类名称,从而可以运用反射原理,对选中的实现类的实现启动。
完整的调用链关系示例如下:
PerformanceActivity加载性能监控列表mFlootListView;
---------------------------->
mFlootListView绑定性能监控适配器PerformFloatAdapter,在适配器内的onclick()方法内,调用displayManager.updateRecordingItems方法;
---------------------------->
updateRecordingItems通过Provider.startDisplay和Provider.stopDisplay方法实现对监控服务的启停;
---------------------------->
在startDisplay方法内,传入监控实现类的tagname,通过反射动态调用监控服务。
adb提权
基本原理
由于在性能数据收集中,一些数据的采集会受限于android系统的版本(例如android 7 以上,无法直接读取/proc/stat文件)或者具体机型(例如 oppo的手机,即使是android 7也无法直接读取到/proc/stat文件)导致收集失败,因此,除了传统的机内读取文件等形式,solopi还补充了通过adb执行命令的形式来收集数据。
在solopi中,adb功能主要分为两个部分,底层的实现(密钥生成、连接建立等等)引用自开源项目 adblib,git地址:https://github.com/cgutman/AdbLib ,其使用说明和api文档很全,这里不再阐述。还有部分建立在底层之上,是对adb命令的封装和执行,主要集中在CmdLine和CmdTools里。
其基本原理是,在设备上建立与守护进程adbd的连接,从而可以在设备上执行adb shell命令。
adb连接过程
以点击录制工具时为例,简述adb的连接过程。
-
screenRecordBtn.setOnClickListener对录制按钮设置点击监听事件;
-
点击录制工具按钮后,方法PermissionUtil.grantHighPrivilegePermissionAsync(new CmdTools.GrantHighPrivPermissionCallback() {...检测是否具备adb连接条件,如果不具备,则提示“请在命令行执行 adb tcpip 5555”;
-
用户执行命令后,设备的adbd守护进程开始监听端口5555,准备建立连接;
-
再次点击录制工具按钮,重新检测后,执行 CmdTools.generateConnection(),建立adb连接。
cpu性能数据收集
cpu的性能数据收集方法在display目录下的CPUTools文件内,下面是该文件的解析。
原理概述
solopi内,cpu的主要实现原理只有一个(但是途径有两个),就是通过读取/prpo/stat和/proc/pid/stat文件来计算出所要参数。
/proc/pid/stat和/proc/stat这两个文件网上的资料很多,这里就不过多阐述了,主要讲一下具体的算法。
stat读取途径
solopi内有两种读取stat文件的途径,分别是系统内直接读取(由c实现)和adb命令读取。
原因主要是在安卓7.0以上,无法直接读取stat文件,所以这里做了系统判断,如果是7.0以上的或者是特殊机型,那么使用adb途径读取文件;如果是7.0以下的,那么直接使用c进行读取,使用c来读取的好处是更快资源消耗更低,使用adb是不得已的用法。
整体cpu使用率的计算
计算cpu总量的方法是getUsage(),
cpu总量的部分计算代码(已加注释)如下:
try {
currentJiffies = Long.parseLong(cpuInfos[1]) + Long.parseLong(cpuInfos[2]) + Long.parseLong(cpuInfos[3])
+ Long.parseLong(cpuInfos[4]) + Long.parseLong(cpuInfos[5]) + Long.parseLong(cpuInfos[6])
+ Long.parseLong(cpuInfos[7]);// 相加得到当前使用总量
currentIdle = Long.parseLong(cpuInfos[4]);// 当前的空闲用量
} catch (ArrayIndexOutOfBoundsException e) {
LogUtil.e(TAG, "ArrayIndexOutOfBoundsException" + e.getMessage(), e);
return -1f;
} catch (NumberFormatException e) {
LogUtil.e(TAG, "CPU行【%s】格式无法解析", load);
}
if (lastJiffies == 0 || lastIdle == 0) {
lastJiffies = currentJiffies; //currentJiffies是总使用量; lastJiffies 最后记录的总使用量
lastIdle = currentIdle; //currentIdle是空闲时间;lastIdle 最后记录的空闲时间
return -1f;
} else {
long gapJiffies = currentJiffies - lastJiffies; // gapJiffies 间隔时间段算出的间隔总量
long gapIdle = currentIdle - lastIdle; // gapIdle 间隔时间段算出的空闲总量
lastJiffies = currentJiffies; // 刷新一下最后用量
lastIdle = currentIdle;
if (gapIdle < 0 || gapJiffies < 0) {
return -1f;//数据有问题返回-1f
}
LogUtil.d(TAG, "CPU占用率:" + (gapJiffies - gapIdle) / (float) gapJiffies);
return 100 * (gapJiffies - gapIdle) / (float) gapJiffies;
}
可以看到,solopi的整体cpu占用率计算公式是:100 * (gapJiffies - gapIdle) / (float) gapJiffies,即(总占用-空闲占用)/总占用
指定进程cpu占用率计算
指定进程的cpu占用率计算的方法是getPidsUsage(),
该方法主要使用命令“grep cpu /proc/stat && cat /proc/pid/stat”,执行后的结果存在数组内,分为两个部分,第一部分用于计算总体占用量,这个和上面的计算过程基本一致;第二部分用于计算进程的占用量,部分计算代码如下:
/** * 应用CPU处理 * /proc/<b>pid</b>/stat 应用占用情况 * 2265 (id.XXX) S 610 609 0 0 -1 1077952832 130896 1460 185 0 683 329 3 10 14 -6 63 0 1982194 2124587008 28421 18446744073709551615 1 1 0 0 0 0 4612 0 1073798392 18446744073709551615 0 0 17 3 0 0 0 0 0 0 0 0 0 0 0 0 0 * 第14-17位之和为应用占用CPU时间之和 */
SparseArray<Float> appResult = new SparseArray<>(pids.length + 1);
// 第一行是全局cpu数据
String[] splitLines = new String[origin.length - 1];
System.arraycopy(origin, 1, splitLines, 0, origin.length - 1);
// 处理每行获取到的数据
SparseArray<Long> newAppProcessTime = new SparseArray<>(appProcessTime.size() + 1);
for (String line: splitLines) {
String[] processInfos = line.trim().split("\\s+");
LogUtil.d(TAG, Arrays.toString(processInfos));
// 获取失败的状态
if (processInfos.length < 17) {
continue;
}
try {
int pid = Integer.parseInt(processInfos[0]);
Long pidProcessTime = Long.parseLong(processInfos[13]) + Long.parseLong(processInfos[14]) + Long.parseLong(processInfos[15]) + Long.parseLong(processInfos[16]);
Long lastProcessTime = appProcessTime.get(pid);
newAppProcessTime.put(pid, pidProcessTime);
// 如果没有上次记录,则跳过
if (lastProcessTime == null) {
continue;
}
// 计算APP进程处理时间
Long processRunning = pidProcessTime - lastProcessTime;
appResult.put(pid, 100 * (processRunning / (float) cpuRunning));
} catch (NumberFormatException e) {
LogUtil.e(TAG, "Format for string: " + line + " failed", e);
}
}
可以看到,进程单独的用量的公式是:
(processRunning / (float) cpuRunning)
(喜欢请转发,谢谢!)
加入爱测未来qq群,获取更专业的技术知识分享:
274166295 (爱测未来二群)
610934609 (爱测未来三群)
195730410 (爱测未来四群)
更多精彩文章:
性能分析之激情的过程无奈的结局谈谈从事IT测试行业的我,对于买房买车有什么样的感受
性能平台之Jmeter通过influxdb在Grafana中的数据展现逻辑
——————————————————
爱测未来公众号 WX:itest_forever
测试之道 | 测试技术
长按识别二维码,关注爱测未来公众号,了解更多精彩内容