腾讯性能监控框架Matrix源码分析(七)TracePlugin 之 帧率处理FrameTracer

879 阅读6分钟

通过前面文章UIThreadMonitor Choreographer的分析我们已经知道了拿到帧回调数据的方法即doFrame,FrameTracers的作用就是对帧数据进行整合分析,上报和视图展示。下面开始分析源码

首先通过TracePlugin.init中初始化自己,并在onStart方法增加自己doFrame的监听

public class TracePlugin extends Plugin {
    private FrameTracer frameTracer;

    @Override
    public void init(Application app, PluginListener listener) {
        super.init(app, listener);
        // 1.初始化
        frameTracer = new FrameTracer(traceConfig);
    }

    @Override
    public void start() {
        super.start();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (!UIThreadMonitor.getMonitor().isInit()) {
                    try {
                        UIThreadMonitor.getMonitor().init(traceConfig);
                    } catch (java.lang.RuntimeException e) {
                        MatrixLog.e(TAG, "[start] RuntimeException:%s", e);
                        return;
                    }
                }
                // 开始监听主线程
                UIThreadMonitor.getMonitor().onStart();
                // 2.Tracer 开始工作
                frameTracer.onStartTrace();
            }
        };
    }
}

FrameTracer 的构造器主要利用传入的用户配置 traceConfig 进行参数设置,然后添加一个 FPS 的监听

private static final String TAG = "Matrix.FrameTracer";
private final HashSet<IDoFrameListener> listeners = new HashSet<>();
private final long frameIntervalMs; //每帧间隔时间 一般就是16.7
private final TraceConfig config; //配置
private long timeSliceMs;// fps 的上报时间阈值
private boolean isFPSEnable;//FPS 监控是否开启
private long frozenThreshold; //一秒钟 掉帧 42帧 为 FROZEN
private long highThreshold; //一秒钟 掉帧 24帧 为 HIGH
private long middleThreshold;//一秒钟 掉帧 9帧 为 MIDDLE
private long normalThreshold;//一秒钟 掉帧 3帧 为 NORMAL

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 收集器
        addListener(new FPSCollector());
    }
}

frameTracer.onStartTrace():TracePlugin 的 start() 方法是由开发者手动调用的,里面调用父类 Tracer 的onStartTrace() 标记 FrameTracer 进入活动状态。

因为父类 Tracer 实现了 LooperObserver 接口,所以可以被添加到监听列表中,而添加监听的目的就是为了接收每帧回调。

在前边的准备中,UIThreadMonitor 已经拥有了监听系统 VSync 信号的能力,只需要在接收到信号的时候回调这些监听就可以让 FrameTracer 接收到每帧的回调。

@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    if (isForeground()) {
        notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
    }
}

FrameTracer 通过 doFrame() 方法就可以拿到数据展示信息。依据这些数据就可以进行帧率统计、帧率图绘制等工作了。

了解下doFrame参数

focusedActivity: 当前顶层的activity名字
startNs:消息开始分发的时间点 纳秒
endNs:消息分发结束的时间点 纳秒
isVsyncFrame: 是否是UI渲染帧。只要触发了input callback回调,那肯定是渲染消息。
intendedFrameTimeNs:当前时间,纳秒。如果反射获取失败,则把startNs作为默认值。 通过反射Choreographer内部类 FrameDisplayEventReceiver类中的变量:mTimestampNanos。
inputCostNs:input耗时。现在这三个比较简单了吧
animationCostNs:animation耗时。
traversalCostNs:traversal耗时。

我们注意到只有在前台才会去做数据的收集和处理,后台情况下不会处理。为什么? 因为应用不可见的时候是不会进行UI渲染的。这是系统来控制的,避免造成资源白白浪费。

接着看 notifyListener 方法:

