| /* |
| * 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); |
| } |
| } |