859 lines
33 KiB
Java
859 lines
33 KiB
Java
/*
|
|
* Copyright (C) 2020 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.internal.jank;
|
|
|
|
import static android.view.SurfaceControl.JankData.DISPLAY_HAL;
|
|
import static android.view.SurfaceControl.JankData.JANK_APP_DEADLINE_MISSED;
|
|
import static android.view.SurfaceControl.JankData.JANK_NONE;
|
|
import static android.view.SurfaceControl.JankData.JANK_SURFACEFLINGER_DEADLINE_MISSED;
|
|
import static android.view.SurfaceControl.JankData.JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED;
|
|
import static android.view.SurfaceControl.JankData.PREDICTION_ERROR;
|
|
import static android.view.SurfaceControl.JankData.SURFACE_FLINGER_SCHEDULING;
|
|
|
|
import static com.android.internal.jank.DisplayRefreshRate.UNKNOWN_REFRESH_RATE;
|
|
import static com.android.internal.jank.DisplayRefreshRate.VARIABLE_REFRESH_RATE;
|
|
import static com.android.internal.jank.InteractionJankMonitor.ACTION_SESSION_CANCEL;
|
|
import static com.android.internal.jank.InteractionJankMonitor.ACTION_SESSION_END;
|
|
import static com.android.internal.jank.InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT;
|
|
|
|
import android.annotation.IntDef;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.UiThread;
|
|
import android.graphics.HardwareRendererObserver;
|
|
import android.os.Handler;
|
|
import android.os.Trace;
|
|
import android.text.TextUtils;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.view.Choreographer;
|
|
import android.view.FrameMetrics;
|
|
import android.view.SurfaceControl;
|
|
import android.view.SurfaceControl.JankData.JankType;
|
|
import android.view.ThreadedRenderer;
|
|
import android.view.View;
|
|
import android.view.ViewRootImpl;
|
|
import android.view.WindowCallbacks;
|
|
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
import com.android.internal.jank.DisplayRefreshRate.RefreshRate;
|
|
import com.android.internal.jank.InteractionJankMonitor.Configuration;
|
|
import com.android.internal.util.FrameworkStatsLog;
|
|
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/**
|
|
* A class that allows the app to get the frame metrics from HardwareRendererObserver.
|
|
* @hide
|
|
*/
|
|
public class FrameTracker extends SurfaceControl.OnJankDataListener
|
|
implements HardwareRendererObserver.OnFrameMetricsAvailableListener {
|
|
private static final String TAG = "FrameTracker";
|
|
|
|
private static final long INVALID_ID = -1;
|
|
public static final int NANOS_IN_MILLISECOND = 1_000_000;
|
|
|
|
private static final int MAX_LENGTH_EVENT_DESC = 127;
|
|
|
|
private static final int MAX_FLUSH_ATTEMPTS = 3;
|
|
private static final int FLUSH_DELAY_MILLISECOND = 60;
|
|
|
|
static final int REASON_END_UNKNOWN = -1;
|
|
static final int REASON_END_NORMAL = 0;
|
|
static final int REASON_END_SURFACE_DESTROYED = 1;
|
|
static final int REASON_CANCEL_NORMAL = 16;
|
|
static final int REASON_CANCEL_NOT_BEGUN = 17;
|
|
static final int REASON_CANCEL_SAME_VSYNC = 18;
|
|
static final int REASON_CANCEL_TIMEOUT = 19;
|
|
|
|
/** @hide */
|
|
@IntDef({
|
|
REASON_END_UNKNOWN,
|
|
REASON_END_NORMAL,
|
|
REASON_END_SURFACE_DESTROYED,
|
|
REASON_CANCEL_NORMAL,
|
|
REASON_CANCEL_NOT_BEGUN,
|
|
REASON_CANCEL_SAME_VSYNC,
|
|
REASON_CANCEL_TIMEOUT,
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface Reasons {
|
|
}
|
|
|
|
private final HardwareRendererObserver mObserver;
|
|
private final int mTraceThresholdMissedFrames;
|
|
private final int mTraceThresholdFrameTimeMillis;
|
|
private final ThreadedRendererWrapper mRendererWrapper;
|
|
private final FrameMetricsWrapper mMetricsWrapper;
|
|
private final SparseArray<JankInfo> mJankInfos = new SparseArray<>();
|
|
private final Configuration mConfig;
|
|
private final ViewRootWrapper mViewRoot;
|
|
private final SurfaceControlWrapper mSurfaceControlWrapper;
|
|
private final int mDisplayId;
|
|
private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback;
|
|
private final Handler mHandler;
|
|
private final ChoreographerWrapper mChoreographer;
|
|
private final StatsLogWrapper mStatsLog;
|
|
private final boolean mDeferMonitoring;
|
|
private final FrameTrackerListener mListener;
|
|
|
|
@VisibleForTesting
|
|
public final boolean mSurfaceOnly;
|
|
|
|
private SurfaceControl mSurfaceControl;
|
|
private long mBeginVsyncId = INVALID_ID;
|
|
private long mEndVsyncId = INVALID_ID;
|
|
private boolean mMetricsFinalized;
|
|
private boolean mCancelled = false;
|
|
private boolean mTracingStarted = false;
|
|
private Runnable mWaitForFinishTimedOut;
|
|
|
|
private static class JankInfo {
|
|
long frameVsyncId;
|
|
long totalDurationNanos;
|
|
boolean isFirstFrame;
|
|
boolean hwuiCallbackFired;
|
|
boolean surfaceControlCallbackFired;
|
|
@JankType int jankType;
|
|
@RefreshRate int refreshRate;
|
|
|
|
static JankInfo createFromHwuiCallback(long frameVsyncId, long totalDurationNanos,
|
|
boolean isFirstFrame) {
|
|
return new JankInfo(frameVsyncId, true, false, JANK_NONE, UNKNOWN_REFRESH_RATE,
|
|
totalDurationNanos, isFirstFrame);
|
|
}
|
|
|
|
static JankInfo createFromSurfaceControlCallback(long frameVsyncId,
|
|
@JankType int jankType, @RefreshRate int refreshRate) {
|
|
return new JankInfo(
|
|
frameVsyncId, false, true, jankType, refreshRate, 0, false /* isFirstFrame */);
|
|
}
|
|
|
|
private JankInfo(long frameVsyncId, boolean hwuiCallbackFired,
|
|
boolean surfaceControlCallbackFired, @JankType int jankType,
|
|
@RefreshRate int refreshRate,
|
|
long totalDurationNanos, boolean isFirstFrame) {
|
|
this.frameVsyncId = frameVsyncId;
|
|
this.hwuiCallbackFired = hwuiCallbackFired;
|
|
this.surfaceControlCallbackFired = surfaceControlCallbackFired;
|
|
this.jankType = jankType;
|
|
this.refreshRate = refreshRate;
|
|
this.totalDurationNanos = totalDurationNanos;
|
|
this.isFirstFrame = isFirstFrame;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
StringBuilder str = new StringBuilder();
|
|
switch (jankType) {
|
|
case JANK_NONE:
|
|
str.append("JANK_NONE");
|
|
break;
|
|
case JANK_APP_DEADLINE_MISSED:
|
|
str.append("JANK_APP_DEADLINE_MISSED");
|
|
break;
|
|
case JANK_SURFACEFLINGER_DEADLINE_MISSED:
|
|
str.append("JANK_SURFACEFLINGER_DEADLINE_MISSED");
|
|
break;
|
|
case JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED:
|
|
str.append("JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED");
|
|
break;
|
|
case DISPLAY_HAL:
|
|
str.append("DISPLAY_HAL");
|
|
break;
|
|
case PREDICTION_ERROR:
|
|
str.append("PREDICTION_ERROR");
|
|
break;
|
|
case SURFACE_FLINGER_SCHEDULING:
|
|
str.append("SURFACE_FLINGER_SCHEDULING");
|
|
break;
|
|
default:
|
|
str.append("UNKNOWN: ").append(jankType);
|
|
break;
|
|
}
|
|
str.append(", ").append(frameVsyncId);
|
|
str.append(", ").append(totalDurationNanos);
|
|
return str.toString();
|
|
}
|
|
}
|
|
|
|
public FrameTracker(@NonNull Configuration config,
|
|
@Nullable ThreadedRendererWrapper renderer,
|
|
@Nullable ViewRootWrapper viewRootWrapper,
|
|
@NonNull SurfaceControlWrapper surfaceControlWrapper,
|
|
@NonNull ChoreographerWrapper choreographer,
|
|
@Nullable FrameMetricsWrapper metrics,
|
|
@NonNull StatsLogWrapper statsLog,
|
|
int traceThresholdMissedFrames, int traceThresholdFrameTimeMillis,
|
|
@Nullable FrameTrackerListener listener) {
|
|
mSurfaceOnly = config.isSurfaceOnly();
|
|
mConfig = config;
|
|
mHandler = config.getHandler();
|
|
mChoreographer = choreographer;
|
|
mSurfaceControlWrapper = surfaceControlWrapper;
|
|
mStatsLog = statsLog;
|
|
mDeferMonitoring = config.shouldDeferMonitor();
|
|
|
|
// HWUI instrumentation init.
|
|
mRendererWrapper = mSurfaceOnly ? null : renderer;
|
|
mMetricsWrapper = mSurfaceOnly ? null : metrics;
|
|
mViewRoot = mSurfaceOnly ? null : viewRootWrapper;
|
|
mObserver = mSurfaceOnly
|
|
? null
|
|
: new HardwareRendererObserver(this, mMetricsWrapper.getTiming(),
|
|
mHandler, /* waitForPresentTime= */ false);
|
|
|
|
mTraceThresholdMissedFrames = traceThresholdMissedFrames;
|
|
mTraceThresholdFrameTimeMillis = traceThresholdFrameTimeMillis;
|
|
mListener = listener;
|
|
mDisplayId = config.getDisplayId();
|
|
|
|
if (mSurfaceOnly) {
|
|
mSurfaceControl = config.getSurfaceControl();
|
|
mSurfaceChangedCallback = null;
|
|
} else {
|
|
// HWUI instrumentation init.
|
|
// If the surface isn't valid yet, wait until it's created.
|
|
if (mViewRoot.getSurfaceControl().isValid()) {
|
|
mSurfaceControl = mViewRoot.getSurfaceControl();
|
|
}
|
|
|
|
mSurfaceChangedCallback = new ViewRootImpl.SurfaceChangedCallback() {
|
|
@Override
|
|
public void surfaceCreated(SurfaceControl.Transaction t) {
|
|
mHandler.runWithScissors(() -> {
|
|
if (mSurfaceControl == null) {
|
|
mSurfaceControl = mViewRoot.getSurfaceControl();
|
|
if (mBeginVsyncId != INVALID_ID) {
|
|
// Previous begin invocation is not successfully, begin it again.
|
|
begin();
|
|
}
|
|
}
|
|
}, EXECUTOR_TASK_TIMEOUT);
|
|
}
|
|
|
|
@Override
|
|
public void surfaceReplaced(SurfaceControl.Transaction t) {
|
|
}
|
|
|
|
@Override
|
|
public void surfaceDestroyed() {
|
|
|
|
// Wait a while to give the system a chance for the remaining
|
|
// frames to arrive, then force finish the session.
|
|
mHandler.postDelayed(() -> {
|
|
if (!mMetricsFinalized) {
|
|
end(REASON_END_SURFACE_DESTROYED);
|
|
finish();
|
|
}
|
|
}, 50);
|
|
}
|
|
};
|
|
// This callback has a reference to FrameTracker,
|
|
// remember to remove it to avoid leakage.
|
|
mViewRoot.addSurfaceChangedCallback(mSurfaceChangedCallback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Begin a trace session of the CUJ.
|
|
*/
|
|
@UiThread
|
|
public void begin() {
|
|
final long currentVsync = mChoreographer.getVsyncId();
|
|
// In normal case, we should begin at the next frame,
|
|
// the id of the next frame is not simply increased by 1,
|
|
// but we can exclude the current frame at least.
|
|
if (mBeginVsyncId == INVALID_ID) {
|
|
mBeginVsyncId = mDeferMonitoring ? currentVsync + 1 : currentVsync;
|
|
}
|
|
if (mSurfaceControl != null) {
|
|
if (mDeferMonitoring && currentVsync < mBeginVsyncId) {
|
|
markEvent("FT#deferMonitoring", 0);
|
|
// Normal case, we begin the instrument from the very beginning,
|
|
// will exclude the first frame.
|
|
postTraceStartMarker(this::beginInternal);
|
|
} else {
|
|
// If we don't begin the instrument from the very beginning,
|
|
// there is no need to skip the frame where the begin invocation happens.
|
|
beginInternal();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start trace section at appropriate time.
|
|
*/
|
|
@VisibleForTesting
|
|
public void postTraceStartMarker(Runnable action) {
|
|
mChoreographer.mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, action, null);
|
|
}
|
|
|
|
@UiThread
|
|
private void beginInternal() {
|
|
if (mCancelled || mEndVsyncId != INVALID_ID) {
|
|
return;
|
|
}
|
|
mTracingStarted = true;
|
|
String name = mConfig.getSessionName();
|
|
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, name, name, (int) mBeginVsyncId);
|
|
markEvent("FT#beginVsync", mBeginVsyncId);
|
|
markEvent("FT#layerId", mSurfaceControl.getLayerId());
|
|
mSurfaceControlWrapper.addJankStatsListener(this, mSurfaceControl);
|
|
if (!mSurfaceOnly) {
|
|
mRendererWrapper.addObserver(mObserver);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* End the trace session of the CUJ.
|
|
*/
|
|
@UiThread
|
|
public boolean end(@Reasons int reason) {
|
|
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 {
|
|
final String name = mConfig.getSessionName();
|
|
markEvent("FT#end", reason);
|
|
markEvent("FT#endVsync", mEndVsyncId);
|
|
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, name, (int) mBeginVsyncId);
|
|
|
|
// 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 = new Runnable() {
|
|
private int mFlushAttempts = 0;
|
|
|
|
@Override
|
|
public void run() {
|
|
if (mWaitForFinishTimedOut == null || mMetricsFinalized) {
|
|
return;
|
|
}
|
|
|
|
// Send a flush jank data transaction.
|
|
if (mSurfaceControl != null && mSurfaceControl.isValid()) {
|
|
SurfaceControl.Transaction.sendSurfaceFlushJankData(mSurfaceControl);
|
|
}
|
|
|
|
long delay;
|
|
if (mFlushAttempts < MAX_FLUSH_ATTEMPTS) {
|
|
delay = FLUSH_DELAY_MILLISECOND;
|
|
mFlushAttempts++;
|
|
} else {
|
|
mWaitForFinishTimedOut = () -> {
|
|
Log.e(TAG, "force finish cuj, time out: " + name);
|
|
finish();
|
|
};
|
|
delay = TimeUnit.SECONDS.toMillis(10);
|
|
}
|
|
mHandler.postDelayed(mWaitForFinishTimedOut, delay);
|
|
}
|
|
};
|
|
mHandler.postDelayed(mWaitForFinishTimedOut, FLUSH_DELAY_MILLISECOND);
|
|
notifyCujEvent(ACTION_SESSION_END, reason);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel the trace session of the CUJ.
|
|
*/
|
|
@UiThread
|
|
public boolean cancel(@Reasons int reason) {
|
|
final boolean cancelFromEnd =
|
|
reason == REASON_CANCEL_NOT_BEGUN || reason == REASON_CANCEL_SAME_VSYNC;
|
|
if (mCancelled || (mEndVsyncId != INVALID_ID && !cancelFromEnd)) return false;
|
|
mCancelled = true;
|
|
markEvent("FT#cancel", reason);
|
|
// We don't need to end the trace section if it has never begun.
|
|
if (mTracingStarted) {
|
|
Trace.asyncTraceForTrackEnd(
|
|
Trace.TRACE_TAG_APP, mConfig.getSessionName(), (int) mBeginVsyncId);
|
|
}
|
|
|
|
// Always remove the observers in cancel call to avoid leakage.
|
|
removeObservers();
|
|
|
|
// Notify the listener the session has been cancelled.
|
|
// We don't notify the listeners if the session never begun.
|
|
notifyCujEvent(ACTION_SESSION_CANCEL, reason);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Mark the FrameTracker events in the trace.
|
|
*
|
|
* @param eventName The description of the trace event,
|
|
* @param eventValue The value of the related trace event
|
|
* Both shouldn't exceed {@link #MAX_LENGTH_EVENT_DESC}.
|
|
*/
|
|
private void markEvent(@NonNull String eventName, long eventValue) {
|
|
if (Trace.isTagEnabled(Trace.TRACE_TAG_APP)) {
|
|
String event = TextUtils.formatSimple("%s#%s", eventName, eventValue);
|
|
if (event.length() > MAX_LENGTH_EVENT_DESC) {
|
|
throw new IllegalArgumentException(TextUtils.formatSimple(
|
|
"The length of the trace event description <%s> exceeds %d",
|
|
event, MAX_LENGTH_EVENT_DESC));
|
|
}
|
|
Trace.instantForTrack(Trace.TRACE_TAG_APP, mConfig.getSessionName(), event);
|
|
}
|
|
}
|
|
|
|
private void notifyCujEvent(String action, @Reasons int reason) {
|
|
if (mListener == null) return;
|
|
mListener.onCujEvents(this, action, reason);
|
|
}
|
|
|
|
@Override
|
|
public void onJankDataAvailable(SurfaceControl.JankData[] jankData) {
|
|
postCallback(() -> {
|
|
if (mCancelled || mMetricsFinalized) {
|
|
return;
|
|
}
|
|
|
|
for (SurfaceControl.JankData jankStat : jankData) {
|
|
if (!isInRange(jankStat.frameVsyncId)) {
|
|
continue;
|
|
}
|
|
int refreshRate = DisplayRefreshRate.getRefreshRate(jankStat.frameIntervalNs);
|
|
JankInfo info = findJankInfo(jankStat.frameVsyncId);
|
|
if (info != null) {
|
|
info.surfaceControlCallbackFired = true;
|
|
info.jankType = jankStat.jankType;
|
|
info.refreshRate = refreshRate;
|
|
} else {
|
|
mJankInfos.put((int) jankStat.frameVsyncId,
|
|
JankInfo.createFromSurfaceControlCallback(
|
|
jankStat.frameVsyncId, jankStat.jankType, refreshRate));
|
|
}
|
|
}
|
|
processJankInfos();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* For easier argument capture.
|
|
*/
|
|
@VisibleForTesting
|
|
public void postCallback(Runnable callback) {
|
|
mHandler.post(callback);
|
|
}
|
|
|
|
@Nullable
|
|
private JankInfo findJankInfo(long frameVsyncId) {
|
|
return mJankInfos.get((int) frameVsyncId);
|
|
}
|
|
|
|
private boolean isInRange(long vsyncId) {
|
|
// It's possible that we may miss a callback for the frame with vsyncId == mEndVsyncId.
|
|
// Because of that, we collect all frames even if they happen after the end so we eventually
|
|
// have a frame after the end with both callbacks present.
|
|
return vsyncId >= mBeginVsyncId;
|
|
}
|
|
|
|
@Override
|
|
public void onFrameMetricsAvailable(int dropCountSinceLastInvocation) {
|
|
postCallback(() -> {
|
|
if (mCancelled || mMetricsFinalized) {
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
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();
|
|
});
|
|
}
|
|
|
|
@UiThread
|
|
private boolean hasReceivedCallbacksAfterEnd() {
|
|
if (mEndVsyncId == INVALID_ID) {
|
|
return false;
|
|
}
|
|
JankInfo last = mJankInfos.size() == 0 ? null : mJankInfos.valueAt(mJankInfos.size() - 1);
|
|
if (last == null) {
|
|
return false;
|
|
}
|
|
if (last.frameVsyncId < mEndVsyncId) {
|
|
return false;
|
|
}
|
|
for (int i = mJankInfos.size() - 1; i >= 0; i--) {
|
|
JankInfo info = mJankInfos.valueAt(i);
|
|
if (info.frameVsyncId >= mEndVsyncId) {
|
|
if (callbacksReceived(info)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@UiThread
|
|
private void processJankInfos() {
|
|
if (mMetricsFinalized) {
|
|
return;
|
|
}
|
|
if (!hasReceivedCallbacksAfterEnd()) {
|
|
return;
|
|
}
|
|
finish();
|
|
}
|
|
|
|
private boolean callbacksReceived(JankInfo info) {
|
|
return mSurfaceOnly
|
|
? info.surfaceControlCallbackFired
|
|
: info.hwuiCallbackFired && info.surfaceControlCallbackFired;
|
|
}
|
|
|
|
@UiThread
|
|
private void finish() {
|
|
if (mMetricsFinalized || mCancelled) return;
|
|
mMetricsFinalized = true;
|
|
|
|
mHandler.removeCallbacks(mWaitForFinishTimedOut);
|
|
mWaitForFinishTimedOut = null;
|
|
markEvent("FT#finish", mJankInfos.size());
|
|
|
|
// The tracing has been ended, remove the observer, see if need to trigger perfetto.
|
|
removeObservers();
|
|
|
|
final String name = mConfig.getSessionName();
|
|
|
|
int totalFramesCount = 0;
|
|
long maxFrameTimeNanos = 0;
|
|
int missedFramesCount = 0;
|
|
int missedAppFramesCount = 0;
|
|
int missedSfFramesCount = 0;
|
|
int maxSuccessiveMissedFramesCount = 0;
|
|
int successiveMissedFramesCount = 0;
|
|
@RefreshRate int refreshRate = UNKNOWN_REFRESH_RATE;
|
|
|
|
for (int i = 0; i < mJankInfos.size(); i++) {
|
|
JankInfo info = mJankInfos.valueAt(i);
|
|
final boolean isFirstDrawn = !mSurfaceOnly && info.isFirstFrame;
|
|
if (isFirstDrawn) {
|
|
continue;
|
|
}
|
|
if (info.frameVsyncId > mEndVsyncId) {
|
|
break;
|
|
}
|
|
if (info.surfaceControlCallbackFired) {
|
|
totalFramesCount++;
|
|
boolean missedFrame = false;
|
|
if ((info.jankType & JANK_APP_DEADLINE_MISSED) != 0) {
|
|
Log.w(TAG, "Missed App frame:" + info + ", CUJ=" + name);
|
|
missedAppFramesCount++;
|
|
missedFrame = true;
|
|
}
|
|
if ((info.jankType & DISPLAY_HAL) != 0
|
|
|| (info.jankType & JANK_SURFACEFLINGER_DEADLINE_MISSED) != 0
|
|
|| (info.jankType & JANK_SURFACEFLINGER_GPU_DEADLINE_MISSED) != 0
|
|
|| (info.jankType & SURFACE_FLINGER_SCHEDULING) != 0
|
|
|| (info.jankType & PREDICTION_ERROR) != 0) {
|
|
Log.w(TAG, "Missed SF frame:" + info + ", CUJ=" + name);
|
|
missedSfFramesCount++;
|
|
missedFrame = true;
|
|
}
|
|
if (missedFrame) {
|
|
missedFramesCount++;
|
|
successiveMissedFramesCount++;
|
|
} else {
|
|
maxSuccessiveMissedFramesCount = Math.max(
|
|
maxSuccessiveMissedFramesCount, successiveMissedFramesCount);
|
|
successiveMissedFramesCount = 0;
|
|
}
|
|
if (info.refreshRate != UNKNOWN_REFRESH_RATE && info.refreshRate != refreshRate) {
|
|
refreshRate = (refreshRate == UNKNOWN_REFRESH_RATE)
|
|
? info.refreshRate : VARIABLE_REFRESH_RATE;
|
|
}
|
|
// TODO (b/174755489): Early latch currently gets fired way too often, so we have
|
|
// to ignore it for now.
|
|
if (!mSurfaceOnly && !info.hwuiCallbackFired) {
|
|
markEvent("FT#MissedHWUICallback", info.frameVsyncId);
|
|
Log.w(TAG, "Missing HWUI jank callback for vsyncId: " + info.frameVsyncId
|
|
+ ", CUJ=" + name);
|
|
}
|
|
}
|
|
if (!mSurfaceOnly && info.hwuiCallbackFired) {
|
|
maxFrameTimeNanos = Math.max(info.totalDurationNanos, maxFrameTimeNanos);
|
|
if (!info.surfaceControlCallbackFired) {
|
|
markEvent("FT#MissedSFCallback", info.frameVsyncId);
|
|
Log.w(TAG, "Missing SF jank callback for vsyncId: " + info.frameVsyncId
|
|
+ ", CUJ=" + name);
|
|
}
|
|
}
|
|
}
|
|
maxSuccessiveMissedFramesCount = Math.max(
|
|
maxSuccessiveMissedFramesCount, successiveMissedFramesCount);
|
|
|
|
// Log the frame stats as counters to make them easily accessible in traces.
|
|
Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#missedFrames", missedFramesCount);
|
|
Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#missedAppFrames", missedAppFramesCount);
|
|
Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#missedSfFrames", missedSfFramesCount);
|
|
Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#totalFrames", totalFramesCount);
|
|
Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#maxFrameTimeMillis",
|
|
(int) (maxFrameTimeNanos / NANOS_IN_MILLISECOND));
|
|
Trace.traceCounter(Trace.TRACE_TAG_APP, name + "#maxSuccessiveMissedFrames",
|
|
maxSuccessiveMissedFramesCount);
|
|
|
|
// Trigger perfetto if necessary.
|
|
if (mListener != null
|
|
&& shouldTriggerPerfetto(missedFramesCount, (int) maxFrameTimeNanos)) {
|
|
mListener.triggerPerfetto(mConfig);
|
|
}
|
|
if (mConfig.logToStatsd()) {
|
|
mStatsLog.write(
|
|
FrameworkStatsLog.UI_INTERACTION_FRAME_INFO_REPORTED,
|
|
mDisplayId,
|
|
refreshRate,
|
|
mConfig.getStatsdInteractionType(),
|
|
totalFramesCount,
|
|
missedFramesCount,
|
|
maxFrameTimeNanos, /* will be 0 if mSurfaceOnly == true */
|
|
missedSfFramesCount,
|
|
missedAppFramesCount,
|
|
maxSuccessiveMissedFramesCount);
|
|
}
|
|
}
|
|
|
|
ThreadedRendererWrapper getThreadedRenderer() {
|
|
return mRendererWrapper;
|
|
}
|
|
|
|
ViewRootWrapper getViewRoot() {
|
|
return mViewRoot;
|
|
}
|
|
|
|
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.
|
|
*/
|
|
@VisibleForTesting
|
|
@UiThread
|
|
public void removeObservers() {
|
|
mSurfaceControlWrapper.removeJankStatsListener(this);
|
|
if (!mSurfaceOnly) {
|
|
// HWUI part.
|
|
mRendererWrapper.removeObserver(mObserver);
|
|
if (mSurfaceChangedCallback != null) {
|
|
mViewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A wrapper class that we can spy FrameMetrics (a final class) in unit tests.
|
|
*/
|
|
public static class FrameMetricsWrapper {
|
|
private final FrameMetrics mFrameMetrics;
|
|
|
|
public FrameMetricsWrapper() {
|
|
mFrameMetrics = new FrameMetrics();
|
|
}
|
|
|
|
/**
|
|
* Wrapper method.
|
|
* @return timing data of the metrics
|
|
*/
|
|
public long[] getTiming() {
|
|
return mFrameMetrics.mTimingData;
|
|
}
|
|
|
|
/**
|
|
* Wrapper method.
|
|
* @param index specific index of the timing data
|
|
* @return the timing data of the specified index
|
|
*/
|
|
public long getMetric(int index) {
|
|
return mFrameMetrics.getMetric(index);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A wrapper class that we can spy ThreadedRenderer (a final class) in unit tests.
|
|
*/
|
|
public static class ThreadedRendererWrapper {
|
|
private final ThreadedRenderer mRenderer;
|
|
|
|
public ThreadedRendererWrapper(ThreadedRenderer renderer) {
|
|
mRenderer = renderer;
|
|
}
|
|
|
|
/**
|
|
* Wrapper method.
|
|
* @param observer observer
|
|
*/
|
|
public void addObserver(HardwareRendererObserver observer) {
|
|
mRenderer.addObserver(observer);
|
|
}
|
|
|
|
/**
|
|
* Wrapper method.
|
|
* @param observer observer
|
|
*/
|
|
public void removeObserver(HardwareRendererObserver observer) {
|
|
mRenderer.removeObserver(observer);
|
|
}
|
|
}
|
|
|
|
public static class ViewRootWrapper {
|
|
private final ViewRootImpl mViewRoot;
|
|
|
|
public ViewRootWrapper(ViewRootImpl viewRoot) {
|
|
mViewRoot = viewRoot;
|
|
}
|
|
|
|
/**
|
|
* {@link ViewRootImpl#addSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback)}
|
|
* @param callback {@link ViewRootImpl.SurfaceChangedCallback}
|
|
*/
|
|
public void addSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback callback) {
|
|
mViewRoot.addSurfaceChangedCallback(callback);
|
|
}
|
|
|
|
/**
|
|
* {@link ViewRootImpl#removeSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback)}
|
|
* @param callback {@link ViewRootImpl.SurfaceChangedCallback}
|
|
*/
|
|
public void removeSurfaceChangedCallback(ViewRootImpl.SurfaceChangedCallback callback) {
|
|
mViewRoot.removeSurfaceChangedCallback(callback);
|
|
}
|
|
|
|
public SurfaceControl getSurfaceControl() {
|
|
return mViewRoot.getSurfaceControl();
|
|
}
|
|
|
|
void requestInvalidateRootRenderNode() {
|
|
mViewRoot.requestInvalidateRootRenderNode();
|
|
}
|
|
|
|
void addWindowCallbacks(WindowCallbacks windowCallbacks) {
|
|
mViewRoot.addWindowCallbacks(windowCallbacks);
|
|
}
|
|
|
|
void removeWindowCallbacks(WindowCallbacks windowCallbacks) {
|
|
mViewRoot.removeWindowCallbacks(windowCallbacks);
|
|
}
|
|
|
|
View getView() {
|
|
return mViewRoot.getView();
|
|
}
|
|
|
|
int dipToPx(int dip) {
|
|
final DisplayMetrics displayMetrics =
|
|
mViewRoot.mContext.getResources().getDisplayMetrics();
|
|
return (int) (displayMetrics.density * dip + 0.5f);
|
|
}
|
|
}
|
|
|
|
public static class SurfaceControlWrapper {
|
|
|
|
public void addJankStatsListener(SurfaceControl.OnJankDataListener listener,
|
|
SurfaceControl surfaceControl) {
|
|
SurfaceControl.addJankDataListener(listener, surfaceControl);
|
|
}
|
|
|
|
public void removeJankStatsListener(SurfaceControl.OnJankDataListener listener) {
|
|
SurfaceControl.removeJankDataListener(listener);
|
|
}
|
|
}
|
|
|
|
public static class ChoreographerWrapper {
|
|
|
|
private final Choreographer mChoreographer;
|
|
|
|
public ChoreographerWrapper(Choreographer choreographer) {
|
|
mChoreographer = choreographer;
|
|
}
|
|
|
|
public long getVsyncId() {
|
|
return mChoreographer.getVsyncId();
|
|
}
|
|
}
|
|
|
|
public static class StatsLogWrapper {
|
|
private final DisplayResolutionTracker mDisplayResolutionTracker;
|
|
|
|
public StatsLogWrapper(DisplayResolutionTracker displayResolutionTracker) {
|
|
mDisplayResolutionTracker = displayResolutionTracker;
|
|
}
|
|
|
|
/** {@see FrameworkStatsLog#write) */
|
|
public void write(int code, int displayId, @RefreshRate int refreshRate,
|
|
int arg1, long arg2, long arg3, long arg4, long arg5, long arg6, long arg7) {
|
|
FrameworkStatsLog.write(code, arg1, arg2, arg3, arg4, arg5, arg6, arg7,
|
|
mDisplayResolutionTracker.getResolution(displayId), refreshRate);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A listener that notifies cuj events.
|
|
*/
|
|
public interface FrameTrackerListener {
|
|
/**
|
|
* Notify that the CUJ session was created.
|
|
*
|
|
* @param tracker the tracker
|
|
* @param action the specific action
|
|
* @param reason the reason for the action
|
|
*/
|
|
void onCujEvents(FrameTracker tracker, String action, @Reasons int reason);
|
|
|
|
/**
|
|
* Notify that the Perfetto trace should be triggered.
|
|
*
|
|
* @param config the tracker configuration
|
|
*/
|
|
void triggerPerfetto(Configuration config);
|
|
}
|
|
}
|