Merge "Remove deadlock path between InteractionJankMonitor and FrameTracker" into sc-v2-dev am: 9de009c3a5

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15828030

Change-Id: I516149ab133ca36bcc915bb9cbc65191342a8a01
This commit is contained in:
TreeHugger Robot
2021-11-02 15:30:19 +00:00
committed by Automerger Merge Worker
3 changed files with 242 additions and 183 deletions

View File

@@ -99,6 +99,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback; private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback;
private final Handler mHandler; private final Handler mHandler;
private final ChoreographerWrapper mChoreographer; private final ChoreographerWrapper mChoreographer;
private final Object mLock = InteractionJankMonitor.getInstance().getLock();
@VisibleForTesting @VisibleForTesting
public final boolean mSurfaceOnly; public final boolean mSurfaceOnly;
@@ -181,7 +182,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() { mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
@Override @Override
public void surfaceCreated(SurfaceControl.Transaction t) { public void surfaceCreated(SurfaceControl.Transaction t) {
synchronized (FrameTracker.this) { synchronized (mLock) {
if (mSurfaceControl == null) { if (mSurfaceControl == null) {
mSurfaceControl = mViewRoot.getSurfaceControl(); mSurfaceControl = mViewRoot.getSurfaceControl();
if (mBeginVsyncId != INVALID_ID) { if (mBeginVsyncId != INVALID_ID) {
@@ -203,12 +204,12 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
// Wait a while to give the system a chance for the remaining // Wait a while to give the system a chance for the remaining
// frames to arrive, then force finish the session. // frames to arrive, then force finish the session.
mHandler.postDelayed(() -> { mHandler.postDelayed(() -> {
synchronized (FrameTracker.this) { synchronized (mLock) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "surfaceDestroyed: " + mSession.getName() Log.d(TAG, "surfaceDestroyed: " + mSession.getName()
+ ", finalized=" + mMetricsFinalized + ", finalized=" + mMetricsFinalized
+ ", info=" + mJankInfos.size() + ", info=" + mJankInfos.size()
+ ", vsync=" + mBeginVsyncId + "-" + mEndVsyncId); + ", vsync=" + mBeginVsyncId);
} }
if (!mMetricsFinalized) { if (!mMetricsFinalized) {
end(REASON_END_SURFACE_DESTROYED); end(REASON_END_SURFACE_DESTROYED);
@@ -227,7 +228,8 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
/** /**
* Begin a trace session of the CUJ. * Begin a trace session of the CUJ.
*/ */
public synchronized void begin() { public void begin() {
synchronized (mLock) {
mBeginVsyncId = mChoreographer.getVsyncId() + 1; mBeginVsyncId = mChoreographer.getVsyncId() + 1;
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId); Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
@@ -239,8 +241,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
if (!mSurfaceOnly) { if (!mSurfaceOnly) {
mRendererWrapper.addObserver(mObserver); mRendererWrapper.addObserver(mObserver);
} }
if (mListener != null) { notifyCujEvent(ACTION_SESSION_BEGIN);
mListener.onCujEvents(mSession, ACTION_SESSION_BEGIN);
} }
} }
@@ -250,7 +251,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
@VisibleForTesting @VisibleForTesting
public void postTraceStartMarker() { public void postTraceStartMarker() {
mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, () -> { mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, () -> {
synchronized (FrameTracker.this) { synchronized (mLock) {
if (mCancelled || mEndVsyncId != INVALID_ID) { if (mCancelled || mEndVsyncId != INVALID_ID) {
return; return;
} }
@@ -263,17 +264,17 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
/** /**
* End the trace session of the CUJ. * End the trace session of the CUJ.
*/ */
public synchronized void end(@Reasons int reason) { public boolean end(@Reasons int reason) {
if (mEndVsyncId != INVALID_ID) return; synchronized (mLock) {
if (mCancelled || mEndVsyncId != INVALID_ID) return false;
mEndVsyncId = mChoreographer.getVsyncId(); mEndVsyncId = mChoreographer.getVsyncId();
// Cancel the session if: // Cancel the session if:
// 1. The session begins and ends at the same vsync id. // 1. The session begins and ends at the same vsync id.
// 2. The session never begun. // 2. The session never begun.
if (mBeginVsyncId == INVALID_ID) { if (mBeginVsyncId == INVALID_ID) {
cancel(REASON_CANCEL_NOT_BEGUN); return cancel(REASON_CANCEL_NOT_BEGUN);
} else if (mEndVsyncId <= mBeginVsyncId) { } else if (mEndVsyncId <= mBeginVsyncId) {
cancel(REASON_CANCEL_SAME_VSYNC); return cancel(REASON_CANCEL_SAME_VSYNC);
} else { } else {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "end: " + mSession.getName() Log.d(TAG, "end: " + mSession.getName()
@@ -281,9 +282,6 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
} }
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId); Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
mSession.setReason(reason); mSession.setReason(reason);
if (mListener != null) {
mListener.onCujEvents(mSession, ACTION_SESSION_END);
}
// We don't remove observer here, // We don't remove observer here,
// will remove it when all the frame metrics in this duration are called back. // will remove it when all the frame metrics in this duration are called back.
@@ -294,15 +292,21 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
finish(mJankInfos.size() - 1); finish(mJankInfos.size() - 1);
}; };
mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10)); mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
notifyCujEvent(ACTION_SESSION_END);
return true;
}
} }
} }
/** /**
* Cancel the trace session of the CUJ. * Cancel the trace session of the CUJ.
*/ */
public synchronized void cancel(@Reasons int reason) { public boolean cancel(@Reasons int reason) {
synchronized (mLock) {
final boolean cancelFromEnd =
reason == REASON_CANCEL_NOT_BEGUN || reason == REASON_CANCEL_SAME_VSYNC;
if (mCancelled || (mEndVsyncId != INVALID_ID && !cancelFromEnd)) return false;
mCancelled = true; mCancelled = true;
// We don't need to end the trace section if it never begun. // We don't need to end the trace section if it never begun.
if (mTracingStarted) { if (mTracingStarted) {
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId); Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
@@ -312,20 +316,26 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
removeObservers(); removeObservers();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "cancel: " + mSession.getName() Log.d(TAG, "cancel: " + mSession.getName() + ", begin=" + mBeginVsyncId
+ ", begin=" + mBeginVsyncId + ", end=" + mEndVsyncId + ", reason=" + reason); + ", end=" + mEndVsyncId + ", reason=" + reason);
} }
mSession.setReason(reason); mSession.setReason(reason);
// Notify the listener the session has been cancelled. // Notify the listener the session has been cancelled.
// We don't notify the listeners if the session never begun. // We don't notify the listeners if the session never begun.
if (mListener != null) { notifyCujEvent(ACTION_SESSION_CANCEL);
mListener.onCujEvents(mSession, ACTION_SESSION_CANCEL); return true;
} }
} }
private void notifyCujEvent(String action) {
if (mListener == null) return;
mListener.onCujEvents(mSession, action);
}
@Override @Override
public synchronized void onJankDataAvailable(SurfaceControl.JankData[] jankData) { public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
synchronized (mLock) {
if (mCancelled) { if (mCancelled) {
return; return;
} }
@@ -346,6 +356,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
} }
processJankInfos(); processJankInfos();
} }
}
private @Nullable JankInfo findJankInfo(long frameVsyncId) { private @Nullable JankInfo findJankInfo(long frameVsyncId) {
return mJankInfos.get((int) frameVsyncId); return mJankInfos.get((int) frameVsyncId);
@@ -359,17 +370,19 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
} }
@Override @Override
public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) { public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
synchronized (mLock) {
if (mCancelled) { if (mCancelled) {
return; return;
} }
// Since this callback might come a little bit late after the end() call. // Since this callback might come a little bit late after the end() call.
// We should keep tracking the begin / end timestamp. // We should keep tracking the begin / end timestamp that we can compare with
// Then compare with vsync timestamp to check if the frame is in the duration of the CUJ. // vsync timestamp to check if the frame is in the duration of the CUJ.
long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION); long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1; boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
long frameVsyncId = mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID]; long frameVsyncId =
mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
if (!isInRange(frameVsyncId)) { if (!isInRange(frameVsyncId)) {
return; return;
@@ -385,6 +398,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
} }
processJankInfos(); processJankInfos();
} }
}
/** /**
* Finds the first index in {@link #mJankInfos} which happened on or after {@link #mEndVsyncId}, * Finds the first index in {@link #mJankInfos} which happened on or after {@link #mEndVsyncId},
@@ -497,11 +511,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
(int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND)); (int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND));
// Trigger perfetto if necessary. // Trigger perfetto if necessary.
boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1 if (shouldTriggerPerfetto(missedFramesCount, (int) maxFrameTimeNanos)) {
&& missedFramesCount >= mTraceThresholdMissedFrames;
boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
&& maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
if (overMissedFramesThreshold || overFrameTimeThreshold) {
triggerPerfetto(); triggerPerfetto();
} }
if (mSession.logToStatsd()) { if (mSession.logToStatsd()) {
@@ -513,9 +523,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */ maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
missedSfFramesCount, missedSfFramesCount,
missedAppFramesCount); missedAppFramesCount);
if (mListener != null) { notifyCujEvent(ACTION_METRICS_LOGGED);
mListener.onCujEvents(mSession, ACTION_METRICS_LOGGED);
}
} }
if (DEBUG) { if (DEBUG) {
Log.i(TAG, "finish: CUJ=" + mSession.getName() Log.i(TAG, "finish: CUJ=" + mSession.getName()
@@ -528,6 +536,14 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
} }
} }
private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) {
boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
&& missedFramesCount >= mTraceThresholdMissedFrames;
boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
&& maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
return overMissedFramesThreshold || overFrameTimeThreshold;
}
/** /**
* Remove all the registered listeners, observers and callbacks. * Remove all the registered listeners, observers and callbacks.
*/ */

