腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 之 FrameTracer

版本

v0.6.5

温馨提示

  1. 在读这篇文章之前墙裂建议先读腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析
  2. TracePlugin 是比较复杂的,很多东西文章中可能讲的不是很清楚,配合 推荐 Matrix 源码完整注释
    可能会有更好的效果

概述

本篇文章是 腾讯开源的 APM 框架 Matrix 系列文章的第四篇,将对matrix-trace-canary这个模块种的FrameTracer类进行解析。这个类主要是对UIThreadMonitor提供的数据进行简单的整理,并分发给各个IDoFrameListener,FrameTracer自身携带了一个FPS的收集器FPSCollector。上一篇为腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析

1. FrameTracer.<init>

首先我们来看一下FrameTracer的构造方法

    public FrameTracer(TraceConfig config) {
        this.config = config;
        //每帧间隔时间 一般就是16.7
        this.frameIntervalMs = TimeUnit.MILLISECONDS.convert(UIThreadMonitor.getMonitor().getFrameIntervalNanos(), TimeUnit.NANOSECONDS) + 1;
        //fps 的上报时间阈值
        this.timeSliceMs = config.getTimeSliceMs();
        //FPS 监控是否开启
        this.isFPSEnable = config.isFPSEnable();
        //一秒钟 掉帧 42帧 为 FROZEN
        this.frozenThreshold = config.getFrozenThreshold();
        //一秒钟 掉帧 24帧 为 HIGH
        this.highThreshold = config.getHighThreshold();
        //一秒钟 掉帧 3帧 为 NORMAL
        this.normalThreshold = config.getNormalThreshold();
        //一秒钟 掉帧 9帧 为 MIDDLE
        this.middleThreshold = config.getMiddleThreshold();

        MatrixLog.i(TAG, "[init] frameIntervalMs:%s isFPSEnable:%s", frameIntervalMs, isFPSEnable);
        if (isFPSEnable) {
            //添加 FPS 收集器 详见【2.1】
            addListener(new FPSCollector());
        }
    }

构造方法中就是对配置的记录,然后就是添加了一个FPS的收集器到FrameTracer

1.1 Tracer.onStartTrace

腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析中我们知道当TracePlugin在启动的时候(执行自己的start()方法)会调用各个TraceronStartTrace()方法,那么第一步我们先看看这个方法。

    final synchronized public void onStartTrace() {
        if (!isAlive) {
            //标识当前Tracer是活着的
            this.isAlive = true;
            //详见【1.2】
            onAlive();
        }
    }

1.2 FrameTracer.onAlive

TraceronAlive()是一个空实现,FrameTracer复写了这个方法,所以我们直接进入到FrameTracer.onAlive

    public void onAlive() {
        super.onAlive();
        //添加 Observer 到 UIThreadMonitor 详见【1.3】
        UIThreadMonitor.getMonitor().addObserver(this);
    }

1.3 UIThreadMonitor.getMonitor().addObserver

关于Tracer.onCloseTrace方法就是Tracer.onStartTrace的反操作,所以我们就不废话了直接跳过。读过上一篇文章腾讯 Apm 框架 Matrix 源码阅读 - TracePlugin 架构解析的同学都知道(如果没看的同学建议先去看一下,上一篇文章其实就是TracePlugin这个插件的核心),UIThreadMonitor会配合LooperMonitor获得每个刷新帧的各个阶段的耗时时间,并回调dispatchBegin,doFrame,dispatchEnd这三个方法。FrameTracer复写了doFrame这个方法所以我们直接进入到这个方法里。

    public void doFrame(String focusedActivityName, long start, long end, long frameCostMs, long inputCostNs, long animationCostNs, long traversalCostNs) {
        //处于前台
        if (isForeground()) {
            //详见【1.4】
            notifyListener(focusedActivityName, end - start, frameCostMs, frameCostMs >= 0);
        }
    }

