observer.doFrame(AppMethodBeat.getVisibleScene(), startNs, endNs, isVsyncFrame, intendedFrameTimeNs, queueCost[CALLBACK_INPUT], queueCost[CALLBACK_ANIMATION], queueCost[CALLBACK_TRAVERSAL]);
}
}
}
dispatchTimeMs[3] = SystemClock.currentThreadTimeMillis();
dispatchTimeMs[1] = System.nanoTime();
AppMethodBeat.o(AppMethodBeat.METHOD_ID_DISPATCH);
synchronized (observers) {
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
observer.dispatchEnd(dispatchTimeMs[0], dispatchTimeMs[2], dispatchTimeMs[1], dispatchTimeMs[3], token, isVsyncFrame);
}
}
}
this.isVsyncFrame = false;
if (config.isDevEnv()) {
MatrixLog.d(TAG, “[dispatchEnd#run] inner cost:%sns”, System.nanoTime() - traceBegin);
}
}
UIThreadMonitor则不太一样,其中dispatchEnd方法有其中LooperMonitor所接受到的。
而LooperMonitor他通过主线程的Looper的setMessageLogging方法设置一个LooperPrinter。dispatchEnd在主线程的方法执行结束之后,通过反射Choreographer获取当前的绘制的Vsync和渲染时长。最后当IdleHandler被触发的时候,则重置LooperPrinter时间的方式,从而避免主线程闲置状况下方法耗时计算出问题。
这部分源代的传送门Matrix LoopMonitor
https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-trace-canary/src/main/java/com/tencent/matrix/trace/core/LooperMonitor.java
为什么要绕一个大圈子来监控Fps呢?这么写的好处是什么呢?
我特地去翻查了下Matrix官方的wiki *https://github.com/Tencent/matrix/wiki/Matrix-Android-TraceCanary *,Martix参考了BlockCanary的代码,通过结合了下Choreographer和BlockCanary,当出现卡顿帧的时候获取当前的主线程卡顿的堆栈,然后通过LooperPrinter把当前的卡顿的堆栈方法输出,这样可以更好的辅助开发去定位卡顿问题,而不是直接告诉业务方你的页面卡顿了。
采样分析
文章开始抛出过一个问题,如果采集的每一个数据都上报首先会对服务器产生巨大的无效数据压力,其次也会有很多无效的数据上报,那么应该怎么做呢?
这一块我们参考了Matrix的代码,首先Fps数据不可能是实时上报的,其次最好能从一个时间段内的数据中筛选出有问题的数据,Matrix的Fps采集的有几个小细节其实做的很好。
-
延迟200毫秒.先收集200帧的数据,然后对其数据内容进行分析,筛选遍历出最大帧最小帧,以及平均帧,之后内存保存数据。
-
子线程处理数据,筛选遍历的操作移动到子线程,这样避免APM反倒造成App卡顿问题。
-
200毫秒的数据只是作为其中一个数据片段,Matrix的上报节点是以一个更长的时间段作为上报的,当时间超过1分钟左右的情况下,才会作为一个Issue片段上报。
-
前后台切换状态并不需要采集数据。
private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
long traceBegin = System.currentTimeMillis();
try {
final long jiter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jiter / frameIntervalNs);
droppedSum += dropFrame;
durationSum += Math.max(jiter, frameIntervalNs);
synchronized (listeners) {
for (final IDoFrameListener listener : listeners) {
if (config.isDevEnv()) {
listener.time = SystemClock.uptimeMillis();
}
if (null != listener.getExecutor()) {
if (listener.getIntervalFrameReplay() > 0) {
listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
} else {
listener.getExecutor().execute(new Runnable() {
@Override
public void run() {
listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
});
}
} else {
listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
…
}
}
}
}
上面是Matirx的源代码,其中我们可以看出listener.getIntervalFrameReplay() > 0当这个条件触发的情况下,listener会先做一次collection操作,当触发到一定的数据量之后,才会触发后续的逻辑。其次我们可以看到判断了null != listener.getExecutor(),所以这部分收集的操作被执行在线程池中。
private class FPSCollector extends IDoFrameListener {
private Handler frameHandler = new Handler(MatrixHandlerThread.getDefaultHandlerThread().getLooper());
Executor executor = new Executor() {
@Override
public void execute(Runnable command) {
frameHandler.post(command);
}
};
private HashMap<String, FrameCollectItem> map = new HashMap<>();
@Override
public Executor getExecutor() {
return executor;
}
@Override
public int getIntervalFrameReplay() {
return 200;
}
@Override
public void doReplay(List list) {
super.doReplay(list);
for (FrameReplay replay : list) {
doReplayInner(replay.focusedActivity, replay.startNs, replay.endNs, replay.dropFrame, replay.isVsyncFrame,
replay.intendedFrameTimeNs, replay.inputCostNs, replay.animationCostNs, replay.traversalCostNs);
}
}
public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames,
boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs,
long animationCostNs, long traversalCostNs) {
if (Utils.isEmpty(visibleScene)) return;
if (!isVsyncFrame) return;
FrameCollectItem item = map.get(visibleScene);
if (null == item) {
item = new FrameCollectItem(visibleScene);
map.put(visibleScene, item);
}
item.collect(droppedFrames);
if (item.sumFrameCost >= timeSliceMs) { // report
map.remove(visibleScene);
item.report();
}
}
}
private class FrameCollectItem {
String visibleScene;
long sumFrameCost;
int sumFrame = 0;
int sumDroppedFrames;
// record the level of frames dropped each time
int[] dropLevel = new int[DropStatus.values().length];
int[] dropSum = new int[DropStatus.values().length];
FrameCollectItem(String visibleScene) {
this.visibleScene = visibleScene;
}
void collect(int droppedFrames) {
float frameIntervalCost = 1f * UIThreadMonitor.getMonitor().getFrameIntervalNanos() / Constants.TIME_MILLIS_TO_NANO;
sumFrameCost += (droppedFrames + 1) * frameIntervalCost;
sumDroppedFrames += droppedFrames;
sumFrame++;
if (droppedFrames >= frozenThreshold) {
dropLevel[DropStatus.DROPPED_FROZEN.index]++;
dropSum[DropStatus.DROPPED_FROZEN.index] += droppedFrames;
} else if (droppedFrames >= highThreshold) {
dropLevel[DropStatus.DROPPED_HIGH.index]++;
dropSum[DropStatus.DROPPED_HIGH.index] += droppedFrames;
} else if (droppedFrames >= middleThreshold) {
dropLevel[DropStatus.DROPPED_MIDDLE.index]++;
dropSum[DropStatus.DROPPED_MIDDLE.index] += droppedFrames;
} else if (droppedFrames >= normalThreshold) {
dropLevel[DropStatus.DROPPED_NORMAL.index]++;
dropSum[DropStatus.DROPPED_NORMAL.index] += droppedFrames;
} else {
dropLevel[DropStatus.DROPPED_BEST.index]++;
dropSum[DropStatus.DROPPED_BEST.index] += Math.max(droppedFrames, 0);
}
}
}
这部分代码则是Matrix对于一个帧片段进行数据处理的逻辑。可以看出,collect方法内筛选出了最大最小等等多个纬度的数据,丰富了一个数据片段。这个地方数据越多越能帮助一个开发定位问题。
采集逻辑也参考了Matrix的这部分代码,但是在实际测试阶段发现了个小Bug,因为上报的是一个比较大的时间片段,用户切换了页面之后,会把上个页面的fps数据也当做下个页面的数据上报。
所以我们增加了一个ActivityLifeCycle,当页面发生变化的情况下进行一次数据上报操作。其次我们把Matrix内的前后台切换等逻辑也进行了一次调整,更换成更可靠的ProcessLifecycleOwner。
内存和Cpu的使用状况可以更好的帮我们检测线上用户的真实情况,而不是等到用户crash之后我们再去反推这个问题,可以根据页面维度筛选出不同的页面数据,方便开发分析对应的问题。
在已经获取到Fps的经验之后,我们在这个基础上增加了Cpu和Memory的数据收集。相对来说我们可以借鉴大量的采集逻辑,然后只要在获取关键性数据进行调整就好了。
-
数据在子线程中采集,避免采集数据卡顿主线程。
-
同时每秒采集一次数据,数据内容本地分析,计算峰值谷值均值
-
数据上报节点拆分,一定时间内,页面切换,生成一个数据。
-
合并Cpu和内存数据,作为同一个数据结构上报,优化数据流量问题。
Memory 数据采集
Memory的数据我们参考了下Dokit的代码,高低版本也有差异,高版本可以直接通过Debug.MemoryInfo()获取到内存的数据,低版本则需要通过ams获取到ActivityManager从中获取数据。
以下是性能采集的工具类同时采集了cpu数据,各位可以直接使用。
object PerformanceUtils {
private var CPU_CMD_INDEX = -1
@JvmStatic
fun getMemory(): Float {
val mActivityManager: ActivityManager? = Hasaki.getApplication().getSystemService(Context.ACTIVITY_SERVICE)
as ActivityManager?
var mem = 0.0f
try {
var memInfo: Debug.MemoryInfo? = null
if (Build.VERSION.SDK_INT > 28) {
// 统计进程的内存信息 totalPss
memInfo = Debug.MemoryInfo()
Debug.getMemoryInfo(memInfo)
} else {
//As of Android Q, for regular apps this method will only return information about the memory info for the processes running as the caller’s uid;
// no other process memory info is available and will be zero. Also of Android Q the sample rate allowed by this API is significantly limited, if called faster the limit you will receive the same data as the previous call.
val memInfos = mActivityManager?.getProcessMemoryInfo(intArrayOf(Process.myPid()))
memInfos?.firstOrNull()?.apply {
memInfo = this
}
}
memInfo?.apply {
val totalPss = totalPss
if (totalPss >= 0) {
mem = totalPss / 1024.0f
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return mem
}
/**
-
8.0以下获取cpu的方式
-
@return
*/
private fun getCPUData(): String {
val commandResult = ShellUtils.execCmd(“top -n 1 | grep ${Process.myPid()}”, false)
val msg = commandResult.successMsg
return try {
msg.split(“\s+”.toRegex())[CPU_CMD_INDEX]
} catch (e: Exception) {
“0.5%”
}
}
@WorkerThread
fun getCpu(): String {
if (CPU_CMD_INDEX == -1) {
getCpuIndex()
}
if (CPU_CMD_INDEX == -1) {
return “”
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
getCpuDataForO()
} else {
getCPUData()
}
}
/**
-
8.0以上获取cpu的方式
-
@return
*/
private fun getCpuDataForO(): String {
return try {
val commandResult = ShellUtils.execCmd(“top -n 1 | grep ${Process.myPid()}”, false)
var cpu = 0F
commandResult.successMsg.split(“\n”).forEach {
val cpuTemp = it.split(“\s+”.toRegex())
val cpuRate = cpuTemp[CPU_CMD_INDEX].toFloatOrNull()?.div(Runtime.getRuntime()
.availableProcessors())?.div(100) ?: 0F
cpu += cpuRate
}
NumberFormat.getPercentInstance().format(cpu)
} catch (e: Exception) {
“”
}
}
private fun getCpuIndex() {
try {
val process = Runtime.getRuntime().exec(“top -n 1”)
val reader = BufferedReader(InputStreamReader(process.inputStream))
var line: String? = null
while (reader.readLine().also { line = it } != null) {
line?.let {
line = it.trim { it <= ’ ’ }
line?.apply {
val tempIndex = getCPUIndex(this)
if (tempIndex != -1) {
CPU_CMD_INDEX = tempIndex
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getCPUIndex(line: String): Int {
if (line.contains(“CPU”)) {
val titles = line.split(“\s+”.toRegex()).toTypedArray()
for (i in titles.indices) {
if (titles[i].contains(“CPU”)) {
return i
}
}
}
return -1
}
}
Cpu采集
Cpu的数据采集代码也在上面,这部分代码相对来说也比较简单,复杂的地方在于命令行以及版本适配的问题。获取的时候也需要区分系统版本,高低版本均通过cmd命令获取,修复了低版本的CpuId获取失败的问题。然后优化了下DoKit的代码逻辑。ShellUtils则可以参考Blank写的工具类集合。
Fps,cpu,内存这些数据只能算是Apm中最简单的一个环节而已,Apm的实际目的是要更好的辅助开发人员去定位线上出现的问题状况,通过预警机制避免线上问题的产生以及对性能的监控。而后续还需要收集更多的用户行为数据等来辅助开发来更准确的定位线上的问题。