private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
                            final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
    long traceBegin = System.currentTimeMillis();
    try {
        final long jiter = endNs - intendedFrameTimeNs;
        //整个绘制过程的帧数: 实际上一帧的耗时/理论每一帧的时间。因此,就是卡顿导致的掉帧数
        final int dropFrame = (int) (jiter / frameIntervalNs);
        if (dropFrameListener != null) {
            //触发掉帧数阈值,则分发掉帧数回调
            if (dropFrame > dropFrameListenerThreshold) {
                try {
                    if (AppActiveMatrixDelegate.getTopActivityName() != null) {
                        //掉帧回调,给需要的子控件提供,后序会说
                        long lastResumeTime = lastResumeTimeMap.get(AppActiveMatrixDelegate.getTopActivityName());
                        dropFrameListener.dropFrame(dropFrame, AppActiveMatrixDelegate.getTopActivityName(), lastResumeTime);
                    }
                } catch (Exception e) {
                    MatrixLog.e(TAG, "dropFrameListener error e:" + e.getMessage());
                }
            }
        }
        //计算总的掉帧数和时间
        droppedSum += dropFrame;
        durationSum += Math.max(jiter, frameIntervalNs);

        synchronized (listeners) {
            for (final IDoFrameListener listener : listeners) {
                if (config.isDevEnv()) {
                    listener.time = SystemClock.uptimeMillis();
                }
                if (null != listener.getExecutor()) {
                    //只有指定收集帧数才会收集,否则不收集走下边的UI展示逻辑
                    //这里listener传入了FPSCollector 默认300帧一分析
                    if (listener.getIntervalFrameReplay() > 0) {
                        //开始真正的收集上报
                        listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                    } else {
                        listener.getExecutor().execute(new Runnable() {
                            @Override
                            public void run() {
                                //做UI展示用,
                                listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                                        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                            }
                        });
                    }
                } else {
                    //最早的收集回调,最终交给实现类处理
                    listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
                            intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
                }

                if (config.isDevEnv()) {
                    listener.time = SystemClock.uptimeMillis() - listener.time;
                    MatrixLog.d(TAG, "[notifyListener] cost:%sms listener:%s", listener.time, listener);
                }
            }
        }
    } finally {
        long cost = System.currentTimeMillis() - traceBegin;
        if (config.isDebug() && cost > frameIntervalNs) {
            MatrixLog.w(TAG, "[notifyListener] warm! maybe do heavy work in doFrameSync! size:%s cost:%sms", listeners.size(), cost);
        }
    }
} 

这个方法看起来比较长,主要做了两件事情:

  1. 计算掉帧数
  2. 通过异步的方式收集数据进行处理

掉帧数的计算

final long jiter = endNs - intendedFrameTimeNs;
//整个绘制过程的帧数: 实际上一帧的耗时/理论每一帧的时间。因此,就是卡顿导致的掉帧数
final int dropFrame = (int) (jiter / frameIntervalNs); 

原理: 理论上我们要求一帧的渲染时间为16.66ms。那么用实际上一帧花费的时间除以理论的时间就得到了这段时间的帧数。 举个例子:

假设一帧实际上花费时间:<16.66ms,那么 dropFrame=0。表示没有掉帧,非常nice。
假设一帧实际上花费时间:=16.66ms,那么 dropFrame=1。表示没有掉帧,非常nice。
假设一帧实际上花费时间:16.66ms< <约32ms,那么 dropFrame=1。虽然没有达到理论值,但基本认为nice。
假设一帧实际上花费时间:=32ms,那么 dropFrame=2。认为掉了一帧,但基本认为nice。

按照这种计算,matrix根据掉帧数的区间分布,定出了这样一个衡量流畅度的标准:

037d3651462bf1f2ee91e0e51374e837.png 如果一个页面的掉帧数是0~3帧区间,认为是best级别。也就是每一帧的渲染时间基本维持在16ms ~ 48ms之间。转换一下更好理解:

dad9fae1c5e8bfe77535d1a4df3c5884.png 这么定义有什么好处呢? 如果单纯的去看界面的平均帧率,看不出来哪里卡顿了。再有就算每一帧是32ms,只要保持连续帧都是32ms,那么对人眼来说也不认为是卡顿。只有突然出现一帧如 >300ms,那么用户就会觉得不流畅。

因此,我们通过收集掉帧数,来评判一个界面在一段时间内(matrix认为这段时间是200帧。也就是每200帧做一次分析)的掉帧数分布。官网图:

f00b68ee2fd3e88ea39e47ba7da71d98.png 2.1.2 数据处理逻辑

如果开启了fps开关,那么就开始分析数据吧

//这个是开了fpsmonitor开关才会走这里,否则是默认是走else
listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
        intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs); 

DoFrameListener.collect() 方法

FPSCollector 继承了 IDoFrameListener类,所以先看下collect方法:

