Loading core/java/android/provider/DeviceConfig.java +7 −0 Original line number Diff line number Diff line Loading @@ -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 = Loading core/java/com/android/internal/jank/FrameTracker.java +19 −18 Original line number Diff line number Diff line Loading @@ -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; } /** Loading Loading @@ -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. Loading @@ -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()) { Loading @@ -168,7 +170,6 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai if (isJankyFrame) { mMissedFramesCount += 1; mShouldTriggerTrace = true; } } Loading core/java/com/android/internal/jank/InteractionJankMonitor.java +80 −43 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -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; Loading Loading @@ -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({ Loading @@ -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() { Loading @@ -154,6 +180,7 @@ public class InteractionJankMonitor { /** * This constructor should be only public to tests. * * @param worker the worker thread for the callbacks */ @VisibleForTesting Loading @@ -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) { Loading @@ -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); } } } Loading @@ -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 */ Loading @@ -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); Loading @@ -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. */ Loading @@ -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) { Loading @@ -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; } /** Loading Loading @@ -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; } Loading core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -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(); } Loading core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java +22 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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")); Loading @@ -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. Loading @@ -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); Loading Loading @@ -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); Loading Loading
core/java/android/provider/DeviceConfig.java +7 −0 Original line number Diff line number Diff line Loading @@ -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 = Loading
core/java/com/android/internal/jank/FrameTracker.java +19 −18 Original line number Diff line number Diff line Loading @@ -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; } /** Loading Loading @@ -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. Loading @@ -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()) { Loading @@ -168,7 +170,6 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai if (isJankyFrame) { mMissedFramesCount += 1; mShouldTriggerTrace = true; } } Loading
core/java/com/android/internal/jank/InteractionJankMonitor.java +80 −43 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; /** Loading @@ -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; Loading Loading @@ -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({ Loading @@ -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() { Loading @@ -154,6 +180,7 @@ public class InteractionJankMonitor { /** * This constructor should be only public to tests. * * @param worker the worker thread for the callbacks */ @VisibleForTesting Loading @@ -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) { Loading @@ -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); } } } Loading @@ -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 */ Loading @@ -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); Loading @@ -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. */ Loading @@ -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) { Loading @@ -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; } /** Loading Loading @@ -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; } Loading
core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -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(); } Loading
core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java +22 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading @@ -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")); Loading @@ -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. Loading @@ -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); Loading Loading @@ -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); Loading