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 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,20 +228,20 @@ 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() {
mBeginVsyncId = mChoreographer.getVsyncId() + 1; synchronized (mLock) {
if (DEBUG) { mBeginVsyncId = mChoreographer.getVsyncId() + 1;
Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId); if (DEBUG) {
} Log.d(TAG, "begin: " + mSession.getName() + ", begin=" + mBeginVsyncId);
if (mSurfaceControl != null) { }
postTraceStartMarker(); if (mSurfaceControl != null) {
mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl); postTraceStartMarker();
} mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
if (!mSurfaceOnly) { }
mRendererWrapper.addObserver(mObserver); if (!mSurfaceOnly) {
} mRendererWrapper.addObserver(mObserver);
if (mListener != null) { }
mListener.onCujEvents(mSession, ACTION_SESSION_BEGIN); notifyCujEvent(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,88 +264,98 @@ 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) {
mEndVsyncId = mChoreographer.getVsyncId(); 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: // We don't remove observer here,
// 1. The session begins and ends at the same vsync id. // will remove it when all the frame metrics in this duration are called back.
// 2. The session never begun. // See onFrameMetricsAvailable for the logic of removing the observer.
if (mBeginVsyncId == INVALID_ID) { // Waiting at most 10 seconds for all callbacks to finish.
cancel(REASON_CANCEL_NOT_BEGUN); mWaitForFinishTimedOut = () -> {
} else if (mEndVsyncId <= mBeginVsyncId) { Log.e(TAG, "force finish cuj because of time out:" + mSession.getName());
cancel(REASON_CANCEL_SAME_VSYNC); finish(mJankInfos.size() - 1);
} else { };
if (DEBUG) { mHandler.postDelayed(mWaitForFinishTimedOut, TimeUnit.SECONDS.toMillis(10));
Log.d(TAG, "end: " + mSession.getName() notifyCujEvent(ACTION_SESSION_END);
+ ", end=" + mEndVsyncId + ", reason=" + reason); 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. * Cancel the trace session of the CUJ.
*/ */
public synchronized void cancel(@Reasons int reason) { public boolean cancel(@Reasons int reason) {
mCancelled = true; 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. // Always remove the observers in cancel call to avoid leakage.
if (mTracingStarted) { removeObservers();
Trace.endAsyncSection(mSession.getName(), (int) mBeginVsyncId);
}
// Always remove the observers in cancel call to avoid leakage. if (DEBUG) {
removeObservers(); Log.d(TAG, "cancel: " + mSession.getName() + ", begin=" + mBeginVsyncId
+ ", end=" + mEndVsyncId + ", reason=" + reason);
}
if (DEBUG) { mSession.setReason(reason);
Log.d(TAG, "cancel: " + mSession.getName() // Notify the listener the session has been cancelled.
+ ", begin=" + mBeginVsyncId + ", end=" + mEndVsyncId + ", reason=" + reason); // We don't notify the listeners if the session never begun.
} notifyCujEvent(ACTION_SESSION_CANCEL);
return true;
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);
} }
} }
@Override private void notifyCujEvent(String action) {
public synchronized void onJankDataAvailable(SurfaceControl.JankData[] jankData) { if (mListener == null) return;
if (mCancelled) { mListener.onCujEvents(mSession, action);
return; }
}
for (SurfaceControl.JankData jankStat : jankData) { @Override
if (!isInRange(jankStat.frameVsyncId)) { public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
continue; synchronized (mLock) {
if (mCancelled) {
return;
} }
JankInfo info = findJankInfo(jankStat.frameVsyncId);
if (info != null) { for (SurfaceControl.JankData jankStat : jankData) {
info.surfaceControlCallbackFired = true; if (!isInRange(jankStat.frameVsyncId)) {
info.jankType = jankStat.jankType; continue;
} else { }
mJankInfos.put((int) jankStat.frameVsyncId, JankInfo info = findJankInfo(jankStat.frameVsyncId);
JankInfo.createFromSurfaceControlCallback( if (info != null) {
jankStat.frameVsyncId, jankStat.jankType)); 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) { private @Nullable JankInfo findJankInfo(long frameVsyncId) {
@@ -359,31 +370,34 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener
} }
@Override @Override
public synchronized void onFrameMetricsAvailable(int dropCountSinceLastInvocation) { public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
if (mCancelled) { synchronized (mLock) {
return; if (mCancelled) {
} 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;
}
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)); (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.
removeTracker(cujType); if (tracker.end(REASON_END_NORMAL)) {
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.
removeTracker(cujType); if (tracker.cancel(reason)) {
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,10 +573,8 @@ 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()));
}
} }
/** /**

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