1.4 FrameTracer.notifyListener

  /**
     * @param visibleScene    当前Activity名
     * @param taskCostMs      整个任务耗时
     * @param frameCostMs     该帧耗时
     * @param isContainsFrame 是否是帧刷新
     */
    private void notifyListener(final String visibleScene, final long taskCostMs, final long frameCostMs, final boolean isContainsFrame) {
        long start = System.currentTimeMillis();
        try {
            synchronized (listeners) {
                for (final IDoFrameListener listener : listeners) {
                    if (config.isDevEnv()) {
                        listener.time = SystemClock.uptimeMillis();
                    }
                    //当前事件 消耗的帧数
                    final int dropFrame = (int) (taskCostMs / frameIntervalMs);
                    //同步 回调 doFrameSync 方法
                    listener.doFrameSync(visibleScene, taskCostMs, frameCostMs, dropFrame, isContainsFrame);
                    //如果 listener.getExecutor()不为空,就执行异步的回调方法
                    if (null != listener.getExecutor()) {
                        listener.getExecutor().execute(new Runnable() {
                            @Override
                            public void run() {
                                //异步回调 doFrameAsync 方法
                                listener.doFrameAsync(visibleScene, taskCostMs, frameCostMs, dropFrame, isContainsFrame);
                            }
                        });
                    }
                    ....
                }
            }
        } finally {
            long cost = System.currentTimeMillis() - start;
            if (config.isDebug() && cost > frameIntervalMs) {
                MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost);
            }
        }
    }

notifyListener就是计算出当前事件(任务)消耗的帧数(事件总耗时/每帧间隔)然后将这些数据通过同步或者异步的方式传递给各个IDoFrameListener.下面我们具体分析一下FPSCollector这个IDoFrameListener是怎么工作的。

2.1 FPSCollector.doFrameAsync

我们看到FPSCollectorgetExecutor()方法返回不为空,所以直接进入doFrameAsync()方法一探究竟。

    /**
         *
         * @param visibleScene 当前Activity名
         * @param taskCost 整个任务耗时
         * @param frameCostMs 该帧耗时
         * @param droppedFrames 消耗帧数
         * @param isContainsFrame 是否属于帧刷新
         */
        @Override
        public void doFrameAsync(String visibleScene, long taskCost, long frameCostMs, int droppedFrames, boolean isContainsFrame) {
            super.doFrameAsync(visibleScene, taskCost, frameCostMs, droppedFrames, isContainsFrame);
            if (Utils.isEmpty(visibleScene)) {
                return;
            }

            FrameCollectItem item = map.get(visibleScene);
            if (null == item) {
                item = new FrameCollectItem(visibleScene);
                map.put(visibleScene, item);
            }

            //详见【2.2】
            item.collect(droppedFrames, isContainsFrame);

            //每个visibleScene(页面)监控的 总时间超过 预设阀值 就 进行报告,并重置
            if (item.sumFrameCost >= timeSliceMs) {
                map.remove(visibleScene);
                //详见【2.3】
                item.report();
            }
        }

2.2 FrameCollectItem.collect

/**
         * @param droppedFrames   消耗帧数
         * @param isContainsFrame
         */
        void collect(int droppedFrames, boolean isContainsFrame) {
            long frameIntervalCost = UIThreadMonitor.getMonitor().getFrameIntervalNanos();
            //积累的 总时间 ms值 ,这里不够一帧当一帧计算
            sumFrameCost += (droppedFrames + 1) * frameIntervalCost / Constants.TIME_MILLIS_TO_NANO;
            //下降的总帧数
            sumDroppedFrames += droppedFrames;
            //doFrameAsync 回调次数
            sumFrame++;
            if (!isContainsFrame) {
                //除过 刷新帧 事件外,其他 事件数
                sumTaskFrame++;
            }

            if (droppedFrames >= frozenThreshold) {//frozen
                dropLevel[DropStatus.DROPPED_FROZEN.index]++;// 冻结数+1
                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] += (droppedFrames < 0 ? 0 : droppedFrames);
            }
        }

这个方法中会计算并记录当前页面一段时间内累积的执行任务时间,使用帧数,并对使用帧数进行分级记录和记录在dropLeveldropSum

2.3 FrameCollectItem.report