@CallSuper
public void collect(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame,
                    long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    //FrameReplay 代表每一帧的信息
    FrameReplay replay = FrameReplay.create();
    replay.focusedActivity = focusedActivity;
    replay.startNs = startNs;
    replay.endNs = endNs;
    replay.dropFrame = dropFrame;
    replay.isVsyncFrame = isVsyncFrame;
    replay.intendedFrameTimeNs = intendedFrameTimeNs;
    replay.inputCostNs = inputCostNs;
    replay.animationCostNs = animationCostNs;
    replay.traversalCostNs = traversalCostNs;
    list.add(replay);
    
    //当收集到的帧数>300,这里可以控制,且有提供线程执行器的时候,开始分析收集到的帧数信息
    if (list.size() >= intervalFrame && getExecutor() != null) {
   
        final List<FrameReplay> copy = new LinkedList<>(list);
        list.clear();
        //拷贝一份,是为了不干扰原来内存的数据
        getExecutor().execute(new Runnable() {
            @Override
            public void run() {
                doReplay(copy);
                for (FrameReplay record : copy) {
                    //处理完成后,进行释放
                    record.recycle(); //循环利用
                }
            }
        });
    }
} 

注意FrameReplay的创建和回收增加了一个链表缓冲池,因为对于频繁的创建对象是很消耗性能的,尤其是帧率的监测,细节满满!!!

private final static LinkedList<FrameReplay> sPool = new LinkedList<>();

public static final class FrameReplay {
    public String focusedActivity;
    public long startNs;
    public long endNs;
    public int dropFrame;
    public boolean isVsyncFrame;
    public long intendedFrameTimeNs;
    public long inputCostNs;
    public long animationCostNs;
    public long traversalCostNs;
    //不用的对象放进回收池,只是擦除属性,下次可以复用
    public void recycle() {
        if (sPool.size() <= 1000) {
            this.focusedActivity = "";
            this.startNs = 0;
            this.endNs = 0;
            this.dropFrame = 0;
            this.isVsyncFrame = false;
            this.intendedFrameTimeNs = 0;
            this.inputCostNs = 0;
            this.animationCostNs = 0;
            this.traversalCostNs = 0;
            synchronized (sPool) {
                sPool.add(this);
            }
        }
    }
       
    public static FrameReplay create() {
        FrameReplay replay;
        //优先取池内的对象
        synchronized (sPool) {
            replay = sPool.poll();
        }
        if (replay == null) {
            return new FrameReplay();
        }
        return replay;
    }
}

很简单,注释里写的清楚了。 接着看 doReplay 方法:

@Override
public void doReplay(List<FrameReplay> list) {
    // 开始分析回放帧集合
    super.doReplay(list);

    for (FrameReplay replay : list) {
        doReplayInner(replay.focusedActivity, replay.startNs, replay.endNs, replay.dropFrame, replay.isVsyncFrame,
                replay.intendedFrameTimeNs, replay.inputCostNs, replay.animationCostNs, replay.traversalCostNs);
    }
} 

遍历帧集合,开始对每一帧进行分析~

 public void doReplayInner(String visibleScene, long startNs, long endNs, int droppedFrames,
                              boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs,
                              long animationCostNs, long traversalCostNs) {

        if (Utils.isEmpty(visibleScene)) return;
        // 这个变量代表是ui处理的消息,这样避免把非ui的操作统计在内,因为我们只关注帧率
        if (!isVsyncFrame) return;

        //从页面维度,把每一帧和当前的页面关联起来
        //收集到的帧,肯定有多个帧属于同一个页面的。因此,基于页面的维度来进行统计。 而不是从帧的维度进行统计。这样更难全面的反映页面的卡顿情况分布。
        FrameCollectItem item = map.get(visibleScene);
        if (null == item) {
            item = new FrameCollectItem(visibleScene);
            map.put(visibleScene, item);
        }
        //3,开始对每个页面的所属帧信息进行收集
        item.collect(droppedFrames);

        
        //4,某个页面触发上报逻辑 timeSliceMs 默认是10s,可配置
        if (item.sumFrameCost >= timeSliceMs) { // report
            map.remove(visibleScene);
           
            // 5, 开始上报
            item.report();
        }
    }
} 

根据掉帧数划分出相应的卡顿等级

