/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.internal.jank;

import static android.view.SurfaceControl.JankData.DISPLAY_HAL;
import static android.view.SurfaceControl.JankData.JANK_APP_DEADLINE_MISSED;
import static android.view.SurfaceControl.JankData.JANK_NONE;
import static android.view.SurfaceControl.JankData.JANK_SURFACEFLINGER_DEADLINE_MISSED;
import static android.view.SurfaceControl.JankData.JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED;
import static android.view.SurfaceControl.JankData.PREDICTION_ERROR;
import static android.view.SurfaceControl.JankData.SURFACE_FLINGER_SCHEDULING;

import static com.android.internal.jank.DisplayRefreshRate.UNKNOWN_REFRESH_RATE;
import static com.android.internal.jank.DisplayRefreshRate.VARIABLE_REFRESH_RATE;
import static com.android.internal.jank.InteractionJankMonitor.ACTION_SESSION_CANCEL;
import static com.android.internal.jank.InteractionJankMonitor.ACTION_SESSION_END;
import static com.android.internal.jank.InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiThread;
import android.graphics.HardwareRendererObserver;
import android.os.Handler;
import android.os.Trace;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.Choreographer;
import android.view.FrameMetrics;
import android.view.SurfaceControl;
import android.view.SurfaceControl.JankData.JankType;
import android.view.ThreadedRenderer;
import android.view.View;
import android.view.ViewRootImpl;
import android.view.WindowCallbacks;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.DisplayRefreshRate.RefreshRate;
import com.android.internal.jank.InteractionJankMonitor.Configuration;
import com.android.internal.util.FrameworkStatsLog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.TimeUnit;

/**
 * A class that allows the app to get the frame metrics from HardwareRendererObserver.
 * @hide
 */