每个visibleScene(页面)监控的 总时间(sumFrameCost)超过 预设阀值就进行上报

 void report() {
            //计算 fps 一秒内的平均帧率
            float fps = Math.min(60.f, 1000.f * sumFrame / sumFrameCost);
            MatrixLog.i(TAG, "[report] FPS:%s %s", fps, toString());

            try {
                TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
                if (null == plugin) {
                    return;
                }
                //记录卡顿级别,及其出现的次数
                JSONObject dropLevelObject = new JSONObject();
                dropLevelObject.put(DropStatus.DROPPED_FROZEN.name(), dropLevel[DropStatus.DROPPED_FROZEN.index]);
                dropLevelObject.put(DropStatus.DROPPED_HIGH.name(), dropLevel[DropStatus.DROPPED_HIGH.index]);
                dropLevelObject.put(DropStatus.DROPPED_MIDDLE.name(), dropLevel[DropStatus.DROPPED_MIDDLE.index]);
                dropLevelObject.put(DropStatus.DROPPED_NORMAL.name(), dropLevel[DropStatus.DROPPED_NORMAL.index]);
                dropLevelObject.put(DropStatus.DROPPED_BEST.name(), dropLevel[DropStatus.DROPPED_BEST.index]);

                //记录卡顿级别,及掉帧总次数
                JSONObject dropSumObject = new JSONObject();
                dropSumObject.put(DropStatus.DROPPED_FROZEN.name(), dropSum[DropStatus.DROPPED_FROZEN.index]);
                dropSumObject.put(DropStatus.DROPPED_HIGH.name(), dropSum[DropStatus.DROPPED_HIGH.index]);
                dropSumObject.put(DropStatus.DROPPED_MIDDLE.name(), dropSum[DropStatus.DROPPED_MIDDLE.index]);
                dropSumObject.put(DropStatus.DROPPED_NORMAL.name(), dropSum[DropStatus.DROPPED_NORMAL.index]);
                dropSumObject.put(DropStatus.DROPPED_BEST.name(), dropSum[DropStatus.DROPPED_BEST.index]);

                JSONObject resultObject = new JSONObject();
                resultObject = DeviceUtil.getDeviceInfo(resultObject, plugin.getApplication());

                resultObject.put(SharePluginInfo.ISSUE_SCENE, visibleScene);
                resultObject.put(SharePluginInfo.ISSUE_DROP_LEVEL, dropLevelObject);
                resultObject.put(SharePluginInfo.ISSUE_DROP_SUM, dropSumObject);
                resultObject.put(SharePluginInfo.ISSUE_FPS, fps);
                resultObject.put(SharePluginInfo.ISSUE_SUM_TASK_FRAME, sumTaskFrame);

                Issue issue = new Issue();
                issue.setTag(SharePluginInfo.TAG_PLUGIN_FPS);
                issue.setContent(resultObject);
                plugin.onDetectIssue(issue);

            } catch (JSONException e) {
                MatrixLog.e(TAG, "json error", e);
            } finally {
                sumFrame = 0;
                sumDroppedFrames = 0;
                sumFrameCost = 0;
                sumTaskFrame = 0;
            }
        }

这个方法中会计算出 具体的FPS值,并组建成Json通过TracePlugin进行上报。

总结

FrameTracer就是通过UIThreadMonitor提供的感知每帧耗时的能力。进行简单的整合再通知给各个IDoFrameListener。Matrx中提供了两个IDoFrameListener一个就是FPSCollector用于上报FPS,另一个是FrameDecorator用于直接显示FPS。

FPSCollector上报数据解析

scene:当前可见的activity
dropLevel:记录各个卡段级别出现的次数,卡顿级别可分为DROPPED_FROZEN,DROPPED_HIGH,DROPPED_MIDDLE,DROPPED_NORMAL,DROPPED_BEST;例:
    "DROPPED_MIDDLE":18,表示时间阈值内共有 18此时 DROPPED_MIDDLE的情况
dropSum:记录各个卡段级别掉帧总数,例:
    "DROPPED_MIDDLE":218, 表示时间阈值内共有 218帧是 位于 DROPPED_MIDDLE
fps:时间阈值内的平均帧率
dropTaskFrameSum:不太清楚

系列文章

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容