void collect(int droppedFrames) {
    float frameIntervalCost = 1f * FrameTracer.this.frameIntervalNs
            / Constants.TIME_MILLIS_TO_NANO;
    //统计的时间内,这个页面总的绘制耗时
    sumFrameCost += (droppedFrames + 1) * frameIntervalCost;
    //统计的时间内,这个页面总的掉帧数
    sumDroppedFrames += droppedFrames;
    //一个页面总共耗帧
    sumFrame++;
    //这相当于是一个页面的所有统计。
    //说白了,就是统计在某个页面,出现了好、坏、中、高等掉帧数的一个分布。看看占比。非常的nice呀!
    if (droppedFrames >= frozenThreshold) {
        dropLevel[DropStatus.DROPPED_FROZEN.index]++;
        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] += Math.max(droppedFrames, 0);
    }
}

上报数据

void report() {

    // 当前页面的实际fps: 每秒多少帧 
    float fps = Math.min(refreshRate, 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());

        //上报 当前界面的名字+ 卡顿区间发生次数+卡顿区间掉帧总数+实际fps
        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);

        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;
    }
}
总结

FrameTrace 做的事情如下:

  1. 收集每一帧的信息:包含绘制开始和结束的时间、当前页面名字、input、animation、traversal阶段的耗时等等
  2. 从页面的维度触发,对当前页面发生的渲染帧进行了一个全面的评估:卡顿区间的占比是多少。该页面卡顿引起的原因是连续的掉帧? 还是某个帧严重卡顿?
  3. 当然,还提供了fps相关的页面展示类: FrameDecorator。在测试环境下,外部可以直接用。

在前面文章提到过在系统26以下依靠的是UIThreadMonitor,26以上为我们提供了一个更方便API去获取每帧的情况

我们看FrameTracer onAlive方法

@Override
public void onAlive() {
    super.onAlive();
    if (isFPSEnable) {
        //26以下
        if (!useFrameMetrics) {
            UIThreadMonitor.getMonitor().addObserver(this);
        }
       //注册了activity生命周期回调,具体干了什么呢? Matrix.with().getApplication().registerActivityLifecycleCallbacks(this);
    }
}

在这注册了activity生命周期回调,具体干了什么呢?

@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
    //系统26及以上
    if (useFrameMetrics) {
        this.refreshRate = (int) activity.getWindowManager().getDefaultDisplay().getRefreshRate();
        this.frameIntervalNs = Constants.TIME_SECOND_TO_NANO / (long) refreshRate;
        Window.OnFrameMetricsAvailableListener onFrameMetricsAvailableListener = new Window.OnFrameMetricsAvailableListener() {
            @RequiresApi(api = Build.VERSION_CODES.O)
            @Override
            public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {
                FrameMetrics frameMetricsCopy = new FrameMetrics(frameMetrics);
                long vsynTime = frameMetricsCopy.getMetric(FrameMetrics.VSYNC_TIMESTAMP);
                long intendedVsyncTime = frameMetricsCopy.getMetric(FrameMetrics.INTENDED_VSYNC_TIMESTAMP);
                frameMetricsCopy.getMetric(FrameMetrics.DRAW_DURATION);
                notifyListener(AppActiveMatrixDelegate.INSTANCE.getVisibleScene(), intendedVsyncTime, vsynTime, true, intendedVsyncTime, 0, 0, 0);
            }
        };
        this.frameListenerMap.put(activity.hashCode(), onFrameMetricsAvailableListener);
        activity.getWindow().addOnFrameMetricsAvailableListener(onFrameMetricsAvailableListener, new Handler());
        MatrixLog.i(TAG, "onActivityCreated addOnFrameMetricsAvailableListener");
    }
}

在每个页面创建的时候注册了OnFrameMetricsAvailableListener 这是做什么的呢?这是为客户端回调,客户端需要窗口渲染的每一帧的帧计时信息。

通过实现onFrameMetricsAvailable 可以拿到FrameMetrics即每一帧的信息

FrameMetrics 参数解析 :

图片.png

通过获取 INTENDED_VSYNC_TIMESTAMP VSYNC_TIMESTAMP参数即可拿到start end值然后调用notifyListener 即走到了刚才分析处理数据的逻辑,本篇分析到此为止,下篇我们会分析UI的展示

你的 点赞、评论,是对我的巨大鼓励!