Merge "Remove deadlock path between InteractionJankMonitor and FrameTracker" into sc-v2-dev

This commit is contained in:
TreeHugger Robot
2021-11-02 15:26:32 +00:00
committed by Android (Google) Code Review
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 Handler mHandler;
private final ChoreographerWrapper mChoreographer;
private final Object mLock = InteractionJankMonitor.getInstance().getLock();
@VisibleForTesting
public final boolean mSurfaceOnly;
@@ -181,7 +182,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
@Override
public void surfaceCreated(SurfaceControl.Transaction t) {
synchronized (FrameTracker.this) {
synchronized (mLock) {
if (mSurfaceControl == null) {
mSurfaceControl = mViewRoot.getSurfaceControl();
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
// frames to arrive, then force finish the session.
mHandler.postDelayed(() -> {
synchronized (FrameTracker.this) {
synchronized (mLock) {
if (DEBUG) {
Log.d(TAG, "surfaceDestroyed: " + mSession.getName()
+ ", finalized=" + mMetricsFinalized
+ ", info=" + mJankInfos.size()
+ ", vsync=" + mBeginVsyncId + "-" + mEndVsyncId);
+ ", vsync=" + mBeginVsyncId);
}
if (!mMetricsFinalized) {
end(REASON_END_SURFACE_DESTROYED);
@@ -227,20 +228,20 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
/**
* Begin a trace session of the CUJ.
*/
public synchronized void begin() {
mBeginVsyncId = mChoreographer.getVsyncId() + 1;
if (DEBUG) {
Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
}
if (mSurfaceControl != null) {
postTraceStartMarker();
mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
}
if (!mSurfaceOnly) {
mRendererWrapper.addObserver(mObserver);
}
if (mListener != null) {
mListener.onCujEvents(mSession, ACTION_SESSION_BEGIN);
public void begin() {
synchronized (mLock) {
mBeginVsyncId = mChoreographer.getVsyncId() + 1;
if (DEBUG) {
Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
}
if (mSurfaceControl != null) {
postTraceStartMarker();
mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
}
if (!mSurfaceOnly) {
mRendererWrapper.addObserver(mObserver);
}
notifyCujEvent(ACTION_SESSION_BEGIN);
}
}
@@ -250,7 +251,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
@VisibleForTesting
public void postTraceStartMarker() {
mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, () -> {
synchronized (FrameTracker.this) {
synchronized (mLock) {
if (mCancelled || mEndVsyncId != INVALID_ID) {
return;
}
@@ -263,88 +264,98 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
/**
* End the trace session of the CUJ.
*/
public synchronized void end(@Reasons int reason) {
if (mEndVsyncId != INVALID_ID) return;
mEndVsyncId = mChoreographer.getVsyncId();
public boolean end(@Reasons int reason) {
synchronized (mLock) {
if (mCancelled || mEndVsyncId != INVALID_ID) return false;
mEndVsyncId = mChoreographer.getVsyncId();
// Cancel the session if:
// 1. The session begins and ends at the same vsync id.
// 2. The session never begun.
if (mBeginVsyncId == INVALID_ID) {
return cancel(REASON_CANCEL_NOT_BEGUN);
} else if (mEndVsyncId <= mBeginVsyncId) {
return cancel(REASON_CANCEL_SAME_VSYNC);
} else {
if (DEBUG) {
Log.d(TAG, "end: " + mSession.getName()
+ ", end=" + mEndVsyncId + ", reason=" + reason);
}
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
mSession.setReason(reason);
// Cancel the session if:
// 1. The session begins and ends at the same vsync id.
// 2. The session never begun.
if (mBeginVsyncId == INVALID_ID) {
cancel(REASON_CANCEL_NOT_BEGUN);
} else if (mEndVsyncId <= mBeginVsyncId) {
cancel(REASON_CANCEL_SAME_VSYNC);
} else {
if (DEBUG) {
Log.d(TAG, "end: " + mSession.getName()
+ ", end=" + mEndVsyncId + ", reason=" + reason);
// We don't remove observer here,
// will remove it when all the frame metrics in this duration are called back.
// See onFrameMetricsAvailable for the logic of removing the observer.
// Waiting at most 10 seconds for all callbacks to finish.
mWaitForFinishTimedOut = () -> {
Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
finish(mJankInfos.size() - 1);
};
mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
notifyCujEvent(ACTION_SESSION_END);
return true;
}
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
mSession.setReason(reason);
if (mListener != null) {
mListener.onCujEvents(mSession, ACTION_SESSION_END);
}
// We don't remove observer here,
// will remove it when all the frame metrics in this duration are called back.
// See onFrameMetricsAvailable for the logic of removing the observer.
// Waiting at most 10 seconds for all callbacks to finish.
mWaitForFinishTimedOut = () -> {
Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
finish(mJankInfos.size() - 1);
};
mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
}
}
/**
* Cancel the trace session of the CUJ.
*/
public synchronized void cancel(@Reasons int reason) {
mCancelled = true;
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;
// We don't need to end the trace section if it never begun.
if (mTracingStarted) {
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
}
// We don't need to end the trace section if it never begun.
if (mTracingStarted) {
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
}
// Always remove the observers in cancel call to avoid leakage.
removeObservers();
// Always remove the observers in cancel call to avoid leakage.
removeObservers();
if (DEBUG) {
Log.d(TAG, "cancel: " + mSession.getName() + ", begin=" + mBeginVsyncId
+ ", end=" + mEndVsyncId + ", reason=" + reason);
}
if (DEBUG) {
Log.d(TAG, "cancel: " + mSession.getName()
+ ", begin=" + mBeginVsyncId + ", end=" + mEndVsyncId + ", reason=" + reason);
}
mSession.setReason(reason);
// Notify the listener the session has been cancelled.
// We don't notify the listeners if the session never begun.
if (mListener != null) {
mListener.onCujEvents(mSession, ACTION_SESSION_CANCEL);
mSession.setReason(reason);
// Notify the listener the session has been cancelled.
// We don't notify the listeners if the session never begun.
notifyCujEvent(ACTION_SESSION_CANCEL);
return true;
}
}
@Override
public synchronized void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
if (mCancelled) {
return;
}
private void notifyCujEvent(String action) {
if (mListener == null) return;
mListener.onCujEvents(mSession, action);
}
for (SurfaceControl.JankData jankStat : jankData) {
if (!isInRange(jankStat.frameVsyncId)) {
continue;
@Override
public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
synchronized (mLock) {
if (mCancelled) {
return;
}
JankInfo info = findJankInfo(jankStat.frameVsyncId);
if (info != null) {
info.surfaceControlCallbackFired = true;
info.jankType = jankStat.jankType;
} else {
mJankInfos.put((int) jankStat.frameVsyncId,
JankInfo.createFromSurfaceControlCallback(
jankStat.frameVsyncId, jankStat.jankType));
for (SurfaceControl.JankData jankStat : jankData) {
if (!isInRange(jankStat.frameVsyncId)) {
continue;
}
JankInfo info = findJankInfo(jankStat.frameVsyncId);
if (info != null) {
info.surfaceControlCallbackFired = true;
info.jankType = jankStat.jankType;
} else {
mJankInfos.put((int) jankStat.frameVsyncId,
JankInfo.createFromSurfaceControlCallback(
jankStat.frameVsyncId, jankStat.jankType));
}
}
processJankInfos();
}
processJankInfos();
}
private @Nullable JankInfo findJankInfo(long frameVsyncId) {
@@ -359,31 +370,34 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
}
@Override
public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
if (mCancelled) {
return;
}
public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
synchronized (mLock) {
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.
long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
long frameVsyncId = mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
// Since this callback might come a little bit late after the end() call.
// We should keep tracking the begin / end timestamp that we can compare with
// vsync timestamp to check if the frame is in the duration of the CUJ.
long totalDurationNanos = mMetricsWrapper.getMetric(FrameMetrics.TOTAL_DURATION);
boolean isFirstFrame = mMetricsWrapper.getMetric(FrameMetrics.FIRST_DRAW_FRAME) == 1;
long frameVsyncId =
mMetricsWrapper.getTiming()[FrameMetrics.Index.FRAME_TIMELINE_VSYNC_ID];
if (!isInRange(frameVsyncId)) {
return;
if (!isInRange(frameVsyncId)) {
return;
}
JankInfo info = findJankInfo(frameVsyncId);
if (info != null) {
info.hwuiCallbackFired = true;
info.totalDurationNanos = totalDurationNanos;
info.isFirstFrame = isFirstFrame;
} else {
mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
frameVsyncId, totalDurationNanos, isFirstFrame));
}
processJankInfos();
}
JankInfo info = findJankInfo(frameVsyncId);
if (info != null) {
info.hwuiCallbackFired = true;
info.totalDurationNanos = totalDurationNanos;
info.isFirstFrame = isFirstFrame;
} else {
mJankInfos.put((int) frameVsyncId, JankInfo.createFromHwuiCallback(
frameVsyncId, totalDurationNanos, isFirstFrame));
}
processJankInfos();
}
/**
@@ -497,11 +511,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
(int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND));
// Trigger perfetto if necessary.
boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
&& missedFramesCount >= mTraceThresholdMissedFrames;
boolean overFrameTimeThreshold = !mSurfaceOnly && mTraceThresholdFrameTimeMillis != -1
&& maxFrameTimeNanos >= mTraceThresholdFrameTimeMillis * NANOS_IN_MILLISECOND;
if (overMissedFramesThreshold || overFrameTimeThreshold) {
if (shouldTriggerPerfetto(missedFramesCount, (int) maxFrameTimeNanos)) {
triggerPerfetto();
}
if (mSession.logToStatsd()) {
@@ -513,9 +523,7 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
missedSfFramesCount,
missedAppFramesCount);
if (mListener != null) {
mListener.onCujEvents(mSession, ACTION_METRICS_LOGGED);
}
notifyCujEvent(ACTION_METRICS_LOGGED);
}
if (DEBUG) {
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.
*/

View File

@@ -230,6 +230,7 @@ public class InteractionJankMonitor {
private final SparseArray<FrameTracker> mRunningTrackers;
private final SparseArray<Runnable> mTimeoutActions;
private final HandlerThread mWorker;
private final Object mLock = new Object();
private boolean mEnabled = DEFAULT_ENABLED;
private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
@@ -325,6 +326,10 @@ public class InteractionJankMonitor {
mPropertiesChangedListener);
}
Object getLock() {
return mLock;
}
/**
* Creates a {@link FrameTracker} instance.
*
@@ -344,7 +349,7 @@ public class InteractionJankMonitor {
final ChoreographerWrapper choreographer =
new ChoreographerWrapper(Choreographer.getInstance());
synchronized (this) {
synchronized (mLock) {
FrameTrackerListener eventsListener =
(s, act) -> handleCujEvents(config.getContext(), act, s);
return new FrameTracker(session, mWorker.getThreadHandler(),
@@ -372,11 +377,16 @@ public class InteractionJankMonitor {
final boolean badEnd = action.equals(ACTION_SESSION_END)
&& session.getReason() != REASON_END_NORMAL;
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;
}
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)
&& session.getReason() == REASON_CANCEL_NOT_BEGUN) {
return;
@@ -389,7 +399,7 @@ public class InteractionJankMonitor {
}
private void removeTimeout(@CujType int cujType) {
synchronized (this) {
synchronized (mLock) {
Runnable timeout = mTimeoutActions.get(cujType);
if (timeout != null) {
mWorker.getThreadHandler().removeCallbacks(timeout);
@@ -432,17 +442,9 @@ public class InteractionJankMonitor {
}
private boolean beginInternal(@NonNull Configuration conf) {
synchronized (this) {
synchronized (mLock) {
int cujType = conf.mCujType;
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;
}
if (!shouldMonitor(cujType)) return false;
FrameTracker tracker = getTracker(cujType);
// Skip subsequent calls if we already have an ongoing tracing.
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.
* @param cuj cuj type
@@ -478,14 +498,16 @@ public class InteractionJankMonitor {
* @return boolean true if the tracker is ended successfully, false otherwise.
*/
public boolean end(@CujType int cujType) {
synchronized (this) {
synchronized (mLock) {
// remove the timeout action first.
removeTimeout(cujType);
FrameTracker tracker = getTracker(cujType);
// Skip this call since we haven't started a trace yet.
if (tracker == null) return false;
tracker.end(REASON_END_NORMAL);
removeTracker(cujType);
// if the end call doesn't return true, another thread is handling end of the cuj.
if (tracker.end(REASON_END_NORMAL)) {
removeTracker(cujType);
}
return true;
}
}
@@ -499,33 +521,37 @@ public class InteractionJankMonitor {
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.
removeTimeout(cujType);
FrameTracker tracker = getTracker(cujType);
// Skip this call since we haven't started a trace yet.
if (tracker == null) return false;
tracker.cancel(reason);
removeTracker(cujType);
// if the cancel call doesn't return true, another thread is handling cancel of the cuj.
if (tracker.cancel(reason)) {
removeTracker(cujType);
}
return true;
}
}
private FrameTracker getTracker(@CujType int cuj) {
synchronized (this) {
return mRunningTrackers.get(cuj);
}
return mRunningTrackers.get(cuj);
}
private void removeTracker(@CujType int cuj) {
synchronized (this) {
mRunningTrackers.remove(cuj);
}
mRunningTrackers.remove(cuj);
}
private void updateProperties(DeviceConfig.Properties properties) {
synchronized (this) {
synchronized (mLock) {
mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
DEFAULT_SAMPLING_INTERVAL);
mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
@@ -547,10 +573,8 @@ public class InteractionJankMonitor {
*/
@VisibleForTesting
public void trigger(Session session) {
synchronized (this) {
mWorker.getThreadHandler().post(
() -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
}
mWorker.getThreadHandler().post(
() -> PerfettoTrigger.trigger(session.getPerfettoTrigger()));
}
/**

View File

@@ -16,6 +16,8 @@
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_TO_STATSD_INTERACTION_TYPE;
@@ -34,6 +36,7 @@ import static org.mockito.Mockito.when;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.provider.DeviceConfig;
import android.view.View;
import android.view.ViewAttachTestActivity;
@@ -82,36 +85,23 @@ public class InteractionJankMonitorTest {
Handler handler = spy(new Handler(mActivity.getMainLooper()));
doReturn(true).when(handler).sendMessageAtTime(any(), anyLong());
mWorker = spy(new HandlerThread("Interaction-jank-monitor-test"));
doNothing().when(mWorker).start();
mWorker = mock(HandlerThread.class);
doReturn(handler).when(mWorker).getThreadHandler();
}
@Test
public void testBeginEnd() {
// Should return false if the view is not attached.
InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
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));
InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
FrameTracker tracker = createMockedFrameTracker(null);
doReturn(tracker).when(monitor).createFrameTracker(any(), any());
doNothing().when(tracker).triggerPerfetto();
doNothing().when(tracker).postTraceStartMarker();
doNothing().when(tracker).begin();
doReturn(true).when(tracker).end(anyInt());
// 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();
assertThat(monitor.end(session.getCuj())).isTrue();
verify(tracker).end(FrameTracker.REASON_END_NORMAL);
assertThat(monitor.end(CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE)).isTrue();
verify(tracker).end(REASON_END_NORMAL);
}
@Test
@@ -140,33 +130,23 @@ public class InteractionJankMonitorTest {
}
@Test
public void testBeginCancel() {
InteractionJankMonitor monitor = spy(new InteractionJankMonitor(mWorker));
public void testBeginTimeout() {
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
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(FrameTracker.ChoreographerWrapper.class),
new FrameMetricsWrapper(),
/* traceThresholdMissedFrames= */ 1, /* traceThresholdFrameTimeMillis= */ -1,
/* FrameTrackerListener */ null, config));
InteractionJankMonitor monitor = createMockedInteractionJankMonitor();
FrameTracker tracker = createMockedFrameTracker(null);
doReturn(tracker).when(monitor).createFrameTracker(any(), any());
doNothing().when(tracker).triggerPerfetto();
doNothing().when(tracker).postTraceStartMarker();
doNothing().when(tracker).begin();
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(monitor).scheduleTimeoutAction(anyInt(), anyLong(), captor.capture());
Runnable runnable = captor.getValue();
assertThat(runnable).isNotNull();
mWorker.getThreadHandler().removeCallbacks(runnable);
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
@@ -192,4 +172,43 @@ public class InteractionJankMonitorTest {
.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;
}
}