2018年12月25日,微信团队自研的APM利器,Matrix正式开源了。java
Matrix 是一款微信研发并平常使用的 APM (Application Performance Manage) ,当前主要运行在 Android 平台上。Matrix 的目标是创建统一的应用性能接入框架,经过对各类性能监控方案快速集成,对性能监控项的异常数据进行采集和分析,输出相应问题的分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。android
在精读了Matrix的源码以后,我发出了赞叹和吐槽两种声音。值得赞赏的是,这个APM框架的设计思路确实鬼斧神工,有不少值得Android开发者学习的地方,深刻了解它,可以扩宽咱们的编程视野。 使人吐槽的就是,从文档的丰富性,代码的可读性,代码的注释量,开源的一条龙服务等方面讲,他们作的还不太好。 做为国内的顶尖开发团队,这些方面与国外的顶尖开源开发团队仍是有不小的差距。git
目前Matrix的集成确实很方便,参考官方文档,大概10分钟左右就能集成到项目中。可是它有一个硬伤,对开发者很不友好,它集成很方便,可是分析和定位问题比较困难,开发者须要搭建一套数据采集和分析系统,大大地增长了开发者的使用门槛。 目前它在github上的star量是8.5k。若是微信团队能把采集系统和数据分析系统也开源出来,用户的使用门槛将会大大下降,相信star量也能更上一个台阶。github
吐槽归吐槽。吐槽不是目的。但愿国内的开发者能够更真心实意地作出更好用的开源产品。吐槽完毕,接下来我将聊聊Matrix官方文档中没有讲到的一些设计细节。真的很惊艳。若是你对Matrix曾经有任何偏见,不妨跟随我打破偏见,突破微信团队给咱们设置的使用门槛。sql
Matrix 当前监控范围包括:应用安装包大小,SQLite 操做优化,帧率变化,卡顿,启动耗时,页面切换耗时,慢方法,文件读写性能,I/O 句柄泄漏, 内存泄漏等。编程
更多详细介绍请移步Matrix简介json
本文将着重讲解检测慢方法的实现原理,以及数据格式分析,数据格式是Matrix的重中之重,不管是采集过程生成数据,仍是分析过程解析数据,都须要熟练理解数据格式。其它系列的文章随着我对Matrix的理解程度加深,也会陆续在公众号发布,敬请关注。数组
官方Demo,TestTraceMainActivity#testJankiess(View view)模拟在主线程调用方法超过700ms的场景。Matrix中慢方法的默认阈值是700ms。用户可配置。对应的字段是微信
//Constants.javamarkdown
public static final int DEFAULT_EVIL_METHOD_THRESHOLD_MS = 700;
点击EVIL_METHOD按钮,会调用testJankiess方法。打印Log以下
乍一看,有点丈二的和尚摸不着头脑。出现这样的日志,说明主线程调用时长超过了700ms。把日志中content对应的json格式化,获得以下结果:
{
"machine":"UN_KNOW",
"cpu_app":0,
"mem":1567367168,
"mem_free":851992,
"detail":"NORMAL",
"cost":2262,
"usage":"0.35%",
"scene":"sample.tencent.matrix.trace.TestTraceMainActivity",
"stack":"0,1048574,1,2262\n
1,117,1,2254\n
2,121,1,2254\n
3,124,1,384\n
4,125,1,165\n
5,126,1,21\n
5,127,1,21\n
5,128,1,19\n
4,129,1,24\n
3,130,1,65\n
4,131,1,21\n
4,132,1,6\n
4,133,1,8\n
3,134,1,1004\n",
"stackKey":"121|",
"tag":"Trace_EvilMethod",
"process":"sample.tencent.matrix",
"time":1620523013050
}
复制代码
关于数据格式,官方也有一篇文章介绍,github.com/Tencent/mat…
本文重点关注stack字段。它的功能是上报对应的堆栈。可是堆栈中为啥是一堆阿拉伯数字呢?先让咱们从头提及了。
假设有方法A。我想计算它花费的时间。咱们通常会这样作
public void A() {
long startTime = SystemClock.uptimeMillis()
SystemClock.sleep(1000);
long endTime = SystemClock.uptimeMillis()
System.out.println("cost time " + (endTime-startTime));
}
复制代码
对于单个方法咱们能够这样作。可是若是我想给Android项目中全部的方法都计算调用花费时,咱们须要用到字节码插桩技术。在全部的方法开始处和结束处,添加记录时间的代码。而Matrix也正是使用插桩技术来计算方法的时间调用的。
项目工程中TestTraceMainActivity的A方法
使用Jadx工具反编译apk中的TestTraceMainActivity。发现A方法先后增长了AppMethoBeat.i(121)和AppMethoBeat.o(121)
i/o 方法参数121是什么意思呢?
gradle插件,在处理方法时,会将方法名与从1开始递增的数字对应起来。咱们打开app/build/outputs/mapping/debug/methodMapping.txt文件。从图片咱们能够看到121对应的方法名是sample.tencent.matrix.trace.TestTraceMainActivity A ()V
前文堆栈中的数字0,1048574,1,2262\n 第二列1048574对应的就是方法名对应的数字。这么作的好处是,数据采集节省流量。
有方法调用以下,假设A方法调用耗时1000ms。如何可以肯定调用栈中哪一个子方法的调用最耗时?
public void A() {
B();
C();
D();
}
复制代码
Matrix框架已经实现了调用栈耗时监测,具体分析我放到后面讲解。重点就是后文6.2章节
依赖主线程Looper,监控每次dispatchMessage的执行耗时
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
复制代码
主线程全部执行的任务都在 dispatchMessage 方法中派发执行完成,咱们经过 setMessageLogging 的方式给主线程的 Looper 设置一个 Printer ,由于 dispatchMessage 执行先后都会打印对应信息。咱们能够计算出执行先后的时间花费。
本文只是简单介绍Matrix的插桩技术。浅尝辄止。实现插桩的代码是com/tencent/matrix/trace/MethodTracer.java,它的内部类TraceMethodAdapter实现了AppMethoBeat.i()和AppMethoBeat.o()的插入功能。
第三节提到的慢方法演示,它的代码调用以下。
public void testJankiess(View view) {
A();
}
private void A() {
B();
H();
L();
SystemClock.sleep(800);
}
private void B() {
C();
G();
SystemClock.sleep(200);
}
private void C() {
D();
E();
F();
SystemClock.sleep(100);
}
private void D() {
SystemClock.sleep(20);
}
private void E() {
SystemClock.sleep(20);
}
private void F() {
SystemClock.sleep(20);
}
private void G() {
SystemClock.sleep(20);
}
private void H() {
SystemClock.sleep(20);
I();
J();
K();
}
private void I() {
SystemClock.sleep(20);
}
private void J() {
SystemClock.sleep(6);
}
private void K() {
SystemClock.sleep(10);
}
private void L() {
SystemClock.sleep(1000);
}
复制代码
它对应的methodMapping文件以下:
117,1,sample.tencent.matrix.trace.TestTraceMainActivity testJankiess (Landroid.view.View;)V
121,2,sample.tencent.matrix.trace.TestTraceMainActivity A ()V
122,4,sample.tencent.matrix.battery.TestBatteryActivity onDestroy ()V
123,9,sample.tencent.matrix.sqlitelint.TestSQLiteLintHelper qualityClose (Ljava.io.Closeable;)V
124,2,sample.tencent.matrix.trace.TestTraceMainActivity B ()V
125,2,sample.tencent.matrix.trace.TestTraceMainActivity C ()V
126,2,sample.tencent.matrix.trace.TestTraceMainActivity D ()V
127,2,sample.tencent.matrix.trace.TestTraceMainActivity E ()V
128,2,sample.tencent.matrix.trace.TestTraceMainActivity F ()V
129,2,sample.tencent.matrix.trace.TestTraceMainActivity G ()V
130,2,sample.tencent.matrix.trace.TestTraceMainActivity H ()V
131,2,sample.tencent.matrix.trace.TestTraceMainActivity I ()V
132,2,sample.tencent.matrix.trace.TestTraceMainActivity J ()V
133,2,sample.tencent.matrix.trace.TestTraceMainActivity K ()V
134,2,sample.tencent.matrix.trace.TestTraceMainActivity L ()V
1048574,1,android.os.Handler dispatchMessage (Landroid.os.Message;)V
复制代码
以上方法调用能够概括成如下树形结构:
请注意,该树形图,是我直接根据调用次序画出来的,程序是如何根据调用次序生成调用栈树呢?
编译期已经对全局的函数进行插桩,在运行期间每一个函数的执行先后都会调用 MethodBeat.i/o 的方法,若是是在主线程中执行,则在函数的执行先后获取当前距离 MethodBeat 模块初始化的时间 offset (为了压缩数据,存进一个long类型变量中), 并将当前执行的是 MethodBeat i或者o、mehtod id 及时间 offset,存放到一个 long 类型变量中,记录到一个预先初始化好的数组 long[] 中 index 的位置(预先分配记录数据的 buffer 长度为 100w,内存占用约 7.6M)。数据存储以下图:
//AppMethodBeat.java
private static long[] sBuffer = new long[Constants.BUFFER_SIZE];
//Constants.java
public static final int BUFFER_SIZE = 100 * 10000; // 7.6M
复制代码
AppMethodBeat.i/o方法最终会调用到
//AppMethodBeat.java
private static void mergeData(int methodId, int index, boolean isIn) {
if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
}
long trueId = 0L;
if (isIn) {
trueId |= 1L << 63;
}
trueId |= (long) methodId << 43;
trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
sBuffer[index] = trueId;
checkPileup(index);
sLastIndex = index;
}
复制代码
testJankiess方法调用,经过mergeData方法,最终填充sBuffer以下图:
EvilMethodTracer.java dispatchEnd表示主线程执行结束,若是耗时超过阈值,会在MatrixHandlerThread中执行AnalyseTask,分析调用栈的耗时状况。
1. 调用addMethodItem方法,将后序遍历结果push到栈中
structuredDataToStack()中,若是是out方法,将会出栈,而且push到栈底。获得结果以下:
若是咱们将队列反转过来,对照手画的树咱们可知结果是后序遍历。
126 127 128 125 129 124 131 132 133 130 134 121 117 1048574
2. 调用stackToTree()将队列转换成多叉树。
结果和手画的树同样
3. 调用treeToStack(),得到先序遍历结果 结果以下
Matrix默认最多上传30个堆栈。若是堆栈调用超过30条,须要裁剪堆栈。裁剪策略以下:
从后往前遍历先序遍历结果,若是堆栈大小大于30,则将执行时间小于5*总体遍历次数的节点剔除掉
最多总体遍历60次,每次总体遍历,比较时间增长5ms
若是遍历了60次,堆栈大小仍是大于30,将后面多余的删除掉
若是某个方法调用时间大于整个主线程调用时长的0.3倍,会将该方法id记录到list中,最后排序,过滤。生成traceKey。
reportBuilder就是最终上报的堆栈信息。与文章开头的日志信息一致
日志解析结果以下:
文中如有错误,请留言交流。成文不易,写了一成天。若是以为还不错,分享给你的朋友吧。若是须要解析日志文件的源码,欢迎关注“字节小站”同名公众号。