public class FrameTracker extends SurfaceControl.OnJankDataListener
        implements HardwareRendererObserver.OnFrameMetricsAvailableListener {
    private static final String TAG = "FrameTracker";

    private static final long INVALID_ID = -1;
    public static final int NANOS_IN_MILLISECOND = 1_000_000;

    private static final int MAX_LENGTH_EVENT_DESC = 127;

    private static final int MAX_FLUSH_ATTEMPTS = 3;
    private static final int FLUSH_DELAY_MILLISECOND = 60;

    static final int REASON_END_UNKNOWN = -1;
    static final int REASON_END_NORMAL = 0;
    static final int REASON_END_SURFACE_DESTROYED = 1;
    static final int REASON_CANCEL_NORMAL = 16;
    static final int REASON_CANCEL_NOT_BEGUN = 17;
    static final int REASON_CANCEL_SAME_VSYNC = 18;
    static final int REASON_CANCEL_TIMEOUT = 19;

    /** @hide */
    @IntDef({
            REASON_END_UNKNOWN,
            REASON_END_NORMAL,
            REASON_END_SURFACE_DESTROYED,
            REASON_CANCEL_NORMAL,
            REASON_CANCEL_NOT_BEGUN,
            REASON_CANCEL_SAME_VSYNC,
            REASON_CANCEL_TIMEOUT,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface Reasons {
    }

    private final HardwareRendererObserver mObserver;
    private final int mTraceThresholdMissedFrames;
    private final int mTraceThresholdFrameTimeMillis;
    private final ThreadedRendererWrapper mRendererWrapper;
    private final FrameMetricsWrapper mMetricsWrapper;
    private final SparseArray<JankInfo> mJankInfos = new SparseArray<>();
    private final Configuration mConfig;
    private final ViewRootWrapper mViewRoot;
    private final SurfaceControlWrapper mSurfaceControlWrapper;
    private final int mDisplayId;
    private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback;
    private final Handler mHandler;
    private final ChoreographerWrapper mChoreographer;
    private final StatsLogWrapper mStatsLog;
    private final boolean mDeferMonitoring;
    private final FrameTrackerListener mListener;

    @VisibleForTesting
    public final boolean mSurfaceOnly;

    private SurfaceControl mSurfaceControl;
    private long mBeginVsyncId = INVALID_ID;
    private long mEndVsyncId = INVALID_ID;
    private boolean mMetricsFinalized;
    private boolean mCancelled = false;
    private boolean mTracingStarted = false;
    private Runnable mWaitForFinishTimedOut;

    private static class JankInfo {
        long frameVsyncId;
        long totalDurationNanos;
        boolean isFirstFrame;
        boolean hwuiCallbackFired;
        boolean surfaceControlCallbackFired;
        @JankType int jankType;
        @RefreshRate int refreshRate;

        static JankInfo createFromHwuiCallback(long frameVsyncId, long totalDurationNanos,
                boolean isFirstFrame) {
            return new JankInfo(frameVsyncId, true, false, JANK_NONE, UNKNOWN_REFRESH_RATE,
                    totalDurationNanos, isFirstFrame);
        }

        static JankInfo createFromSurfaceControlCallback(long frameVsyncId,
                @JankType int jankType, @RefreshRate int refreshRate) {
            return new JankInfo(
                    frameVsyncId, false, true, jankType, refreshRate, 0, false /* isFirstFrame */);
        }

        private JankInfo(long frameVsyncId, boolean hwuiCallbackFired,
                boolean surfaceControlCallbackFired, @JankType int jankType,
                @RefreshRate int refreshRate,
                long totalDurationNanos, boolean isFirstFrame) {
            this.frameVsyncId = frameVsyncId;
            this.hwuiCallbackFired = hwuiCallbackFired;
            this.surfaceControlCallbackFired = surfaceControlCallbackFired;
            this.jankType = jankType;
            this.refreshRate = refreshRate;
            this.totalDurationNanos = totalDurationNanos;
            this.isFirstFrame = isFirstFrame;
        }

        @Override
        public String toString() {
            StringBuilder str = new StringBuilder();
            switch (jankType) {
                case JANK_NONE:
                    str.append("JANK_NONE");
                    break;
                case JANK_APP_DEADLINE_MISSED:
                    str.append("JANK_APP_DEADLINE_MISSED");
                    break;
                case JANK_SURFACEFLINGER_DEADLINE_MISSED:
                    str.append("JANK_SURFACEFLINGER_DEADLINE_MISSED");
                    break;
                case JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED:
                    str.append("JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED");
                    break;
                case DISPLAY_HAL:
                    str.append("DISPLAY_HAL");
                    break;
                case PREDICTION_ERROR:
                    str.append("PREDICTION_ERROR");
                    break;
                case SURFACE_FLINGER_SCHEDULING:
                    str.append("SURFACE_FLINGER_SCHEDULING");
                    break;
                default:
                    str.append("UNKNOWN: ").append(jankType);
                    break;
            }
            str.append(", ").append(frameVsyncId);
            str.append(", ").append(totalDurationNanos);
            return str.toString();
        }
    }

    public FrameTracker(@NonNull Configuration config,
            @Nullable ThreadedRendererWrapper renderer,
            @Nullable ViewRootWrapper viewRootWrapper,
            @NonNull SurfaceControlWrapper surfaceControlWrapper,
            @NonNull ChoreographerWrapper choreographer,
            @Nullable FrameMetricsWrapper metrics,
            @NonNull StatsLogWrapper statsLog,
            int traceThresholdMissedFrames, int traceThresholdFrameTimeMillis,
            @Nullable FrameTrackerListener listener) {
        mSurfaceOnly = config.isSurfaceOnly();
        mConfig = config;
        mHandler = config.getHandler();
        mChoreographer = choreographer;
        mSurfaceControlWrapper = surfaceControlWrapper;
        mStatsLog = statsLog;
        mDeferMonitoring = config.shouldDeferMonitor();

        // HWUI instrumentation init.
        mRendererWrapper = mSurfaceOnly ? null : renderer;
        mMetricsWrapper = mSurfaceOnly ? null : metrics;
        mViewRoot = mSurfaceOnly ? null : viewRootWrapper;
        mObserver = mSurfaceOnly
                ? null
                : new HardwareRendererObserver(this, mMetricsWrapper.getTiming(),
                        mHandler, /* waitForPresentTime= */ false);

        mTraceThresholdMissedFrames = traceThresholdMissedFrames;
        mTraceThresholdFrameTimeMillis = traceThresholdFrameTimeMillis;
        mListener = listener;
        mDisplayId = config.getDisplayId();

        if (mSurfaceOnly) {
            mSurfaceControl = config.getSurfaceControl();
            mSurfaceChangedCallback = null;
        } else {
            // HWUI instrumentation init.
            // If the surface isn't valid yet, wait until it's created.
            if (mViewRoot.getSurfaceControl().isValid()) {
                mSurfaceControl = mViewRoot.getSurfaceControl();
            }

            mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
                @Override
                public void surfaceCreated(SurfaceControl.Transaction t) {
                    mHandler.runWithScissors(() -> {
                        if (mSurfaceControl == null) {
                            mSurfaceControl = mViewRoot.getSurfaceControl();
                            if (mBeginVsyncId != INVALID_ID) {
                                // Previous begin invocation is not successfully, begin it again.
                                begin();
                            }
                        }
                    }, EXECUTOR_TASK_TIMEOUT);
                }

                @Override
                public void surfaceReplaced(SurfaceControl.Transaction t) {
                }

                @Override
                public void surfaceDestroyed() {

                    // Wait a while to give the system a chance for the remaining
                    // frames to arrive, then force finish the session.
                    mHandler.postDelayed(() -> {
                        if (!mMetricsFinalized) {
                            end(REASON_END_SURFACE_DESTROYED);
                            finish();
                        }
                    }, 50);
                }
            };
            // This callback has a reference to FrameTracker,
            // remember to remove it to avoid leakage.
            mViewRoot.addSurfaceChangedCallback(mSurfaceChangedCallback);
        }
    }

    /**
     * Begin a trace session of the CUJ.
     */
    @UiThread
    public void begin() {
        final long currentVsync = mChoreographer.getVsyncId();
        // In normal case, we should begin at the next frame,
        // the id of the next frame is not simply increased by 1,
        // but we can exclude the current frame at least.
        if (mBeginVsyncId == INVALID_ID) {
            mBeginVsyncId = mDeferMonitoring ? currentVsync + 1 : currentVsync;
        }
        if (mSurfaceControl != null) {
            if (mDeferMonitoring && currentVsync < mBeginVsyncId) {
                markEvent("FT#deferMonitoring", 0);
                // Normal case, we begin the instrument from the very beginning,
                // will exclude the first frame.
                postTraceStartMarker(this::beginInternal);
            } else {
                // If we don't begin the instrument from the very beginning,
                // there is no need to skip the frame where the begin invocation happens.
                beginInternal();
            }
        }
    }

    /**
     * Start trace section at appropriate time.
     */
    @VisibleForTesting
    public void postTraceStartMarker(Runnable action) {
        mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, action, null);
    }

    @UiThread
    private void beginInternal() {
        if (mCancelled || mEndVsyncId != INVALID_ID) {
            return;
        }
        mTracingStarted = true;
        String name = mConfig.getSessionName();
        Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, name, name, (int) mBeginVsyncId);
        markEvent("FT#beginVsync", mBeginVsyncId);
        markEvent("FT#layerId", mSurfaceControl.getLayerId());
        mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
        if (!mSurfaceOnly) {
            mRendererWrapper.addObserver(mObserver);
        }
    }

    /**
     * End the trace session of the CUJ.
     */
    @UiThread
    public boolean end(@Reasons int reason) {
        if (mCancelled || mEndVsyncId != INVALID_ID) return false;
        mEndVsyncId = mChoreographer.getVsyncId();
        // Cancel the session if:
        // 1. The session begins and ends at the same vsync id.
        // 2. The session never begun.
        if (mBeginVsyncId == INVALID_ID) {
            return cancel(REASON_CANCEL_NOT_BEGUN);
        } else if (mEndVsyncId <= mBeginVsyncId) {
            return cancel(REASON_CANCEL_SAME_VSYNC);
        } else {
            final String name = mConfig.getSessionName();
            markEvent("FT#end", reason);
            markEvent("FT#endVsync", mEndVsyncId);
            Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, name, (int) mBeginVsyncId);

            // We don't remove observer here,
            // will remove it when all the frame metrics in this duration are called back.
            // See onFrameMetricsAvailable for the logic of removing the observer.
            // Waiting at most 10 seconds for all callbacks to finish.
            mWaitForFinishTimedOut = new Runnable() {
                private int mFlushAttempts = 0;

                @Override
                public void run() {
                    if (mWaitForFinishTimedOut == null || mMetricsFinalized) {
                        return;
                    }

                    // Send a flush jank data transaction.
                    if (mSurfaceControl != null && mSurfaceControl.isValid()) {
                        SurfaceControl.Transaction.sendSurfaceFlushJankData(mSurfaceControl);
                    }

                    long delay;
                    if (mFlushAttempts < MAX_FLUSH_ATTEMPTS) {
                        delay = FLUSH_DELAY_MILLISECOND;
                        mFlushAttempts++;
                    } else {
                        mWaitForFinishTimedOut = () -> {
                            Log.e(TAG, "force finish cuj, time out: " + name);
                            finish();
                        };
                        delay = TimeUnit.SECONDS.toMillis(10);
                    }
                    mHandler.postDelayed(mWaitForFinishTimedOut, delay);
                }
            };
            mHandler.postDelayed(mWaitForFinishTimedOut, FLUSH_DELAY_MILLISECOND);
            notifyCujEvent(ACTION_SESSION_END, reason);
            return true;
        }
    }

    /**
     * Cancel the trace session of the CUJ.
     */
    @UiThread
    public boolean cancel(@Reasons int reason) {
        final boolean cancelFromEnd =
                reason == REASON_CANCEL_NOT_BEGUN || reason == REASON_CANCEL_SAME_VSYNC;
        if (mCancelled || (mEndVsyncId != INVALID_ID && !cancelFromEnd)) return false;
        mCancelled = true;
        markEvent("FT#cancel", reason);
        // We don't need to end the trace section if it has never begun.
        if (mTracingStarted) {
            Trace.asyncTraceForTrackEnd(
                    Trace.TRACE_TAG_APP, mConfig.getSessionName(), (int) mBeginVsyncId);
        }

        // Always remove the observers in cancel call to avoid leakage.
        removeObservers();

        // Notify the listener the session has been cancelled.
        // We don't notify the listeners if the session never begun.
        notifyCujEvent(ACTION_SESSION_CANCEL, reason);
        return true;
    }

    /**
     * Mark the FrameTracker events in the trace.
     *
     * @param eventName  The description of the trace event,
     * @param eventValue The value of the related trace event
     *                   Both shouldn't exceed {@link #MAX_LENGTH_EVENT_DESC}.
     */
    private void markEvent(@NonNull String eventName, long eventValue) {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
            String event = TextUtils.formatSimple("%s#%s", eventName, eventValue);
            if (event.length() > MAX_LENGTH_EVENT_DESC) {
                throw new IllegalArgumentException(TextUtils.formatSimple(
                        "The length of the trace event description <%s> exceeds %d",
                        event, MAX_LENGTH_EVENT_DESC));
            }
            Trace.instantForTrack(Trace.TRACE_TAG_APP, mConfig.getSessionName(), event);
        }
    }

    private void notifyCujEvent(String action, @Reasons int reason) {
        if (mListener == null) return;
        mListener.onCujEvents(this, action, reason);
    }

    @Override
    public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
        postCallback(() -> {
            if (mCancelled || mMetricsFinalized) {
                return;
            }

            for (SurfaceControl.JankData jankStat : jankData) {
                if (!isInRange(jankStat.frameVsyncId)) {
                    continue;
                }
                int refreshRate = DisplayRefreshRate.getRefreshRate(jankStat.frameIntervalNs);
                JankInfo info = findJankInfo(jankStat.frameVsyncId);
                if (info != null) {
                    info.surfaceControlCallbackFired = true;
                    info.jankType = jankStat.jankType;
                    info.refreshRate = refreshRate;
                } else {
                    mJankInfos.put((int) jankStat.frameVsyncId,
                            JankInfo.createFromSurfaceControlCallback(
                                    jankStat.frameVsyncId, jankStat.jankType, refreshRate));
                }
            }
            processJankInfos();
        });
    }

    /**
     * For easier argument capture.
     */
    @VisibleForTesting
    public void postCallback(Runnable callback) {
        mHandler.post(callback);
    }

    @Nullable
    private JankInfo findJankInfo(long frameVsyncId) {
        return mJankInfos.get((int) frameVsyncId);
    }

    private boolean isInRange(long vsyncId) {
        // It's possible that we may miss a callback for the frame with vsyncId == mEndVsyncId.
        // Because of that, we collect all frames even if they happen after the end so we eventually
        // have a frame after the end with both callbacks present.
        return vsyncId >= mBeginVsyncId;
    }

    @Override
    public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
        postCallback(() -> {
            if (mCancelled || mMetricsFinalized) {
                return;
            }

            // Since this callback might come a little bit late after the end() call.
            // We should keep tracking the begin / end timestamp that we can compare with
            // vsync timestamp to check if the frame is in the duration of the CUJ.
            long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
            boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
            long frameVsyncId =
                    mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];

            if (!isInRange(frameVsyncId)) {
                return;
            }
            JankInfo info = findJankInfo(frameVsyncId);
            if (info != null) {
                info.hwuiCallbackFired = true;
                info.totalDurationNanos = totalDurationNanos;
                info.isFirstFrame = isFirstFrame;
            } else {
                mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
                        frameVsyncId, totalDurationNanos, isFirstFrame));
            }
            processJankInfos();
        });
    }

    @UiThread
    private boolean hasReceivedCallbacksAfterEnd() {
        if (mEndVsyncId == INVALID_ID) {
            return false;
        }
        JankInfo last = mJankInfos.size() == 0 ? null : mJankInfos.valueAt(mJankInfos.size() - 1);
        if (last == null) {
            return false;
        }
        if (last.frameVsyncId < mEndVsyncId) {
            return false;
        }
        for (int i = mJankInfos.size() - 1; i >= 0; i--) {
            JankInfo info = mJankInfos.valueAt(i);
            if (info.frameVsyncId >= mEndVsyncId) {
                if (callbacksReceived(info)) {
                    return true;
                }
            }
        }
        return false;
    }

    @UiThread
    private void processJankInfos() {
        if (mMetricsFinalized) {
            return;
        }
        if (!hasReceivedCallbacksAfterEnd()) {
            return;
        }
        finish();
    }

    private boolean callbacksReceived(JankInfo info) {
        return mSurfaceOnly
                ? info.surfaceControlCallbackFired
                : info.hwuiCallbackFired && info.surfaceControlCallbackFired;
    }

    @UiThread
    private void finish() {
        if (mMetricsFinalized || mCancelled) return;
        mMetricsFinalized = true;

        mHandler.removeCallbacks(mWaitForFinishTimedOut);
        mWaitForFinishTimedOut = null;
        markEvent("FT#finish", mJankInfos.size());

        // The tracing has been ended, remove the observer, see if need to trigger perfetto.
        removeObservers();

        final String name = mConfig.getSessionName();

        int totalFramesCount = 0;
        long maxFrameTimeNanos = 0;
        int missedFramesCount = 0;
        int missedAppFramesCount = 0;
        int missedSfFramesCount = 0;
        int maxSuccessiveMissedFramesCount = 0;
        int successiveMissedFramesCount = 0;
        @RefreshRate int refreshRate = UNKNOWN_REFRESH_RATE;

        for (int i = 0; i < mJankInfos.size(); i++) {
            JankInfo info = mJankInfos.valueAt(i);
            final boolean isFirstDrawn = !mSurfaceOnly && info.isFirstFrame;
            if (isFirstDrawn) {
                continue;
            }
            if (info.frameVsyncId > mEndVsyncId) {
                break;
            }
            if (info.surfaceControlCallbackFired) {
                totalFramesCount++;
                boolean missedFrame = false;
                if ((info.jankType & JANK_APP_DEADLINE_MISSED) != 0) {
                    Log.w(TAG, "Missed App frame:" + info + ", CUJ=" + name);
                    missedAppFramesCount++;
                    missedFrame = true;
                }
                if ((info.jankType & DISPLAY_HAL) != 0
                        || (info.jankType & JANK_SURFACEFLINGER_DEADLINE_MISSED) != 0
                        || (info.jankType & JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED) != 0
                        || (info.jankType & SURFACE_FLINGER_SCHEDULING) != 0
                        || (info.jankType & PREDICTION_ERROR) != 0) {
                    Log.w(TAG, "Missed SF frame:" + info + ", CUJ=" + name);
                    missedSfFramesCount++;
                    missedFrame = true;
                }
                if (missedFrame) {
                    missedFramesCount++;
                    successiveMissedFramesCount++;
                } else {
                    maxSuccessiveMissedFramesCount = Math.max(
                            maxSuccessiveMissedFramesCount, successiveMissedFramesCount);
                    successiveMissedFramesCount = 0;
                }
                if (info.refreshRate != UNKNOWN_REFRESH_RATE && info.refreshRate != refreshRate) {
                    refreshRate = (refreshRate == UNKNOWN_REFRESH_RATE)
                            ? info.refreshRate : VARIABLE_REFRESH_RATE;
                }
                // TODO (b/174755489): Early latch currently gets fired way too often, so we have
                // to ignore it for now.
                if (!mSurfaceOnly && !info.hwuiCallbackFired) {
                    markEvent("FT#MissedHWUICallback", info.frameVsyncId);
                    Log.w(TAG, "Missing HWUI jank callback for vsyncId: " + info.frameVsyncId
                            + ", CUJ=" + name);
                }
            }
            if (!mSurfaceOnly && info.hwuiCallbackFired) {
                maxFrameTimeNanos = Math.max(info.totalDurationNanos, maxFrameTimeNanos);
                if (!info.surfaceControlCallbackFired) {
                    markEvent("FT#MissedSFCallback", info.frameVsyncId);
                    Log.w(TAG, "Missing SF jank callback for vsyncId: " + info.frameVsyncId
                            + ", CUJ=" + name);
                }
            }
        }
        maxSuccessiveMissedFramesCount = Math.max(
                maxSuccessiveMissedFramesCount, successiveMissedFramesCount);

        // Log the frame stats as counters to make them easily accessible in traces.
        Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#missedFrames", missedFramesCount);
        Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#missedAppFrames", missedAppFramesCount);
        Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#missedSfFrames", missedSfFramesCount);
        Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#totalFrames", totalFramesCount);
        Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#maxFrameTimeMillis",
                (int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND));
        Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#maxSuccessiveMissedFrames",
                maxSuccessiveMissedFramesCount);

        // Trigger perfetto if necessary.
        if (mListener != null
                && shouldTriggerPerfetto(missedFramesCount, (int) maxFrameTimeNanos)) {
            mListener.triggerPerfetto(mConfig);
        }
        if (mConfig.logToStatsd()) {
            mStatsLog.write(
                    FrameworkStatsLog.UI_INTERACTION_FRAME_INFO_REPORTED,
                    mDisplayId,
                    refreshRate,
                    mConfig.getStatsdInteractionType(),
                    totalFramesCount,
                    missedFramesCount,
                    maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
                    missedSfFramesCount,
                    missedAppFramesCount,
                    maxSuccessiveMissedFramesCount);
        }
    }

    ThreadedRendererWrapper getThreadedRenderer() {
        return mRendererWrapper;
    }

    ViewRootWrapper getViewRoot() {
        return mViewRoot;
    }

    private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) {
        boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
                && missedFramesCount >= mTraceThresholdMissedFrames;
        boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
                && maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
        return overMissedFramesThreshold || overFrameTimeThreshold;
    }

    /**
     * Remove all the registered listeners, observers and callbacks.
     */
    @VisibleForTesting
    @UiThread
    public void removeObservers() {
        mSurfaceControlWrapper.removeJankStatsListener(this);
        if (!mSurfaceOnly) {
            // HWUI part.
            mRendererWrapper.removeObserver(mObserver);
            if (mSurfaceChangedCallback != null) {
                mViewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
            }
        }
    }

    /**
     * A wrapper class that we can spy FrameMetrics (a final class) in unit tests.
     */
    public static class FrameMetricsWrapper {
        private final FrameMetrics mFrameMetrics;

        public FrameMetricsWrapper() {
            mFrameMetrics = new FrameMetrics();
        }

        /**
         * Wrapper method.
         * @return timing data of the metrics
         */
        public long[] getTiming() {
            return mFrameMetrics.mTimingData;
        }

        /**
         * Wrapper method.
         * @param index specific index of the timing data
         * @return the timing data of the specified index
         */
        public long getMetric(int index) {
            return mFrameMetrics.getMetric(index);
        }
    }

    /**
     * A wrapper class that we can spy ThreadedRenderer (a final class) in unit tests.
     */
    public static class ThreadedRendererWrapper {
        private final ThreadedRenderer mRenderer;

        public ThreadedRendererWrapper(ThreadedRenderer renderer) {
            mRenderer = renderer;
        }

        /**
         * Wrapper method.
         * @param observer observer
         */
        public void addObserver(HardwareRendererObserver observer) {
            mRenderer.addObserver(observer);
        }

        /**
         * Wrapper method.
         * @param observer observer
         */
        public void removeObserver(HardwareRendererObserver observer) {
            mRenderer.removeObserver(observer);
        }
    }

    public static class ViewRootWrapper {
        private final ViewRootImpl mViewRoot;

        public ViewRootWrapper(ViewRootImpl viewRoot) {
            mViewRoot = viewRoot;
        }

        /**
         * {@link ViewRootImpl#addSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback)}
         * @param callback {@link ViewRootImpl.SurfaceChangedCallback}
         */
        public void addSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback callback) {
            mViewRoot.addSurfaceChangedCallback(callback);
        }

        /**
         * {@link ViewRootImpl#removeSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback)}
         * @param callback {@link ViewRootImpl.SurfaceChangedCallback}
         */
        public void removeSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback callback) {
            mViewRoot.removeSurfaceChangedCallback(callback);
        }

        public SurfaceControl getSurfaceControl() {
            return mViewRoot.getSurfaceControl();
        }

        void requestInvalidateRootRenderNode() {
            mViewRoot.requestInvalidateRootRenderNode();
        }

        void addWindowCallbacks(WindowCallbacks windowCallbacks) {
            mViewRoot.addWindowCallbacks(windowCallbacks);
        }

        void removeWindowCallbacks(WindowCallbacks windowCallbacks) {
            mViewRoot.removeWindowCallbacks(windowCallbacks);
        }

        View getView() {
            return mViewRoot.getView();
        }

        int dipToPx(int dip) {
            final DisplayMetrics displayMetrics =
                    mViewRoot.mContext.getResources().getDisplayMetrics();
            return (int) (displayMetrics.density * dip + 0.5f);
        }
    }

    public static class SurfaceControlWrapper {

        public void addJankStatsListener(SurfaceControl.OnJankDataListener listener,
                SurfaceControl surfaceControl) {
            SurfaceControl.addJankDataListener(listener, surfaceControl);
        }

        public void removeJankStatsListener(SurfaceControl.OnJankDataListener listener) {
            SurfaceControl.removeJankDataListener(listener);
        }
    }

    public static class ChoreographerWrapper {

        private final Choreographer mChoreographer;

        public ChoreographerWrapper(Choreographer choreographer) {
            mChoreographer = choreographer;
        }

        public long getVsyncId() {
            return mChoreographer.getVsyncId();
        }
    }

    public static class StatsLogWrapper {
        private final DisplayResolutionTracker mDisplayResolutionTracker;

        public StatsLogWrapper(DisplayResolutionTracker displayResolutionTracker) {
            mDisplayResolutionTracker = displayResolutionTracker;
        }

        /** {@see FrameworkStatsLog#write) */
        public void write(int code, int displayId, @RefreshRate int refreshRate,
                int arg1, long arg2, long arg3, long arg4, long arg5, long arg6, long arg7) {
            FrameworkStatsLog.write(code, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
                    mDisplayResolutionTracker.getResolution(displayId), refreshRate);
        }
    }

    /**
     * A listener that notifies cuj events.
     */
    public interface FrameTrackerListener {
        /**
         * Notify that the CUJ session was created.
         *
         * @param tracker the tracker
         * @param action the specific action
         * @param reason the reason for the action
         */
        void onCujEvents(FrameTracker tracker, String action, @Reasons int reason);

        /**
         * Notify that the Perfetto trace should be triggered.
         *
         * @param config the tracker configuration
         */
        void triggerPerfetto(Configuration config);
    }
}
