手写解析微信Matrix性能监控日志的工具

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 手写解析微信Matrix性能监控日志的工具

1. 前言


2018年12月25日,微信团队自研的APM利器,Matrix正式开源了。


Matrix 是一款微信研发并日常使用的 APM (Application Performance Manage) ,当前主要运行在 Android 平台上。Matrix 的目标是建立统一的应用性能接入框架,通过对各种性能监控方案快速集成,对性能监控项的异常数据进行采集和分析,输出相应问题的分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。


在精读了Matrix的源码之后,我发出了赞叹和吐槽两种声音。值得赞扬的是,「这个APM框架的设计思路确实鬼斧神工,有很多值得Android开发者学习的地方,深入了解它,能够扩宽我们的编程视野。」 令人吐槽的就是,「从文档的丰富性,代码的可读性,代码的注释量,开源的一条龙服务等方面讲,他们做的还不太好。」 作为国内的顶尖开发团队,这些方面与国外的顶尖开源开发团队还是有不小的差距。


目前Matrix的集成确实很方便,参考官方文档,大概10分钟左右就能集成到项目中。「但是它有一个硬伤,对开发者很不友好,它集成很方便,但是分析和定位问题比较困难,开发者需要搭建一套数据采集和分析系统,大大地增加了开发者的使用门槛。」  目前它在github上的star量是8.5k。如果微信团队能把采集系统和数据分析系统也开源出来,用户的使用门槛将会大大降低,相信star量也能更上一个台阶。


吐槽归吐槽。吐槽不是目的。希望国内的开发者可以更真心实意地做出更好用的开源产品。「吐槽完毕,接下来我将聊聊Matrix官方文档中没有讲到的一些设计细节。真的很惊艳。如果你对Matrix曾经有任何偏见,不妨跟随我打破偏见,突破微信团队给我们设置的使用门槛。」


2. Matrix功能简介


Matrix 当前监控范围包括:「应用安装包大小,SQLite 操作优化,帧率变化,卡顿,启动耗时,页面切换耗时,慢方法,文件读写性能,I/O 句柄泄漏, 内存泄漏等。」

640.png


更多详细介绍请移步Matrix简介


「本文将着重讲解检测慢方法的实现原理,以及数据格式分析,数据格式是Matrix的重中之重,无论是采集过程生成数据,还是分析过程解析数据,都需要熟练理解数据格式。其它系列的文章随着我对Matrix的理解程度加深,也会陆续在公众号发布,敬请关注。」

3. 慢方法演示


官方Demo,TestTraceMainActivity#testJankiess(View view)模拟在主线程调用方法超过700ms的场景。Matrix中慢方法的默认阈值是700ms。用户可配置。对应的字段是

//Constants.java

public static final int DEFAULT_EVIL_METHOD_THRESHOLD_MS = 700;

640.png


点击EVIL_METHOD按钮,会调用testJankiess方法。打印Log如下

640.png


乍一看,有点丈二的和尚摸不着头脑。出现这样的日志,说明主线程调用时长超过了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
}


关于数据格式,官方也有一篇文章介绍,

https://github.com/Tencent/matrix/wiki/Matrix-Android--data-format,感兴趣的同学可以去看看。


本文重点关注stack字段。它的功能是上报对应的堆栈。但是堆栈中为啥是一堆阿拉伯数字呢?先让我们从头说起了。

4.计算方法调用的时间花费


4.1 计算一个方法调用花费的时间


假设有方法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方法

640.png


使用Jadx工具反编译apk中的TestTraceMainActivity。发现A方法前后增加了「AppMethoBeat.i(121)和AppMethoBeat.o(121)」

640.png

「i/o 方法参数121是什么意思呢?」


4.2 121含义讲解


gradle插件,在处理方法时,会将方法名与从1开始递增的数字对应起来。「我们打开app/build/outputs/mapping/debug/methodMapping.txt文件。从图片我们可以看到121对应的方法名是sample.tencent.matrix.trace.TestTraceMainActivity A ()V」

640.png


前文堆栈中的数字0,1048574,1,2262\n 第二列1048574对应的就是方法名对应的数字。「这么做的好处是,数据采集节省流量。」