View File

@@ -230,6 +230,7 @@ public class InteractionJankMonitor {
private final SparseArray<FrameTracker> mRunningTrackers; private final SparseArray<FrameTracker> mRunningTrackers;
private final SparseArray<Runnable> mTimeoutActions; private final SparseArray<Runnable> mTimeoutActions;
private final HandlerThread mWorker; private final HandlerThread mWorker;
private final Object mLock = new Object();
private boolean mEnabled = DEFAULT_ENABLED; private boolean mEnabled = DEFAULT_ENABLED;
private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL; private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
@@ -325,6 +326,10 @@ public class InteractionJankMonitor {
mPropertiesChangedListener); mPropertiesChangedListener);
} }
Object getLock() {
return mLock;
}
/** /**
* Creates a {@link FrameTracker} instance. * Creates a {@link FrameTracker} instance.
* *
@@ -344,7 +349,7 @@ public class InteractionJankMonitor {
final ChoreographerWrapper choreographer = final ChoreographerWrapper choreographer =
new ChoreographerWrapper(Choreographer.getInstance()); new ChoreographerWrapper(Choreographer.getInstance());
synchronized (this) { synchronized (mLock) {
FrameTrackerListener eventsListener = FrameTrackerListener eventsListener =
(s, act) -> handleCujEvents(config.getContext(), act, s); (s, act) -> handleCujEvents(config.getContext(), act, s);
return new FrameTracker(session, mWorker.getThreadHandler(), return new FrameTracker(session, mWorker.getThreadHandler(),
@@ -372,11 +377,16 @@ public class InteractionJankMonitor {
final boolean badEnd = action.equals(ACTION_SESSION_END) final boolean badEnd = action.equals(ACTION_SESSION_END)
&& session.getReason() != REASON_END_NORMAL; && session.getReason() != REASON_END_NORMAL;
final boolean badCancel = action.equals(ACTION_SESSION_CANCEL) final boolean badCancel = action.equals(ACTION_SESSION_CANCEL)
&& session.getReason() != REASON_CANCEL_NORMAL; && !(session.getReason() == REASON_CANCEL_NORMAL
|| session.getReason() == REASON_CANCEL_TIMEOUT);
return badEnd || badCancel; return badEnd || badCancel;
} }
private void notifyEvents(Context context, String action, Session session) { /**
* Notifies who may interest in some CUJ events.
*/
@VisibleForTesting
public void notifyEvents(Context context, String action, Session session) {
if (action.equals(ACTION_SESSION_CANCEL) if (action.equals(ACTION_SESSION_CANCEL)
&& session.getReason() == REASON_CANCEL_NOT_BEGUN) { && session.getReason() == REASON_CANCEL_NOT_BEGUN) {
return; return;
@@ -389,7 +399,7 @@ public class InteractionJankMonitor {
} }
private void removeTimeout(@CujType int cujType) { private void removeTimeout(@CujType int cujType) {
synchronized (this) { synchronized (mLock) {
Runnable timeout = mTimeoutActions.get(cujType); Runnable timeout = mTimeoutActions.get(cujType);
if (timeout != null) { if (timeout != null) {
mWorker.getThreadHandler().removeCallbacks(timeout); mWorker.getThreadHandler().removeCallbacks(timeout);
@@ -432,17 +442,9 @@ public class InteractionJankMonitor {
} }
private boolean beginInternal(@NonNull Configuration conf) { private boolean beginInternal(@NonNull Configuration conf) {
synchronized (this) { synchronized (mLock) {
int cujType = conf.mCujType; int cujType = conf.mCujType;
boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0; if (!shouldMonitor(cujType)) return false;
if (!mEnabled || !shouldSample) {
if (DEBUG) {
Log.d(TAG, "Skip monitoring cuj: " + getNameOfCuj(cujType)
+ ", enable=" + mEnabled + ", debuggable=" + DEFAULT_ENABLED
+ ", sample=" + shouldSample + ", interval=" + mSamplingInterval);
}
return false;
}
FrameTracker tracker = getTracker(cujType); FrameTracker tracker = getTracker(cujType);
// Skip subsequent calls if we already have an ongoing tracing. // Skip subsequent calls if we already have an ongoing tracing.
if (tracker != null) return false; if (tracker != null) return false;
@@ -459,6 +461,24 @@ public class InteractionJankMonitor {
} }
} }
/**
* Check if the monitoring is enabled and if it should be sampled.
*/
@SuppressWarnings("RandomModInteger")
@VisibleForTesting
public boolean shouldMonitor(@CujType int cujType) {
boolean shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
if (!mEnabled || !shouldSample) {
if (DEBUG) {
Log.d(TAG, "Skip monitoring cuj: " + getNameOfCuj(cujType)
+ ", enable=" + mEnabled + ", debuggable=" + DEFAULT_ENABLED
+ ", sample=" + shouldSample + ", interval=" + mSamplingInterval);
}
return false;
}
return true;
}
/** /**
* Schedules a timeout action. * Schedules a timeout action.
* @param cuj cuj type * @param cuj cuj type
@@ -478,14 +498,16 @@ public class InteractionJankMonitor {
* @return boolean true if the tracker is ended successfully, false otherwise. * @return boolean true if the tracker is ended successfully, false otherwise.
*/ */
public boolean end(@CujType int cujType) { public boolean end(@CujType int cujType) {
synchronized (this) { synchronized (mLock) {
// remove the timeout action first. // remove the timeout action first.
removeTimeout(cujType); removeTimeout(cujType);
FrameTracker tracker = getTracker(cujType); FrameTracker tracker = getTracker(cujType);
// Skip this call since we haven't started a trace yet. // Skip this call since we haven't started a trace yet.
if (tracker == null) return false; if (tracker == null) return false;
tracker.end(REASON_END_NORMAL); // if the end call doesn't return true, another thread is handling end of the cuj.
if (tracker.end(REASON_END_NORMAL)) {
removeTracker(cujType); removeTracker(cujType);
}
return true; return true;
} }
} }
@@ -499,33 +521,37 @@ public class InteractionJankMonitor {
return cancel(cujType, REASON_CANCEL_NORMAL); return cancel(cujType, REASON_CANCEL_NORMAL);
} }
boolean cancel(@CujType int cujType, @Reasons int reason) { /**
synchronized (this) { * Cancels the trace session.
*
* @return boolean true if the tracker is cancelled successfully, false otherwise.
*/
@VisibleForTesting
public boolean cancel(@CujType int cujType, @Reasons int reason) {
synchronized (mLock) {
// remove the timeout action first. // remove the timeout action first.
removeTimeout(cujType); removeTimeout(cujType);
FrameTracker tracker = getTracker(cujType); FrameTracker tracker = getTracker(cujType);
// Skip this call since we haven't started a trace yet. // Skip this call since we haven't started a trace yet.
if (tracker == null) return false; if (tracker == null) return false;
tracker.cancel(reason); // if the cancel call doesn't return true, another thread is handling cancel of the cuj.
if (tracker.cancel(reason)) {
removeTracker(cujType); removeTracker(cujType);
}
return true; return true;
} }
} }
private FrameTracker getTracker(@CujType int cuj) { private FrameTracker getTracker(@CujType int cuj) {
synchronized (this) {
return mRunningTrackers.get(cuj); return mRunningTrackers.get(cuj);
} }
}
private void removeTracker(@CujType int cuj) { private void removeTracker(@CujType int cuj) {
synchronized (this) {
mRunningTrackers.remove(cuj); mRunningTrackers.remove(cuj);
} }
}
private void updateProperties(DeviceConfig.Properties properties) { private void updateProperties(DeviceConfig.Properties properties) {
synchronized (this) { synchronized (mLock) {
mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY, mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
DEFAULT_SAMPLING_INTERVAL); DEFAULT_SAMPLING_INTERVAL);
mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED); mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
@@ -547,11 +573,9 @@ public class InteractionJankMonitor {
*/ */
@VisibleForTesting @VisibleForTesting
public void trigger(Session session) { public void trigger(Session session) {
synchronized (this) {
mWorker.getThreadHandler().post( mWorker.getThreadHandler().post(
() -> PerfettoTrigger.trigger(session.getPerfettoTrigger())); () -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
} }
}
/** /**
* A helper method to translate interaction type to CUJ name. * A helper method to translate interaction type to CUJ name.

View File

@@ -16,6 +16,8 @@
package com.android.internal.jank; package com.android.internal.jank;
import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE; import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE;
@@ -34,6 +36,7 @@ import static org.mockito.Mockito.when;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.SystemClock;
import android.provider.DeviceConfig; import android.provider.DeviceConfig;
import android.view.View; import android.view.View;
import android.view.ViewAttachTestActivity; import android.view.ViewAttachTestActivity;
@@ -82,36 +85,23 @@ public class InteractionJankMonitorTest {
Handler handler = spy(new Handler(mActivity.getMainLooper())); Handler handler = spy(new Handler(mActivity.getMainLooper()));
doReturn(true).when(handler).sendMessageAtTime(any(), anyLong()); doReturn(true).when(handler).sendMessageAtTime(any(), anyLong());
mWorker = spy(new HandlerThread("Interaction-jank-monitor-test")); mWorker = mock(HandlerThread.class);
doNothing().when(mWorker).start();
doReturn(handler).when(mWorker).getThreadHandler(); doReturn(handler).when(mWorker).getThreadHandler();
} }
@Test @Test
public void testBeginEnd() { public void testBeginEnd() {
// Should return false if the view is not attached. InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker)); FrameTracker tracker = createMockedFrameTracker(null);
verify(mWorker).start();
Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX);
Configuration config = mock(Configuration.class);
when(config.isSurfaceOnly()).thenReturn(false);
FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
new ThreadedRendererWrapper(mView.getThreadedRenderer()),
new ViewRootWrapper(mView.getViewRootImpl()),
new SurfaceControlWrapper(), mock(ChoreographerWrapper.class),
new FrameMetricsWrapper(),
/* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
/* FrameTrackerListener */ null, config));
doReturn(tracker).when(monitor).createFrameTracker(any(), any()); doReturn(tracker).when(monitor).createFrameTracker(any(), any());
doNothing().when(tracker).triggerPerfetto(); doNothing().when(tracker).begin();
doNothing().when(tracker).postTraceStartMarker(); doReturn(true).when(tracker).end(anyInt());
// Simulate a trace session and see if begin / end are invoked. // Simulate a trace session and see if begin / end are invoked.
assertThat(monitor.begin(mView, session.getCuj())).isTrue(); assertThat(monitor.begin(mView, CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
verify(tracker).begin(); verify(tracker).begin();
assertThat(monitor.end(session.getCuj())).isTrue(); assertThat(monitor.end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
verify(tracker).end(FrameTracker.REASON_END_NORMAL); verify(tracker).end(REASON_END_NORMAL);
} }
@Test @Test
@@ -140,33 +130,23 @@ public class InteractionJankMonitorTest {
} }
@Test @Test
public void testBeginCancel() { public void testBeginTimeout() {
InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
Session session = new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX); FrameTracker tracker = createMockedFrameTracker(null);
Configuration config = mock(Configuration.class);
when(config.isSurfaceOnly()).thenReturn(false);
FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
new ThreadedRendererWrapper(mView.getThreadedRenderer()),
new ViewRootWrapper(mView.getViewRootImpl()),
new SurfaceControlWrapper(), mock(FrameTracker.ChoreographerWrapper.class),
new FrameMetricsWrapper(),
/* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
/* FrameTrackerListener */ null, config));
doReturn(tracker).when(monitor).createFrameTracker(any(), any()); doReturn(tracker).when(monitor).createFrameTracker(any(), any());
doNothing().when(tracker).triggerPerfetto(); doNothing().when(tracker).begin();
doNothing().when(tracker).postTraceStartMarker(); doReturn(true).when(tracker).cancel(anyInt());
assertThat(monitor.begin(mView, session.getCuj())).isTrue(); assertThat(monitor.begin(mView, CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
verify(tracker).begin(); verify(tracker).begin();
verify(monitor).scheduleTimeoutAction(anyInt(), anyLong(), captor.capture()); verify(monitor).scheduleTimeoutAction(anyInt(), anyLong(), captor.capture());
Runnable runnable = captor.getValue(); Runnable runnable = captor.getValue();
assertThat(runnable).isNotNull(); assertThat(runnable).isNotNull();
mWorker.getThreadHandler().removeCallbacks(runnable); mWorker.getThreadHandler().removeCallbacks(runnable);
runnable.run(); runnable.run();
verify(tracker).cancel(FrameTracker.REASON_CANCEL_TIMEOUT); verify(monitor).cancel(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, REASON_CANCEL_TIMEOUT);
verify(tracker).cancel(REASON_CANCEL_TIMEOUT);
} }
@Test @Test
@@ -192,4 +172,43 @@ public class InteractionJankMonitorTest {
.isTrue(); .isTrue();
} }
} }
private InteractionJankMonitor createMockedInteractionJankMonitor() {
InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
doReturn(true).when(monitor).shouldMonitor(anyInt());
doNothing().when(monitor).notifyEvents(any(), any(), any());
return monitor;
}
private FrameTracker createMockedFrameTracker(FrameTracker.FrameTrackerListener listener) {
Session session = spy(new Session(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, CUJ_POSTFIX));
doReturn(false).when(session).logToStatsd();
ThreadedRendererWrapper threadedRenderer = mock(ThreadedRendererWrapper.class);
doNothing().when(threadedRenderer).addObserver(any());
doNothing().when(threadedRenderer).removeObserver(any());
ViewRootWrapper viewRoot = spy(new ViewRootWrapper(mView.getViewRootImpl()));
doNothing().when(viewRoot).addSurfaceChangedCallback(any());
SurfaceControlWrapper surfaceControl = mock(SurfaceControlWrapper.class);
doNothing().when(surfaceControl).addJankStatsListener(any(), any());
doNothing().when(surfaceControl).removeJankStatsListener(any());
final ChoreographerWrapper choreographer = mock(ChoreographerWrapper.class);
doReturn(SystemClock.elapsedRealtime()).when(choreographer).getVsyncId();
Configuration configuration = mock(Configuration.class);
when(configuration.isSurfaceOnly()).thenReturn(false);
FrameTracker tracker = spy(new FrameTracker(session, mWorker.getThreadHandler(),
threadedRenderer, viewRoot, surfaceControl, choreographer,
new FrameMetricsWrapper(), /* traceThresholdMissedFrames= */ 1,
/* traceThresholdFrameTimeMillis= */ -1, listener, configuration));
doNothing().when(tracker).postTraceStartMarker();
doNothing().when(tracker).triggerPerfetto();
return tracker;
}
} }