美文网首页
腾讯 Apm 框架 Matrix 源码阅读 - TracePlu

腾讯 Apm 框架 Matrix 源码阅读 - TracePlu

作者: 河里的枇杷树 | 来源:发表于2020-03-31 17:21 被阅读0次

    版本

    v0.6.5

    温馨提示

    1. 在读这篇文章之前墙裂建议先读腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析
    2. 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()方法注册了ActivityLifecycleCallbacksIAppMethodBeatListener两个监听,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 源码阅读 - TracePlu

          本文链接:https://www.haomeiwen.com/subject/lzcguhtx.html