版本
v0.6.5
温馨提示
- 在读这篇文章之前墙裂建议先读腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析
- TracePlugin 是比较复杂的,很多东西文章中可能讲的不是很清楚,配合 推荐 Matrix 源码完整注释
可能会有更好的效果
概述
本篇文章是 腾讯开源的 APM 框架 Matrix 系列文章的第五篇,将对matrix-trace-canary
这个模块种的StartupTracer
类进行解析。这个类主要监控并上报App 冷/暖启动时间,Activity启动时间。上一篇为腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 FrameTracer
写在前面
各个时间节点的定义
其实StartupTracer
的类注释已将告诉我们每个时间是如何定义的,下面我们在用文字描述一下
* firstMethod.i LAUNCH_ACTIVITY onWindowFocusChange LAUNCH_ACTIVITY onWindowFocusChange
* ^ ^ ^ ^ ^
* | | | | |
* |---------app---------|---|---firstActivity---|---------...---------|---careActivity---|
* |<--applicationCost-->|
* |<--------------firstScreenCost-------------->|
* |<---------------------------------------coldCost------------------------------------->|
* . |<-----warmCost---->|
*
- applicationCost(Application的启动时间):第一次启动Activity或者Service或者广播的时间(这里没有内容提供者是因为内容提供者是在Application初始化完成之前,加载完毕的) 减去 Application开始启动时间
- firstScreenCost(首屏启动时间):第一个Activity 可操作的时间(Activity获取焦点) 减去 Application开始启动时间
- coldCost(冷启动时间):主Activity可操作的时间(Activity获取焦点) 减去 Application开始启动时间
- warmCost(暖启动时间):最近一个Activity开始启动的时间 减去 这个Activity可操作的时间(Activity获取焦点)
原理简介
当 onActivityFocused 被回调时,进行各个时间点的计算,配合 AppMethodBeat 中记录的方法执行时间,通过一定的逻辑 筛选出 导致启动时间长的方法并上报。
1. StartupTracer.生命周期方法
首先我们先来看一下 的构造方法
,onAlive
(),onDead()
这三个方法,如果要问为什么看了上一篇文章腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 FrameTracer你就知道了。
public StartupTracer(TraceConfig config) {
this.config = config;
//是否可用
this.isStartupEnable = config.isStartupEnable();
//SplashActivities
this.splashActivities = config.getSplashActivities();
this.coldStartupThresholdMs = config.getColdStartupThresholdMs();
this.warmStartupThresholdMs = config.getWarmStartupThresholdMs();
}
@Override
protected void onAlive() {
super.onAlive();
MatrixLog.i(TAG, "[onAlive] isStartupEnable:%s", isStartupEnable);
if (isStartupEnable) {
//注册全局Activity生命周期监听 详见【1.1】
Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
//添加监听 可以感知 activity获得焦点 和 activity的生命周期 详见【1.2】
AppMethodBeat.getInstance().addListener(this);
}
}
@Override
protected void onDead() {
super.onDead();
if (isStartupEnable) {
//移除监听
AppMethodBeat.getInstance().removeListener(this);
Matrix.with().getApplication().unregisterActivityLifecycleCallbacks(this);
}
}
首先构造方法也是读取配置并记录起来,onAlive()
方法注册了ActivityLifecycleCallbacks
和IAppMethodBeatListener
两个监听,onDead()
中对这两个监听进行了移除
1.1 ActivityLifecycleCallbacks相关方法
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//activeActivityCount == 0 && coldCost > 0 说明曾经已经冷启动过,这是没有activity了,但是进程还在
if (activeActivityCount == 0 && coldCost > 0) {
//是否是暖启动
isWarmStartUp = true;
}
activeActivityCount++;
}
@Override
public void onActivityDestroyed(Activity activity) {
activeActivityCount--;
}
....
在 ActivityLifecycleCallbacks
的相关方法中只做了一件事 就是判断当前是否是暖启动。
1.2 StartupTracer.onActivityFocused
因为StartupTracer
注册了IAppMethodBeatListener
监听,所以当有Activity获取焦点,就会回调onActivityFocused
方法
@Override
public void onActivityFocused(String activity) {
if (isColdStartup()) {//判断条件是 coldCost == 0 所以只会进来一次
if (firstScreenCost == 0) {
//首屏启动时间=当前时间点-APP启动时间点
this.firstScreenCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
}
if (hasShowSplashActivity) {
//冷启动耗时 = (MainActivity启动的时间)当前时间-蛋碎时间
//类注释上画了,coldCost = 第二个activity onWindowFocusChange时的时间,
coldCost = uptimeMillis() - ActivityThreadHacker.getEggBrokenTime();
} else {
if (splashActivities.contains(activity)) {
hasShowSplashActivity = true;
} else if (splashActivities.isEmpty()) {//未配置 splashActivities,冷启动时间 == 第一屏时间
MatrixLog.i(TAG, "default splash activity[%s]", activity);
coldCost = firstScreenCost;
} else {
MatrixLog.w(TAG, "pass this activity[%s] at duration of start up! splashActivities=%s", activity, splashActivities);
}
}
if (coldCost > 0) {
//详见【1.3】 analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, coldCost, false);
}
} else if (isWarmStartUp()) {
isWarmStartUp = false;
//暖启动时间=当前时间- 最近一个activity被启动的时间
long warmCost = uptimeMillis() - ActivityThreadHacker.getLastLaunchActivityTime();
if (warmCost > 0) {
analyse(ActivityThreadHacker.getApplicationCost(), firstScreenCost, warmCost, true);
}
}
}
1.3 StartupTracer.analyse
/**
* @param applicationCost: application启动用时
* @param firstScreenCost: 首屏启动时间
* @param allCost :冷启动耗时 或者 暖启动耗时
* @param isWarmStartUp :是冷启动还是暖启动
*/
private void analyse(long applicationCost, long firstScreenCost, long allCost, boolean isWarmStartUp) {
MatrixLog.i(TAG, "[report] applicationCost:%s firstScreenCost:%s allCost:%s isWarmStartUp:%s", applicationCost, firstScreenCost, allCost, isWarmStartUp);
long[] data = new long[0];
if (!isWarmStartUp && allCost >= coldStartupThresholdMs) { //冷启动时间>阈值
//获取 AppMethodBeat.sBuffer 中记录的数据 详见【1.4】
data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sApplicationCreateBeginMethodIndex);
//移除 sApplicationCreateBeginMethodIndex 节点
ActivityThreadHacker.sApplicationCreateBeginMethodIndex.release();
} else if (isWarmStartUp && allCost >= warmStartupThresholdMs) {//暖启动时间>阈值
//详见【1.4】
data = AppMethodBeat.getInstance().copyData(ActivityThreadHacker.sLastLaunchActivityMethodIndex);
//移除 sApplicationCreateBeginMethodIndex 节点
ActivityThreadHacker.sLastLaunchActivityMethodIndex.release();
}
//执行 AnalyseTask 详见【1.5】
MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(data, applicationCost, firstScreenCost, allCost, isWarmStartUp, ActivityThreadHacker.sApplicationCreateScene));
}
1.4 AppMethodBeat.copyData()
//获取从 startRecord 到结束的 所有 IndexRecord
public long[] copyData(IndexRecord startRecord) {
return copyData(startRecord, new IndexRecord(sIndex - 1));
}
private long[] copyData(IndexRecord startRecord, IndexRecord endRecord) {
long current = System.currentTimeMillis();
long[] data = new long[0];
try {
if (startRecord.isValid && endRecord.isValid) {
int length;
int start = Math.max(0, startRecord.index);
int end = Math.max(0, endRecord.index);
//计算出copy区域的长度和copy
if (end > start) {//正常情况下 一次copy
length = end - start + 1;
data = new long[length];
System.arraycopy(sBuffer, start, data, 0, length);
} else if (end < start) {// 两次copy(后半截+前半截)
length = 1 + end + (sBuffer.length - start);
data = new long[length];
System.arraycopy(sBuffer, start, data, 0, sBuffer.length - start);
System.arraycopy(sBuffer, 0, data, sBuffer.length - start, end + 1);
}
return data;
}
return data;
} catch (OutOfMemoryError e) {//这里还捕获 OutOfMemoryError ,大厂程序员真的是细啊
MatrixLog.e(TAG, e.toString());
return data;
} finally {
MatrixLog.i(TAG, "[copyData] [%s:%s] length:%s cost:%sms", Math.max(0, startRecord.index), endRecord.index, data.length, System.currentTimeMillis() - current);
}
}
该方法只要是从AppMethodBeat.sBuffer
中copy出目标数据段。
1.5 AnalyseTask.run
AnalyseTask
是一个Runnable
所以我们直接进入run()
方法
public void run() {
LinkedList<MethodItem> stack = new LinkedList();
if (data.length > 0) {
//根据之前 data 查到的 methodId ,拿到对应插桩函数的执行时间、执行深度,将每个函数的信息封装成 MethodItem,然后存储到 stack 集合当中 详见【1.6】
TraceDataUtils.structuredDataToStack(data, stack, false, -1);
//根据规则 裁剪 stack 中的数据 详见【1.7】
TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
@Override
public boolean isFilter(long during, int filterCount) {
//如果 耗时小于 预设值 则进行裁剪
return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
}
@Override
public int getFilterMaxCount() {
//最大方法裁剪数 60
return Constants.FILTER_STACK_MAX_COUNT;
}
@Override
public void fallback(List<MethodItem> stack, int size) {//降级策略
MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
//循环删除 多余的shuju8
Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
}
});
}
StringBuilder reportBuilder = new StringBuilder();
StringBuilder logcatBuilder = new StringBuilder();
//获取最大的启动时间
long stackCost = Math.max(allCost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
//查询出最耗时的 methodId 详见【1.8】
String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);
// 如果超过阈值 打印log
if ((allCost > coldStartupThresholdMs && !isWarmStartUp)
|| (allCost > warmStartupThresholdMs && isWarmStartUp)) {
MatrixLog.w(TAG, "stackKey:%s \n%s", stackKey, logcatBuilder.toString());
}
// report 详见【1.9】
report(applicationCost, firstScreenCost, reportBuilder, stackKey, stackCost, isWarmStartUp, scene);
}
1.6 TraceDataUtils.structuredDataToStack
public static void structuredDataToStack(long[] buffer, LinkedList<MethodItem> result, boolean isStrict, long endTime) {
long lastInId = 0L;
//记录调用栈深度
int depth = 0;
//是个链表
LinkedList<Long> rawData = new LinkedList<>();
boolean isBegin = !isStrict;
for (long trueId : buffer) {
if (0 == trueId) {
continue;
}
//是严格模式
if (isStrict) {
if (isIn(trueId) && AppMethodBeat.METHOD_ID_DISPATCH == getMethodId(trueId)) {
isBegin = true;
}
if (!isBegin) {
MatrixLog.d(TAG, "never begin! pass this method[%s]", getMethodId(trueId));
continue;
}
}
//如果是 i 方法记录的数据
if (isIn(trueId)) {
//获取methodId
lastInId = getMethodId(trueId);
if (lastInId == AppMethodBeat.METHOD_ID_DISPATCH) { //如果是 handler 的 dispatchMessage 方法 depth 置为0
depth = 0;
}
depth++;
//加入到链表中
rawData.push(trueId);
} else {// 如果是 0 方法记录的数据
//获取methodId
int outMethodId = getMethodId(trueId);
if (!rawData.isEmpty()) {
//拿到i 方法中记录的数据
long in = rawData.pop();
depth--;
int inMethodId;
LinkedList<Long> tmp = new LinkedList<>();
tmp.add(in);
//如果 inMethodId 不等于 outMethodId 调用深度建议
while ((inMethodId = getMethodId(in)) != outMethodId && !rawData.isEmpty()) {
MatrixLog.w(TAG, "pop inMethodId[%s] to continue match ouMethodId[%s]", inMethodId, outMethodId);
in = rawData.pop();
depth--;
tmp.add(in);
}
//如果是 handler的 dispatchMessage方法
if (inMethodId != outMethodId && inMethodId == AppMethodBeat.METHOD_ID_DISPATCH) {
MatrixLog.e(TAG, "inMethodId[%s] != outMethodId[%s] throw this outMethodId!", inMethodId, outMethodId);
rawData.addAll(tmp);
depth += rawData.size();
continue;
}
//获取到 方法执行完的时间
long outTime = getTime(trueId);
// 获取方法开始执行的时间
long inTime = getTime(in);
//该方法执行时间
long during = outTime - inTime;
if (during < 0) {
MatrixLog.e(TAG, "[structuredDataToStack] trace during invalid:%d", during);
rawData.clear();
result.clear();
return;
}
//创建一个 methodItem 并加入
MethodItem methodItem = new MethodItem(outMethodId, (int) during, depth);
addMethodItem(result, methodItem);
} else {
MatrixLog.w(TAG, "[structuredDataToStack] method[%s] not found in! ", outMethodId);
}
}
}
while (!rawData.isEmpty() && isStrict) {
long trueId = rawData.pop();
int methodId = getMethodId(trueId);
boolean isIn = isIn(trueId);
long inTime = getTime(trueId) + AppMethodBeat.getDiffTime();
MatrixLog.w(TAG, "[structuredDataToStack] has never out method[%s], isIn:%s, inTime:%s, endTime:%s,rawData size:%s",
methodId, isIn, inTime, endTime, rawData.size());
if (!isIn) {
MatrixLog.e(TAG, "[structuredDataToStack] why has out Method[%s]? is wrong! ", methodId);
continue;
}
MethodItem methodItem = new MethodItem(methodId, (int) (endTime - inTime), rawData.size());
addMethodItem(result, methodItem);
}
TreeNode root = new TreeNode(null, null);
//将链表转为树 进行整理数据,root是根节点
stackToTree(result, root);
//清空 result
result.clear();
//将 整理过的 数据 保存到 result中
treeToStack(root, result);
}
这个方法主要是根据之前 data 查到的 methodId ,拿到对应插桩函数的执行时间、执行深度,将每个函数的信息封装成 MethodItem,然后存储到 stack 链表当中
1.7 TraceDataUtils.trimStack
public static void trimStack(List<MethodItem> stack, int targetCount, IStructuredDataFilter filter) {
if (0 > targetCount) {
stack.clear();
return;
}
int filterCount = 1;
int curStackSize = stack.size();
while (curStackSize > targetCount) {
ListIterator<MethodItem> iterator = stack.listIterator(stack.size());
while (iterator.hasPrevious()) {
MethodItem item = iterator.previous();
if (filter.isFilter(item.durTime, filterCount)) {//是否要过滤
iterator.remove();
curStackSize--;
if (curStackSize <= targetCount) {
return;
}
}
}
curStackSize = stack.size();
filterCount++;
if (filter.getFilterMaxCount() < filterCount) {
break;
}
}
int size = stack.size();
//如果 stack的 容量还是 大于 阈值,则使用降级策略
if (size > targetCount) {
filter.fallback(stack, size);
}
}
这个方法主要是 通过我们自定义的规则裁剪 stack 中的数据
1.7 TraceDataUtils.getTreeKey
public static String getTreeKey(List<MethodItem> stack, long stackCost) {
StringBuilder ss = new StringBuilder();
long allLimit = (long) (stackCost * Constants.FILTER_STACK_KEY_ALL_PERCENT);
LinkedList<MethodItem> sortList = new LinkedList<>();
//过滤出主要耗时方法
for (MethodItem item : stack) {
if (item.durTime >= allLimit) {
sortList.add(item);
}
}
//排序
Collections.sort(sortList, new Comparator<MethodItem>() {
@Override
public int compare(MethodItem o1, MethodItem o2) {
return Integer.compare((o2.depth + 1) * o2.durTime, (o1.depth + 1) * o1.durTime);
}
});
if (sortList.isEmpty() && !stack.isEmpty()) {//没有主要的耗时方法,就用第一个代替
MethodItem root = stack.get(0);
sortList.add(root);
} else if (sortList.size() > 1 && sortList.peek().methodId == AppMethodBeat.METHOD_ID_DISPATCH) {//如果第一个是 handler.dipatchMessage 那就去掉
sortList.removeFirst();
}
//拼接字符串
for (MethodItem item : sortList) {
ss.append(item.methodId + "|");
break;
}
return ss.toString();
}
这个方法主要是 获取耗时方法的 methodId
拼接成的字符串
1.9 AnalyseTask.report
/**
* @param applicationCost:Application 启动时间
* @param firstScreenCost:首屏启动时间
* @param reportBuilder:需要上报的 method信息
* @param stackKey :主要耗时方法id
* @param allCost: 冷启动耗时 或者 暖启动耗时
* @param isWarmStartUp:是否是 暖启动
* @param scene:app 启动时的场景(可分为 activity ,service ,brodcast )
*/
private void report(long applicationCost, long firstScreenCost, StringBuilder reportBuilder, String stackKey,
long allCost, boolean isWarmStartUp, int scene) {
TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
if (null == plugin) {
return;
}
//上报正常启动信息
try {
JSONObject costObject = new JSONObject();
//添加设备信息
costObject = DeviceUtil.getDeviceInfo(costObject, Matrix.with().getApplication());
//Application 启动时间
costObject.put(SharePluginInfo.STAGE_APPLICATION_CREATE, applicationCost);
//Application 启动场景
costObject.put(SharePluginInfo.STAGE_APPLICATION_CREATE_SCENE, scene);
//首屏启动时间
costObject.put(SharePluginInfo.STAGE_FIRST_ACTIVITY_CREATE, firstScreenCost);
//冷启动时间 或者 暖启动时间
costObject.put(SharePluginInfo.STAGE_STARTUP_DURATION, allCost);
//冷启动 or 暖启动
costObject.put(SharePluginInfo.ISSUE_IS_WARM_START_UP, isWarmStartUp);
Issue issue = new Issue();
issue.setTag(SharePluginInfo.TAG_PLUGIN_STARTUP);
issue.setContent(costObject);
//上报
plugin.onDetectIssue(issue);
} catch (JSONException e) {
MatrixLog.e(TAG, "[JSONException for StartUpReportTask error: %s", e);
}
//上报 启动速度超过预设阈值的信息
if ((allCost > coldStartupThresholdMs && !isWarmStartUp)
|| (allCost > warmStartupThresholdMs && isWarmStartUp)) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication());
jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.STARTUP);
jsonObject.put(SharePluginInfo.ISSUE_COST, allCost);
jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString());
jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey);
jsonObject.put(SharePluginInfo.ISSUE_SUB_TYPE, isWarmStartUp ? 2 : 1);
Issue issue = new Issue();
issue.setTag(SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
issue.setContent(jsonObject);
plugin.onDetectIssue(issue);
} catch (JSONException e) {
MatrixLog.e(TAG, "[JSONException error: %s", e);
}
}
}
}
这个方法就是组建json然后进行上报的操作,在正常启动下会上报一组Tag为Trace_StartUp
的json,如果启动时间超过了预设阈值的情况下还会上传一组Tag为Trace_EvilMethod
的json
StartupTracer 上报数据解析
tag: Trace_EvilMethod or Trace_StartUp
application_create:(Application的启动时间)第一次启动Activity或者Service或者广播的时间(这里没有内容提供者是因为内容提供者是在Application初始化完成之前,加载完毕的) 减去 Application开始启动时间
application_create_scene:启动场景 Activity(159,100),Service(114),broadcastReceiver()113
first_activity_create:(首屏启动时间)第一个Activity 可操作的时间(Activity获取焦点) 减去 Application开始启动时间
startup_duration:启动时间可分为 :
* (冷启动时间):主Activity可操作的时间(Activity获取焦点) 减去 Application开始启动时间
* (暖启动时间):最近一个Activity开始启动的时间 减去 这个Activity可操作的时间(Activity获取焦点)
is_warm_start_up:是否是暖启动
detail:固定为STARTUP
cost:总耗时同 startup_duration
stack:方法栈信息, 每个item之间用“\n”隔开,每个item的含义为,调用深度,methodId,调用次数,耗时
* 比如:0,118,1,5 -> 调用深度为0,methodId=118,调用次数=1,耗时5ms
stackKey:主要耗时方法 的methodId
subType:2:暖启动,1:冷启动
系列文章
- 腾讯 Apm 框架 Matrix 源码阅读 - gradle插件
- 腾讯 Apm 框架 Matrix 源码阅读 - 架构解析
- 腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析
- 腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 FrameTracer
- 腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 StartupTracer
- 腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 AnrTracer
- 腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 上报字段含义