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

Commit d620436b authored by Marcin Oczeretko's avatar Marcin Oczeretko Committed by Android (Google) Code Review
Browse files

Merge "Add configuration options to InteractionJankMonitor"

parents 72c35498 3b462716
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -464,6 +464,13 @@ public final class DeviceConfig {
     */
    public static final String NAMESPACE_LATENCY_TRACKER = "latency_tracker";

    /**
     * InteractionJankMonitor properties definitions.
     *
     * @hide
     */
    public static final String NAMESPACE_INTERACTION_JANK_MONITOR = "interaction_jank_monitor";

    private static final Object sLock = new Object();
    @GuardedBy("sLock")
    private static ArrayMap<OnPropertiesChangedListener, Pair<String, Executor>> sListeners =
+19 −18
Original line number Diff line number Diff line
@@ -37,34 +37,33 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
    //TODO (163431584): need also consider other refresh rates.
    private static final long JANK_THRESHOLD_NANOS = 1000000000 / 60;
    private static final long UNKNOWN_TIMESTAMP = -1;
    public static final int NANOS_IN_MILLISECOND = 1_000_000;

    private final HardwareRendererObserver mObserver;
    private final int mTraceThresholdMissedFrames;
    private final int mTraceThresholdFrameTimeMillis;
    private final ThreadedRendererWrapper mRendererWrapper;
    private final FrameMetricsWrapper mMetricsWrapper;

    private long mBeginTime = UNKNOWN_TIMESTAMP;
    private long mEndTime = UNKNOWN_TIMESTAMP;
    private boolean mShouldTriggerTrace;
    private boolean mSessionEnd;
    private boolean mCancelled = false;
    private int mTotalFramesCount = 0;
    private int mMissedFramesCount = 0;
    private long mMaxFrameTimeNanos = 0;

    private Session mSession;

    /**
     * Constructor of FrameTracker.
     * @param session a trace session.
     * @param handler a handler for handling callbacks.
     * @param renderer a ThreadedRendererWrapper instance.
     * @param metrics a FrameMetricsWrapper instance.
     */
    public FrameTracker(@NonNull Session session, @NonNull Handler handler,
            @NonNull ThreadedRendererWrapper renderer, @NonNull FrameMetricsWrapper metrics) {
            @NonNull ThreadedRendererWrapper renderer, @NonNull FrameMetricsWrapper metrics,
            int traceThresholdMissedFrames, int traceThresholdFrameTimeMillis) {
        mSession = session;
        mRendererWrapper = renderer;
        mMetricsWrapper = metrics;
        mObserver = new HardwareRendererObserver(this, mMetricsWrapper.getTiming(), handler);
        mTraceThresholdMissedFrames = traceThresholdMissedFrames;
        mTraceThresholdFrameTimeMillis = traceThresholdFrameTimeMillis;
    }

    /**
@@ -109,13 +108,15 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
        }
        Trace.endAsyncSection(mSession.getName(), (int) mBeginTime);
        mRendererWrapper.removeObserver(mObserver);
        mBeginTime = UNKNOWN_TIMESTAMP;
        mEndTime = UNKNOWN_TIMESTAMP;
        mShouldTriggerTrace = false;
        mCancelled = true;
    }

    @Override
    public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
        if (mCancelled) {
            return;
        }

        // 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.
@@ -136,13 +137,14 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
            Trace.traceCounter(Trace.TRACE_TAG_APP, mSession.getName() + "#totalFrames",
                    mTotalFramesCount);
            Trace.traceCounter(Trace.TRACE_TAG_APP, mSession.getName() + "#maxFrameTimeMillis",
                    (int) (mMaxFrameTimeNanos / 1_000_000));
                    (int) (mMaxFrameTimeNanos / NANOS_IN_MILLISECOND));

            // Trigger perfetto if necessary.
            if (mShouldTriggerTrace) {
                if (DEBUG) {
                    Log.v(TAG, "Found janky frame, triggering perfetto.");
                }
            boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
                    && mMissedFramesCount >= mTraceThresholdMissedFrames;
            boolean overFrameTimeThreshold = mTraceThresholdFrameTimeMillis != -1
                    && mMaxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
            if (overMissedFramesThreshold || overFrameTimeThreshold) {
                triggerPerfetto();
            }
            if (mSession.logToStatsd()) {
@@ -168,7 +170,6 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai

        if (isJankyFrame) {
            mMissedFramesCount += 1;
            mShouldTriggerTrace = true;
        }
    }

+80 −43
Original line number Diff line number Diff line
@@ -36,7 +36,10 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.os.Build;
import android.os.HandlerExecutor;
import android.os.HandlerThread;
import android.provider.DeviceConfig;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;
@@ -47,6 +50,7 @@ import com.android.internal.jank.FrameTracker.ThreadedRendererWrapper;

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

/**
@@ -55,9 +59,21 @@ import java.util.concurrent.TimeUnit;
 */
public class InteractionJankMonitor {
    private static final String TAG = InteractionJankMonitor.class.getSimpleName();
    private static final boolean DEBUG = false;
    private static final String DEFAULT_WORKER_NAME = TAG + "-Worker";
    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5L);
    private static final String SETTINGS_ENABLED_KEY = "enabled";
    private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
    private static final String SETTINGS_THRESHOLD_MISSED_FRAMES_KEY =
            "trace_threshold_missed_frames";
    private static final String SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY =
            "trace_threshold_frame_time_millis";
    /** Default to being enabled on debug builds. */
    private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
    /** Default to collecting data for all CUJs. */
    private static final int DEFAULT_SAMPLING_INTERVAL = 1;
    /** Default to triggering trace if 3 frames are missed OR a frame takes at least 64ms */
    private static final int DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES = 3;
    private static final int DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS = 64;

    // Every value must have a corresponding entry in CUJ_STATSD_INTERACTION_TYPE.
    public static final int CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE = 0;
@@ -106,12 +122,20 @@ public class InteractionJankMonitor {

    private static volatile InteractionJankMonitor sInstance;

    private final DeviceConfig.OnPropertiesChangedListener mPropertiesChangedListener =
            this::updateProperties;

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

    private boolean mInitialized;
    private boolean mEnabled = DEFAULT_ENABLED;
    private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
    private int mTraceThresholdMissedFrames = DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES;
    private int mTraceThresholdFrameTimeMillis = DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS;

    /** @hide */
    @IntDef({
@@ -134,10 +158,12 @@ public class InteractionJankMonitor {
            CUJ_NOTIFICATION_APP_START,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CujType {}
    public @interface CujType {
    }

    /**
     * Get the singleton of InteractionJankMonitor.
     *
     * @return instance of InteractionJankMonitor
     */
    public static InteractionJankMonitor getInstance() {
@@ -154,6 +180,7 @@ public class InteractionJankMonitor {

    /**
     * This constructor should be only public to tests.
     *
     * @param worker the worker thread for the callbacks
     */
    @VisibleForTesting
@@ -165,11 +192,11 @@ public class InteractionJankMonitor {

    /**
     * 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.
        if (!mInitialized) {
            synchronized (this) {
                if (!mInitialized) {
@@ -180,7 +207,20 @@ public class InteractionJankMonitor {
                    mRenderer = new ThreadedRendererWrapper(view.getThreadedRenderer());
                    mMetrics = new FrameMetricsWrapper();
                    mWorker.start();
                    mEnabled = DEFAULT_ENABLED;
                    mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
                    mInitialized = true;

                    // Post initialization to the background in case we're running on the main
                    // thread.
                    mWorker.getThreadHandler().post(
                            () -> mPropertiesChangedListener.onPropertiesChanged(
                                    DeviceConfig.getProperties(
                                            DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR)));
                    DeviceConfig.addOnPropertiesChangedListener(
                            DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR,
                            new HandlerExecutor(mWorker.getThreadHandler()),
                            mPropertiesChangedListener);
                }
            }
        }
@@ -189,6 +229,7 @@ public class InteractionJankMonitor {

    /**
     * Create a {@link FrameTracker} instance.
     *
     * @param session the session associates with this tracker
     * @return instance of the FrameTracker
     */
@@ -196,47 +237,50 @@ public class InteractionJankMonitor {
    public FrameTracker createFrameTracker(Session session) {
        synchronized (this) {
            if (!mInitialized) return null;
            return new FrameTracker(session, mWorker.getThreadHandler(), mRenderer, mMetrics);
            return new FrameTracker(session, mWorker.getThreadHandler(), mRenderer, mMetrics,
                    mTraceThresholdMissedFrames, mTraceThresholdFrameTimeMillis);
        }
    }

    /**
     * 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.
     */
    public boolean begin(@CujType int cujType) {
        //TODO (163505250): This should be no-op if not in droid food rom.
        synchronized (this) {
            return begin(cujType, 0L /* timeout */);
            return begin(cujType, DEFAULT_TIMEOUT_MS);
        }
    }

    /**
     * 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);
            boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
            if (!mEnabled || !shouldSample) {
                return false;
            }
            FrameTracker tracker = getTracker(cujType);
            // Skip subsequent calls if we already have an ongoing tracing.
            if (tracker != null) return false;

            // begin a new trace session.
            tracker = createFrameTracker(session);
            tracker = createFrameTracker(new Session(cujType));
            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);
@@ -246,6 +290,7 @@ public class InteractionJankMonitor {

    /**
     * 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.
     */
@@ -263,18 +308,18 @@ public class InteractionJankMonitor {
                mTimeoutActions.remove(cujType);
            }

            Session session = new Session(cujType);
            FrameTracker tracker = getTracker(session);
            FrameTracker tracker = getTracker(cujType);
            // Skip this call since we haven't started a trace yet.
            if (tracker == null) return false;
            tracker.end();
            mRunningTrackers.remove(session.getCuj());
            mRunningTrackers.remove(cujType);
            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) {
@@ -291,48 +336,38 @@ public class InteractionJankMonitor {
                mTimeoutActions.remove(cujType);
            }

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

    private void destroy() {
    private FrameTracker getTracker(@CujType int cuj) {
        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;
            if (!mInitialized) return null;
            return mRunningTrackers.get(cuj);
        }
    }

    /**
     * Abandon current instance.
     */
    @VisibleForTesting
    public static void abandon() {
        if (sInstance == null) return;
        synchronized (InteractionJankMonitor.class) {
            if (sInstance == null) return;
            sInstance.destroy();
            sInstance = null;
    private void updateProperties(DeviceConfig.Properties properties) {
        synchronized (this) {
            mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
                    DEFAULT_SAMPLING_INTERVAL);
            mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
            mTraceThresholdMissedFrames = properties.getInt(SETTINGS_THRESHOLD_MISSED_FRAMES_KEY,
                    DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES);
            mTraceThresholdFrameTimeMillis = properties.getInt(
                    SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY,
                    DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS);
        }
    }

    private FrameTracker getTracker(Session session) {
        synchronized (this) {
            if (!mInitialized) return null;
            return mRunningTrackers.get(session.getCuj());
        }
    @VisibleForTesting
    public DeviceConfig.OnPropertiesChangedListener getPropertiesChangedListener() {
        return mPropertiesChangedListener;
    }

    /**
@@ -402,12 +437,14 @@ public class InteractionJankMonitor {
     * A class to represent a session.
     */
    public static class Session {
        private @CujType int mCujType;
        @CujType
        private int mCujType;

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

        @CujType
        public int getCuj() {
            return mCujType;
        }
+1 −1
Original line number Diff line number Diff line
@@ -77,7 +77,7 @@ public class FrameTrackerTest {

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

+22 −5
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertWithMessage;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@@ -32,6 +33,7 @@ import static org.mockito.Mockito.verify;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.provider.DeviceConfig;
import android.view.View;
import android.view.ViewAttachTestActivity;

@@ -50,6 +52,7 @@ import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
@@ -71,8 +74,6 @@ public class InteractionJankMonitorTest {
        mView = mActivity.getWindow().getDecorView();
        assertThat(mView.isAttachedToWindow()).isTrue();

        InteractionJankMonitor.abandon();

        Handler handler = spy(new Handler(mActivity.getMainLooper()));
        doReturn(true).when(handler).sendMessageAtTime(any(), anyLong());
        mWorker = spy(new HandlerThread("Interaction-jank-monitor-test"));
@@ -93,7 +94,7 @@ public class InteractionJankMonitorTest {
        Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
        FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
                new ThreadedRendererWrapper(mView.getThreadedRenderer()),
                new FrameMetricsWrapper()));
                new FrameMetricsWrapper(), 1, -1));
        doReturn(tracker).when(monitor).createFrameTracker(any());

        // Simulate a trace session and see if begin / end are invoked.
@@ -103,6 +104,21 @@ public class InteractionJankMonitorTest {
        verify(tracker).end();
    }

    @Test
    public void testDisabledThroughDeviceConfig() {
        InteractionJankMonitor monitor = new InteractionJankMonitor(mWorker);
        monitor.init(mView);

        HashMap<String, String> propertiesValues = new HashMap<>();
        propertiesValues.put("enabled", "false");
        DeviceConfig.Properties properties = new DeviceConfig.Properties(
                DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR, propertiesValues);
        monitor.getPropertiesChangedListener().onPropertiesChanged(properties);

        assertThat(monitor.begin(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isFalse();
        assertThat(monitor.end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isFalse();
    }

    @Test
    public void testCheckInitState() {
        InteractionJankMonitor monitor = new InteractionJankMonitor(mWorker);
@@ -134,12 +150,13 @@ public class InteractionJankMonitorTest {
        Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE);
        FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
                new ThreadedRendererWrapper(mView.getThreadedRenderer()),
                new FrameMetricsWrapper()));
                new FrameMetricsWrapper(), 1, -1));
        doReturn(tracker).when(monitor).createFrameTracker(any());

        assertThat(monitor.begin(session.getCuj())).isTrue();
        verify(tracker).begin();
        verify(mWorker.getThreadHandler()).sendMessageAtTime(captor.capture(), anyLong());
        verify(mWorker.getThreadHandler(), atLeastOnce()).sendMessageAtTime(captor.capture(),
                anyLong());
        Runnable runnable = captor.getValue().getCallback();
        assertThat(runnable).isNotNull();
        mWorker.getThreadHandler().removeCallbacks(runnable);