Merge "Add configuration options to InteractionJankMonitor"

This commit is contained in:
Marcin Oczeretko
2020-11-18 23:00:53 +00:00
committed by Android (Google) Code Review
5 changed files with 133 additions and 71 deletions

View File

@@ -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 =

View File

@@ -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;
}
}

View File

@@ -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,50 +336,40 @@ 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() {
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;
}
}
/**
* Abandon current instance.
*/
@VisibleForTesting
public static void abandon() {
if (sInstance == null) return;
synchronized (InteractionJankMonitor.class) {
if (sInstance == null) return;
sInstance.destroy();
sInstance = null;
}
}
private FrameTracker getTracker(Session session) {
private FrameTracker getTracker(@CujType int cuj) {
synchronized (this) {
if (!mInitialized) return null;
return mRunningTrackers.get(session.getCuj());
return mRunningTrackers.get(cuj);
}
}
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);
}
}
@VisibleForTesting
public DeviceConfig.OnPropertiesChangedListener getPropertiesChangedListener() {
return mPropertiesChangedListener;
}
/**
* Trigger the perfetto daemon to collect and upload data.
*/
@@ -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;
}

View File

@@ -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();
}

View File

@@ -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);