4.3 获取调用栈的耗时


有方法调用如下,假设A方法调用耗时1000ms。如何能够确定调用栈中哪个子方法的调用最耗时?

public void A() {
  B();
  C();
  D();
}

Matrix框架已经实现了调用栈耗时监测,具体分析我放到后面讲解。「重点就是后文6.2章节」

4.4 获取主线程耗时


依赖主线程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 执行前后都会打印对应信息。我们可以计算出执行前后的时间花费。」


5. Matrix如何实现插桩


本文只是简单介绍Matrix的插桩技术。浅尝辄止。实现插桩的代码是com/tencent/matrix/trace/MethodTracer.java,「它的内部类TraceMethodAdapter实现了AppMethoBeat.i()和AppMethoBeat.o()的插入功能。」

640.png

640.png


6. 慢方法监测的原理


6.1 手画调用栈树

第三节提到的慢方法演示,它的代码调用如下。

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

「以上方法调用可以归纳成以下树形结构:」

640.png

「请注意,该树形图,是我直接根据调用次序画出来的,程序是如何根据调用次序生成调用栈树呢?」


6.2 生成调用栈树


6.2.1 函数调用记录在队列中


编译期已经对全局的函数进行插桩,在运行期间每个函数的执行前后都会调用 MethodBeat.i/o 的方法,如果是在主线程中执行,则在函数的执行前后获取当前距离 MethodBeat 模块初始化的时间 offset 「(为了压缩数据,存进一个long类型变量中),」 并将当前执行的是 MethodBeat i或者o、mehtod id 及时间 offset,存放到一个 long 类型变量中,记录到一个预先初始化好的数组 long[] 中 index 的位置(预先分配记录数据的 buffer 长度为 100w,内存占用约 7.6M)。数据存储如下图:

640.png

//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如下图:」

640.png


6.2.2 主线程调用结束后判断是否超过阈值


EvilMethodTracer.java dispatchEnd表示主线程执行结束,如果耗时超过阈值,会在MatrixHandlerThread中执行AnalyseTask,分析调用栈的耗时情况。

640.jpg

6.2.3 分析堆栈


  1. 调用TraceDataUtils.structuredDataToStack()方法
  2. 调用TraceDataUtils.trimStack()方法
  3. 调用TraceDataUtils.getTreeKey()方法

640.png


6.2.4 将调用队列转换成树的先序遍历结果

640.png


「1. 调用addMethodItem方法,将后序遍历结果push到栈中」


structuredDataToStack()中,如果是out方法,将会出栈,并且push到栈底。得到结果如下:

640.png

如果我们将队列反转过来,对照手画的树我们可知结果是「后序遍历。」


126 127 128 125 129 124 131 132 133 130 134 121 117 1048574


「2. 调用stackToTree()将队列转换成多叉树。」

640.png

结果和手画的树一样

640.png


「3. 调用treeToStack(),获得先序遍历结果」

640.png


结果如下

640.png


6.2.5 裁剪调用堆栈


Matrix默认最多上传30个堆栈。如果堆栈调用超过30条,需要裁剪堆栈。裁剪策略如下:


  1. 从后往前遍历先序遍历结果,如果堆栈大小大于30,则将执行时间小于5*整体遍历次数的节点剔除掉
  2. 最多整体遍历60次,每次整体遍历,比较时间增加5ms
  3. 如果遍历了60次,堆栈大小还是大于30,将后面多余的删除掉

640.png



6.2.5 堆栈信息生成key

如果某个方法调用时间大于整个主线程调用时长的0.3倍,会将该方法id记录到list中,最后排序,过滤。生成traceKey。

640.png

6.2.6 根据裁剪后的先序遍历结果生成上报堆栈


reportBuilder就是最终上报的堆栈信息。与文章开头的日志信息一致

640.png

7. 解析日志


640.png


日志解析结果如下:

