Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3d90d978 authored by Wu Ahan's avatar Wu Ahan Committed by Android (Google) Code Review
Browse files

Merge "Add cancel method to aot infrastucture"

parents 86ee8183 e2659297
Loading
Loading
Loading
Loading
+37 −33
Original line number Diff line number Diff line
@@ -24,11 +24,11 @@ import android.util.Log;
import android.view.FrameMetrics;
import android.view.ThreadedRenderer;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor.Session;
import com.android.internal.util.FrameworkStatsLog;

/**
 * A class that allows the app to get the frame metrics from HardwareRendererObserver.
 * @hide
 */
public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvailableListener {
@@ -45,28 +45,21 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
    private long mBeginTime = UNKNOWN_TIMESTAMP;
    private long mEndTime = UNKNOWN_TIMESTAMP;
    private boolean mShouldTriggerTrace;
    private boolean mSessionEnd;
    private int mTotalFramesCount = 0;
    private int mMissedFramesCount = 0;
    private long mMaxFrameTimeNanos = 0;

    private Session mSession;

    public FrameTracker(@NonNull Session session,
            @NonNull Handler handler, @NonNull ThreadedRenderer renderer) {
        mSession = session;
        mRendererWrapper = new ThreadedRendererWrapper(renderer);
        mMetricsWrapper = new FrameMetricsWrapper();
        mObserver = new HardwareRendererObserver(this, mMetricsWrapper.getTiming(), handler);
    }

    /**
     * This constructor is only for unit tests.
     * Constructor of FrameTracker.
     * @param session a trace session.
     * @param renderer a test double for ThreadedRenderer
     * @param metrics a test double for FrameMetrics
     * @param handler a handler for handling callbacks.
     * @param renderer a ThreadedRendererWrapper instance.
     * @param metrics a FrameMetricsWrapper instance.
     */
    @VisibleForTesting
    public FrameTracker(@NonNull Session session, Handler handler,
    public FrameTracker(@NonNull Session session, @NonNull Handler handler,
            @NonNull ThreadedRendererWrapper renderer, @NonNull FrameMetricsWrapper metrics) {
        mSession = session;
        mRendererWrapper = renderer;
@@ -77,15 +70,11 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
    /**
     * Begin a trace session of the CUJ.
     */
    public void begin() {
    public synchronized void begin() {
        long timestamp = System.nanoTime();
        if (DEBUG) {
            Log.d(TAG, "begin: time(ns)=" + timestamp + ", begin(ns)=" + mBeginTime
                    + ", end(ns)=" + mEndTime + ", session=" + mSession);
        }
        if (mBeginTime != UNKNOWN_TIMESTAMP && mEndTime == UNKNOWN_TIMESTAMP) {
            // We have an ongoing tracing already, skip subsequent calls.
            return;
                    + ", end(ns)=" + mEndTime + ", session=" + mSession.getName());
        }
        mBeginTime = timestamp;
        mEndTime = UNKNOWN_TIMESTAMP;
@@ -96,32 +85,48 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
    /**
     * End the trace session of the CUJ.
     */
    public void end() {
    public synchronized void end() {
        long timestamp = System.nanoTime();
        if (DEBUG) {
            Log.d(TAG, "end: time(ns)=" + timestamp + ", begin(ns)=" + mBeginTime
                    + ", end(ns)=" + mEndTime + ", session=" + mSession);
        }
        if (mBeginTime == UNKNOWN_TIMESTAMP || mEndTime != UNKNOWN_TIMESTAMP) {
            // We haven't started a trace yet.
            return;
                    + ", end(ns)=" + mEndTime + ", session=" + mSession.getName());
        }
        mEndTime = timestamp;
        Trace.endAsyncSection(mSession.getName(), (int) mBeginTime);
        // 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.
    }

    /**
     * Cancel the trace session of the CUJ.
     */
    public synchronized void cancel() {
        if (mBeginTime == UNKNOWN_TIMESTAMP || mEndTime != UNKNOWN_TIMESTAMP) return;
        if (DEBUG) {
            Log.d(TAG, "cancel: time(ns)=" + System.nanoTime() + ", begin(ns)=" + mBeginTime
                    + ", end(ns)=" + mEndTime + ", session=" + mSession.getName());
        }
        Trace.endAsyncSection(mSession.getName(), (int) mBeginTime);
        mRendererWrapper.removeObserver(mObserver);
        mBeginTime = UNKNOWN_TIMESTAMP;
        mEndTime = UNKNOWN_TIMESTAMP;
        mShouldTriggerTrace = false;
    }

    @Override
    public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
    public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
        // Since this callback might come a little bit late after the end() call.
        // We should keep tracking the begin / end timestamp.
        // Then compare with vsync timestamp to check if the frame is in the duration of the CUJ.

        if (mBeginTime == UNKNOWN_TIMESTAMP) return; // We haven't started tracing yet.
        long vsyncTimestamp = mMetricsWrapper.getMetric(FrameMetrics.VSYNC_TIMESTAMP);
        if (vsyncTimestamp < mBeginTime) return; // The tracing has been started.
        // Discard the frame metrics which is not in the trace session.
        if (vsyncTimestamp < mBeginTime) return;

        // If the end time has not been set, we are still in the tracing.
        if (mEndTime != UNKNOWN_TIMESTAMP && vsyncTimestamp > mEndTime) {
        // We stop getting callback when the vsync is later than the end calls.
        if (mEndTime != UNKNOWN_TIMESTAMP && vsyncTimestamp > mEndTime && !mSessionEnd) {
            mSessionEnd = true;
            // The tracing has been ended, remove the observer, see if need to trigger perfetto.
            mRendererWrapper.removeObserver(mObserver);

@@ -170,9 +175,8 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
    /**
     * Trigger the prefetto daemon.
     */
    @VisibleForTesting
    public void triggerPerfetto() {
        InteractionJankMonitor.trigger();
        InteractionJankMonitor.getInstance().trigger();
    }

    /**
+197 −75
Original line number Diff line number Diff line
@@ -21,15 +21,17 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.os.HandlerThread;
import android.view.ThreadedRenderer;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.jank.FrameTracker.FrameMetricsWrapper;
import com.android.internal.jank.FrameTracker.ThreadedRendererWrapper;

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

/**
 * This class let users to begin and end the always on tracing mechanism.
@@ -38,11 +40,17 @@ import java.util.Map;
public class InteractionJankMonitor {
    private static final String TAG = InteractionJankMonitor.class.getSimpleName();
    private static final boolean DEBUG = false;
    private static final Object LOCK = new Object();
    private static final String DEFAULT_WORKER_NAME = TAG + "-Worker";
    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5L);

    // Every value must have a corresponding entry in CUJ_STATSD_INTERACTION_TYPE.
    public static final int CUJ_NOTIFICATION_SHADE_MOTION = 0;
    public static final int CUJ_NOTIFICATION_SHADE_GESTURE = 1;
    public static final int CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE = 1;
    public static final int CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE_LOCK = 0;
    public static final int CUJ_NOTIFICATION_SHADE_SCROLL_FLING = 0;
    public static final int CUJ_NOTIFICATION_SHADE_ROW_EXPAND = 0;
    public static final int CUJ_NOTIFICATION_SHADE_ROW_SWIPE = 0;
    public static final int CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE = 0;
    public static final int CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE = 0;

    private static final int NO_STATSD_LOGGING = -1;

@@ -53,141 +61,255 @@ public class InteractionJankMonitor {
            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__NOTIFICATION_SHADE_SWIPE,
    };

    private static ThreadedRenderer sRenderer;
    private static Map<String, FrameTracker> sRunningTracker;
    private static HandlerThread sWorker;
    private static boolean sInitialized;
    private static volatile InteractionJankMonitor sInstance;

    private ThreadedRendererWrapper mRenderer;
    private FrameMetricsWrapper mMetrics;
    private SparseArray<FrameTracker> mRunningTrackers;
    private SparseArray<Runnable> mTimeoutActions;
    private HandlerThread mWorker;
    private boolean mInitialized;

    /** @hide */
    @IntDef({
            CUJ_NOTIFICATION_SHADE_MOTION,
            CUJ_NOTIFICATION_SHADE_GESTURE
            CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
            CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE_LOCK,
            CUJ_NOTIFICATION_SHADE_SCROLL_FLING,
            CUJ_NOTIFICATION_SHADE_ROW_EXPAND,
            CUJ_NOTIFICATION_SHADE_ROW_SWIPE,
            CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
            CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CujType {}

    /**
     * @param view Any view in the view tree to get context and ThreadedRenderer.
     * Get the singleton of InteractionJankMonitor.
     * @return instance of InteractionJankMonitor
     */
    public static void init(@NonNull View view) {
        init(view, null, null, null);
    public static InteractionJankMonitor getInstance() {
        // Use DCL here since this method might be invoked very often.
        if (sInstance == null) {
            synchronized (InteractionJankMonitor.class) {
                if (sInstance == null) {
                    sInstance = new InteractionJankMonitor(new HandlerThread(DEFAULT_WORKER_NAME));
                }
            }
        }
        return sInstance;
    }

    /**
     * Should be only invoked internally or from unit tests.
     * This constructor should be only public to tests.
     * @param worker the worker thread for the callbacks
     */
    @VisibleForTesting
    public static void init(@NonNull View view, @NonNull ThreadedRenderer renderer,
            @NonNull Map<String, FrameTracker> map, @NonNull HandlerThread worker) {
    public InteractionJankMonitor(@NonNull HandlerThread worker) {
        mRunningTrackers = new SparseArray<>();
        mTimeoutActions = new SparseArray<>();
        mWorker = worker;
    }

    /**
     * Init InteractionJankMonitor for later instrumentation.
     * @param view Any view in the view tree to get context and ThreadedRenderer.
     * @return boolean true if the instance has been initialized successfully.
     */
    public boolean init(@NonNull View view) {
        //TODO (163505250): This should be no-op if not in droid food rom.
        synchronized (LOCK) {
            if (!sInitialized) {
        if (!mInitialized) {
            synchronized (this) {
                if (!mInitialized) {
                    if (!view.isAttachedToWindow()) {
                    throw new IllegalStateException("View is not attached!");
                        Log.d(TAG, "Expect an attached view!", new Throwable());
                        return false;
                    }
                sRenderer = renderer == null ? view.getThreadedRenderer() : renderer;
                sRunningTracker = map == null ? new HashMap<>() : map;
                sWorker = worker == null ? new HandlerThread("Aot-Worker") : worker;
                sWorker.start();
                sInitialized = true;
                    mRenderer = new ThreadedRendererWrapper(view.getThreadedRenderer());
                    mMetrics = new FrameMetricsWrapper();
                    mWorker.start();
                    mInitialized = true;
                }
            }
        }
        return true;
    }

    /**
     * Must invoke init() before invoking this method.
     * Create a {@link FrameTracker} instance.
     * @param session the session associates with this tracker
     * @return instance of the FrameTracker
     */
    public static void begin(@NonNull @CujType int cujType) {
        begin(cujType, null);
    @VisibleForTesting
    public FrameTracker createFrameTracker(Session session) {
        synchronized (this) {
            if (!mInitialized) return null;
            return new FrameTracker(session, mWorker.getThreadHandler(), mRenderer, mMetrics);
        }
    }

    /**
     * Should be only invoked internally or from unit tests.
     * Begin a trace session, must invoke {@link #init(View)} before invoking this method.
     * @param cujType the specific {@link InteractionJankMonitor.CujType}.
     * @return boolean true if the tracker is started successfully, false otherwise.
     */
    @VisibleForTesting
    public static void begin(@NonNull @CujType int cujType, FrameTracker tracker) {
    public boolean begin(@CujType int cujType) {
        //TODO (163505250): This should be no-op if not in droid food rom.
        //TODO (163510843): Remove synchronized, add @UiThread if only invoked from ui threads.
        synchronized (LOCK) {
            checkInitStateLocked();
            Session session = new Session(cujType);
            FrameTracker currentTracker = getTracker(session.getName());
            if (currentTracker != null) return;
            if (tracker == null) {
                tracker = new FrameTracker(session, sWorker.getThreadHandler(), sRenderer);
        synchronized (this) {
            return begin(cujType, 0L /* timeout */);
        }
            sRunningTracker.put(session.getName(), tracker);
    }

    /**
     * Begin a trace session, must invoke {@link #init(View)} before invoking this method.
     * @param cujType the specific {@link InteractionJankMonitor.CujType}.
     * @param timeout the elapsed time in ms until firing the timeout action.
     * @return boolean true if the tracker is started successfully, false otherwise.
     */
    public boolean begin(@CujType int cujType, long timeout) {
        //TODO (163505250): This should be no-op if not in droid food rom.
        synchronized (this) {
            if (!mInitialized) {
                Log.d(TAG, "Not initialized!", new Throwable());
                return false;
            }
            Session session = new Session(cujType);
            FrameTracker tracker = getTracker(session);
            // Skip subsequent calls if we already have an ongoing tracing.
            if (tracker != null) return false;

            // begin a new trace session.
            tracker = createFrameTracker(session);
            mRunningTrackers.put(cujType, tracker);
            tracker.begin();

            // Cancel the trace if we don't get an end() call in specified duration.
            timeout = timeout > 0L ? timeout : DEFAULT_TIMEOUT_MS;
            Runnable timeoutAction = () -> cancel(cujType);
            mTimeoutActions.put(cujType, timeoutAction);
            mWorker.getThreadHandler().postDelayed(timeoutAction, timeout);
            return true;
        }
    }

    /**
     * Must invoke init() before invoking this method.
     * End a trace session, must invoke {@link #init(View)} before invoking this method.
     * @param cujType the specific {@link InteractionJankMonitor.CujType}.
     * @return boolean true if the tracker is ended successfully, false otherwise.
     */
    public static void end(@NonNull @CujType int cujType) {
    public boolean end(@CujType int cujType) {
        //TODO (163505250): This should be no-op if not in droid food rom.
        //TODO (163510843): Remove synchronized, add @UiThread if only invoked from ui threads.
        synchronized (LOCK) {
            checkInitStateLocked();
        synchronized (this) {
            if (!mInitialized) {
                Log.d(TAG, "Not initialized!", new Throwable());
                return false;
            }
            // remove the timeout action first.
            Runnable timeout = mTimeoutActions.get(cujType);
            if (timeout != null) {
                mWorker.getThreadHandler().removeCallbacks(timeout);
                mTimeoutActions.remove(cujType);
            }

            Session session = new Session(cujType);
            FrameTracker tracker = getTracker(session.getName());
            if (tracker != null) {
            FrameTracker tracker = getTracker(session);
            // Skip this call since we haven't started a trace yet.
            if (tracker == null) return false;
            tracker.end();
                sRunningTracker.remove(session.getName());
            mRunningTrackers.remove(session.getCuj());
            return true;
        }
    }

    /**
     * Cancel the trace session, must invoke {@link #init(View)} before invoking this method.
     * @return boolean true if the tracker is cancelled successfully, false otherwise.
     */
    public boolean cancel(@CujType int cujType) {
        //TODO (163505250): This should be no-op if not in droid food rom.
        synchronized (this) {
            if (!mInitialized) {
                Log.d(TAG, "Not initialized!", new Throwable());
                return false;
            }
            // remove the timeout action first.
            Runnable timeout = mTimeoutActions.get(cujType);
            if (timeout != null) {
                mWorker.getThreadHandler().removeCallbacks(timeout);
                mTimeoutActions.remove(cujType);
            }

    private static void checkInitStateLocked() {
        if (!sInitialized) {
            throw new IllegalStateException("InteractionJankMonitor not initialized!");
            Session session = new Session(cujType);
            FrameTracker tracker = getTracker(session);
            // Skip this call since we haven't started a trace yet.
            if (tracker == null) return false;
            tracker.cancel();
            mRunningTrackers.remove(session.getCuj());
            return true;
        }
    }

    private void destroy() {
        synchronized (this) {
            int trackers = mRunningTrackers.size();
            for (int i = 0; i < trackers; i++) {
                mRunningTrackers.valueAt(i).cancel();
            }
            mRunningTrackers = null;
            mTimeoutActions.clear();
            mTimeoutActions = null;
            mWorker.quit();
            mWorker = null;
        }
    }

    /**
     * Should be only invoked from unit tests.
     * Abandon current instance.
     */
    @VisibleForTesting
    public static void reset() {
        sInitialized = false;
        sRenderer = null;
        sRunningTracker = null;
        if (sWorker != null) {
            sWorker.quit();
            sWorker = null;
    public static void abandon() {
        if (sInstance == null) return;
        synchronized (InteractionJankMonitor.class) {
            if (sInstance == null) return;
            sInstance.destroy();
            sInstance = null;
        }
    }

    private static FrameTracker getTracker(String sessionName) {
        synchronized (LOCK) {
            return sRunningTracker.get(sessionName);
    private FrameTracker getTracker(Session session) {
        synchronized (this) {
            if (!mInitialized) return null;
            return mRunningTrackers.get(session.getCuj());
        }
    }

    /**
     * Trigger the perfetto daemon to collect and upload data.
     */
    public static void trigger() {
        sWorker.getThreadHandler().post(
    @VisibleForTesting
    public void trigger() {
        synchronized (this) {
            if (!mInitialized) return;
            mWorker.getThreadHandler().post(
                    () -> PerfettoTrigger.trigger(PerfettoTrigger.TRIGGER_TYPE_JANK));
        }
    }

    /**
     * A class to represent a session.
     */
    public static class Session {
        private @CujType int mId;
        private @CujType int mCujType;

        public Session(@CujType int session) {
            mId = session;
        public Session(@CujType int cujType) {
            mCujType = cujType;
        }

        public int getId() {
            return mId;
        public int getCuj() {
            return mCujType;
        }

        public int getStatsdInteractionType() {
            return CUJ_TO_STATSD_INTERACTION_TYPE[mId];
            return CUJ_TO_STATSD_INTERACTION_TYPE[mCujType];
        }

        /** Describes whether the measurement from this session should be written to statsd. */
@@ -196,7 +318,7 @@ public class InteractionJankMonitor {
        }

        public String getName() {
            return "CujType<" + mId + ">";
            return "Cuj<" + getCuj() + ">";
        }
    }

+27 −11
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@

package com.android.internal.jank;

import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_GESTURE;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;

import static com.google.common.truth.Truth.assertThat;

@@ -75,19 +75,12 @@ public class FrameTrackerTest {
        doNothing().when(mRenderer).addObserver(any());
        doNothing().when(mRenderer).removeObserver(any());

        Session session = new Session(CUJ_NOTIFICATION_SHADE_GESTURE);
        mTracker = Mockito.spy(new FrameTracker(session, handler, mRenderer, mWrapper));
        Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
        mTracker = Mockito.spy(
                new FrameTracker(session, handler, mRenderer, mWrapper));
        doNothing().when(mTracker).triggerPerfetto();
    }

    @Test
    public void testIgnoresSecondBegin() {
        // Observer should be only added once in continuous calls.
        mTracker.begin();
        mTracker.begin();
        verify(mRenderer, only()).addObserver(any());
    }

    @Test
    public void testOnlyFirstFrameOverThreshold() {
        // Just provide current timestamp anytime mWrapper asked for VSYNC_TIMESTAMP
@@ -170,6 +163,29 @@ public class FrameTrackerTest {
        verify(mTracker).triggerPerfetto();
    }

    @Test
    public void testBeginCancel() {
        mTracker.begin();
        verify(mRenderer).addObserver(any());

        // First frame - not janky
        setupFirstFrameMockWithDuration(4);
        mTracker.onFrameMetricsAvailable(0);

        // normal frame - not janky
        setupOtherFrameMockWithDuration(12);
        mTracker.onFrameMetricsAvailable(0);

        // a janky frame
        setupOtherFrameMockWithDuration(30);
        mTracker.onFrameMetricsAvailable(0);

        mTracker.cancel();
        verify(mRenderer).removeObserver(any());
        // Since the tracker has been cancelled, shouldn't trigger perfetto.
        verify(mTracker, never()).triggerPerfetto();
    }

    private void setupFirstFrameMockWithDuration(long durationMillis) {
        doReturn(1L).when(mWrapper).getMetric(FrameMetrics.FIRST_DRAW_FRAME);
        doReturn(TimeUnit.MILLISECONDS.toNanos(durationMillis))
+60 −39

File changed.

Preview size limit exceeded, changes collapsed.