640.jpg


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5天前
|
存储 缓存 关系型数据库
redo log 原理解析
redo log 原理解析
11 0
redo log 原理解析
|
2月前
|
人工智能 小程序 Java
【工具】轻松解锁SQLite数据库,一窥微信聊天记录小秘密
本文介绍了一款名为PyWxDump的开源工具,它可以获取微信账户信息、解密SQLite数据库以查看和备份聊天记录。此工具适用于已登录电脑版微信的用户,通过GitHub下载后简单几步即可操作。适合对数据恢复感兴趣的开发者,但请注意合法合规使用并尊重隐私。
204 2
【工具】轻松解锁SQLite数据库,一窥微信聊天记录小秘密
|
2月前
|
小程序 开发者
微信开发者工具如何集成SVN、同时解决集成SVN无效果的问题
这篇文章介绍了如何在微信开发者工具中集成SVN版本控制系统,并提供了解决集成后无效果问题的方法。
微信开发者工具如何集成SVN、同时解决集成SVN无效果的问题
|
2月前
|
Java Shell Linux
【Linux入门技巧】新员工必看:用Shell脚本轻松解析应用服务日志
关于如何使用Shell脚本来解析Linux系统中的应用服务日志,提供了脚本实现的详细步骤和技巧,以及一些Shell编程的技能扩展。
31 0
【Linux入门技巧】新员工必看:用Shell脚本轻松解析应用服务日志
|
2月前
|
存储 运维 监控
深入Linux核心:文件系统与日志解析
【8月更文挑战第20天】
39 2
|
2月前
|
存储 监控 数据可视化
性能监控之JMeter分布式压测轻量日志解决方案
【8月更文挑战第11天】性能监控之JMeter分布式压测轻量日志解决方案
61 0
性能监控之JMeter分布式压测轻量日志解决方案
|
2月前
|
监控 Java 开发者
揭秘Struts 2性能监控:选对工具与方法,让你的应用跑得更快,赢在起跑线上!
【8月更文挑战第31天】在企业级应用开发中,性能监控对系统的稳定运行至关重要。针对流行的Java EE框架Struts 2,本文探讨了性能监控的工具与方法,包括商用的JProfiler、免费的VisualVM以及Struts 2自带的性能监控插件。通过示例代码展示了如何在实际项目中实施这些监控手段,帮助开发者发现和解决性能瓶颈,确保应用在高并发、高负载环境下稳定运行。选择合适的监控工具需综合考虑项目需求、成本、易用性和可扩展性等因素。
33 0
|
2月前
|
Java 开发者 前端开发
Struts 2、Spring MVC、Play Framework 上演巅峰之战,Web 开发的未来何去何从?
【8月更文挑战第31天】在Web应用开发中,Struts 2框架因强大功能和灵活配置备受青睐,但开发者常遇配置错误、类型转换失败、标签属性设置不当及异常处理等问题。本文通过实例解析常见难题与解决方案,如配置文件中遗漏`result`元素致页面跳转失败、日期格式不匹配需自定义转换器、`&lt;s:checkbox&gt;`标签缺少`label`属性致显示不全及Action中未捕获异常影响用户体验等,助您有效应对挑战。
67 0
|
2月前
|
SQL 监控 关系型数据库
SQL性能监控与调优工具的神奇之处:如何用最佳实践选择最适合你的那一个,让你的数据库飞起来?
【8月更文挑战第31天】在现代软件开发中,数据库性能监控与调优对应用稳定性至关重要。本文对比了数据库内置工具、第三方工具及云服务工具等几种常用SQL性能监控与调优工具,并通过示例代码展示了如何利用MySQL的EXPLAIN功能分析查询性能。选择最适合的工具需综合考虑功能需求、数据库类型及成本预算等因素。遵循了解工具功能、试用工具及定期维护工具等最佳实践,可帮助开发者更高效地管理和优化数据库性能,迎接未来软件开发中的挑战与机遇。
36 0
|
3月前
|
运维 监控 Java
(十)JVM成神路之线上故障排查、性能监控工具分析及各线上问题排错实战
经过前述九章的JVM知识学习后,咱们对于JVM的整体知识体系已经有了全面的认知。但前面的章节中,更多的是停留在理论上进行阐述,而本章节中则更多的会分析JVM的实战操作。

热门文章

最新文章

推荐镜像

更多
下一篇
无影云桌面