diff options
4 files changed, 356 insertions, 8 deletions
diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 1c0da1846536..7fe44c7dd674 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -36,6 +36,7 @@ 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; @@ -43,7 +44,9 @@ 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.InteractionJankMonitor.Configuration; @@ -679,6 +682,14 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener } } + ThreadedRendererWrapper getThreadedRenderer() { + return mRendererWrapper; + } + + ViewRootWrapper getViewRoot() { + return mViewRoot; + } + private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) { boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1 && missedFramesCount >= mTraceThresholdMissedFrames; @@ -791,6 +802,28 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener 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 { diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 6344568480b7..2574927af360 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -96,6 +96,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION; import android.Manifest; +import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; @@ -103,6 +104,7 @@ import android.annotation.UiThread; import android.annotation.WorkerThread; import android.app.ActivityThread; import android.content.Context; +import android.graphics.Color; import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; @@ -142,6 +144,14 @@ import java.util.concurrent.TimeUnit; * adb shell device_config put interaction_jank_monitor enabled true * adb shell device_config put interaction_jank_monitor sampling_interval 1 * + * On debuggable builds, an overlay can be used to display the name of the + * currently running cuj using: + * + * adb shell device_config put interaction_jank_monitor debug_overlay_enabled true + * + * NOTE: The overlay will interfere with metrics, so it should only be used + * for understanding which UI events correspeond to which CUJs. + * * @hide */ public class InteractionJankMonitor { @@ -158,6 +168,7 @@ public class InteractionJankMonitor { "trace_threshold_missed_frames"; private static final String SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY = "trace_threshold_frame_time_millis"; + private static final String SETTINGS_DEBUG_OVERLAY_ENABLED_KEY = "debug_overlay_enabled"; /** Default to being enabled on debug builds. */ private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE; /** Default to collecting data for all CUJs. */ @@ -165,6 +176,7 @@ public class InteractionJankMonitor { /** Default to triggering trace if 3 frames are missed OR a frame takes at least 64ms */ private static final int DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES = 3; private static final int DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS = 64; + private static final boolean DEFAULT_DEBUG_OVERLAY_ENABLED = false; @VisibleForTesting public static final int MAX_LENGTH_OF_CUJ_NAME = 80; @@ -340,6 +352,9 @@ public class InteractionJankMonitor { private final HandlerThread mWorker; private final DisplayResolutionTracker mDisplayResolutionTracker; private final Object mLock = new Object(); + private @ColorInt int mDebugBgColor = Color.CYAN; + private double mDebugYOffset = 0.1; + private InteractionMonitorDebugOverlay mDebugOverlay; private volatile boolean mEnabled = DEFAULT_ENABLED; private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL; @@ -529,7 +544,7 @@ public class InteractionJankMonitor { if (needRemoveTasks(action, session)) { getTracker(session.getCuj()).getHandler().runWithScissors(() -> { removeTimeout(session.getCuj()); - removeTracker(session.getCuj()); + removeTracker(session.getCuj(), session.getReason()); }, EXECUTOR_TASK_TIMEOUT); } } @@ -695,7 +710,7 @@ public class InteractionJankMonitor { if (tracker == null) return false; // if the end call doesn't return true, another thread is handling end of the cuj. if (tracker.end(REASON_END_NORMAL)) { - removeTracker(cujType); + removeTracker(cujType, REASON_END_NORMAL); } return true; } @@ -746,7 +761,7 @@ public class InteractionJankMonitor { if (tracker == null) return false; // if the cancel call doesn't return true, another thread is handling cancel of the cuj. if (tracker.cancel(reason)) { - removeTracker(cujType); + removeTracker(cujType, reason); } return true; } @@ -754,6 +769,13 @@ public class InteractionJankMonitor { private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) { synchronized (mLock) { mRunningTrackers.put(cuj, tracker); + if (mDebugOverlay != null) { + mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot()); + } + if (DEBUG) { + Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj) + + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers)); + } } } @@ -763,9 +785,16 @@ public class InteractionJankMonitor { } } - private void removeTracker(@CujType int cuj) { + private void removeTracker(@CujType int cuj, int reason) { synchronized (mLock) { mRunningTrackers.remove(cuj); + if (mDebugOverlay != null) { + mDebugOverlay.onTrackerRemoved(cuj, reason, mRunningTrackers); + } + if (DEBUG) { + Log.d(TAG, "Removed tracker for " + getNameOfCuj(cuj) + + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers)); + } } } @@ -778,6 +807,16 @@ public class InteractionJankMonitor { mTraceThresholdFrameTimeMillis = properties.getInt( SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY, DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS); + // Never allow the debug overlay to be used on user builds + boolean debugOverlayEnabled = Build.IS_DEBUGGABLE && properties.getBoolean( + SETTINGS_DEBUG_OVERLAY_ENABLED_KEY, + DEFAULT_DEBUG_OVERLAY_ENABLED); + if (debugOverlayEnabled && mDebugOverlay == null) { + mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset); + } else if (!debugOverlayEnabled && mDebugOverlay != null) { + mDebugOverlay.dispose(); + mDebugOverlay = null; + } // The memory visibility is powered by the volatile field, mEnabled. mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED); } @@ -818,6 +857,39 @@ public class InteractionJankMonitor { } /** + * Configures the debug overlay used for displaying interaction names on the screen while they + * occur. + * + * @param bgColor the background color of the box used to display the CUJ names + * @param yOffset number between 0 and 1 to indicate where the top of the box should be relative + * to the height of the screen + */ + public void configDebugOverlay(@ColorInt int bgColor, double yOffset) { + mDebugBgColor = bgColor; + mDebugYOffset = yOffset; + } + + /** + * A helper method for getting a string representation of all running CUJs. For example, + * "(LOCKSCREEN_TRANSITION_FROM_AOD, IME_INSETS_ANIMATION)" + */ + private static String listNamesOfCujs(SparseArray<FrameTracker> trackers) { + if (!DEBUG) { + return null; + } + StringBuilder sb = new StringBuilder(); + sb.append('('); + for (int i = 0; i < trackers.size(); i++) { + sb.append(getNameOfCuj(trackers.keyAt(i))); + if (i < trackers.size() - 1) { + sb.append(", "); + } + } + sb.append(')'); + return sb.toString(); + } + + /** * A helper method to translate CUJ type to CUJ name. * * @param cujType the cuj type defined in this file diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java new file mode 100644 index 000000000000..99b9f2f35fd4 --- /dev/null +++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2023 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 com.android.internal.jank.FrameTracker.REASON_END_NORMAL; + +import android.annotation.ColorInt; +import android.app.ActivityThread; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RecordingCanvas; +import android.graphics.Rect; +import android.os.Trace; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.view.WindowCallbacks; + +import com.android.internal.jank.FrameTracker.Reasons; +import com.android.internal.jank.InteractionJankMonitor.CujType; + +/** + * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window + * associated with one of the CUJs being tracked. There's no guarantee which window it will + * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they + * are actually running. + * <p> + * CUJ names will be drawn as follows: + * <ul> + * <li> Normal text indicates the CUJ is currently running + * <li> Grey text indicates the CUJ ended normally and is no longer running + * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally + * </ul> + */ +class InteractionMonitorDebugOverlay implements WindowCallbacks { + private static final int REASON_STILL_RUNNING = -1000; + // Sparse array where the key in the CUJ and the value is the session status, or null if + // it's currently running + private final SparseIntArray mRunningCujs = new SparseIntArray(); + private FrameTracker.ViewRootWrapper mViewRoot = null; + private final Paint mDebugPaint; + private final Paint.FontMetrics mDebugFontMetrics; + // Used to display the overlay in a different color and position for different processes. + // Otherwise, two overlays will overlap and be difficult to read. + private final int mBgColor; + private final double mYOffset; + private final String mPackageName; + + InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) { + mBgColor = bgColor; + mYOffset = yOffset; + mDebugPaint = new Paint(); + mDebugPaint.setAntiAlias(false); + mDebugFontMetrics = new Paint.FontMetrics(); + final Context context = ActivityThread.currentApplication(); + mPackageName = context.getPackageName(); + } + + void dispose() { + if (mViewRoot != null) { + mViewRoot.removeWindowCallbacks(this); + forceRedraw(); + } + mViewRoot = null; + } + + private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) { + if (mViewRoot == null && viewRoot != null) { + mViewRoot = viewRoot; + viewRoot.addWindowCallbacks(this); + forceRedraw(); + return true; + } + return false; + } + + private float getWidthOfLongestCujName(int cujFontSize) { + mDebugPaint.setTextSize(cujFontSize); + float maxLength = 0; + for (int i = 0; i < mRunningCujs.size(); i++) { + String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); + float textLength = mDebugPaint.measureText(cujName); + if (textLength > maxLength) { + maxLength = textLength; + } + } + return maxLength; + } + + private float getTextHeight(int textSize) { + mDebugPaint.setTextSize(textSize); + mDebugPaint.getFontMetrics(mDebugFontMetrics); + return mDebugFontMetrics.descent - mDebugFontMetrics.ascent; + } + + private int dipToPx(int dip) { + if (mViewRoot != null) { + return mViewRoot.dipToPx(dip); + } else { + return dip; + } + } + + private void forceRedraw() { + if (mViewRoot != null) { + mViewRoot.requestInvalidateRootRenderNode(); + mViewRoot.getView().invalidate(); + } + } + + void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason, + SparseArray<FrameTracker> runningTrackers) { + mRunningCujs.put(removedCuj, reason); + // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended + if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { + mRunningCujs.clear(); + dispose(); + } else { + boolean needsNewViewRoot = true; + if (mViewRoot != null) { + // Check to see if this viewroot is still associated with one of the running + // trackers + for (int i = 0; i < runningTrackers.size(); i++) { + if (mViewRoot.equals( + runningTrackers.valueAt(i).getViewRoot())) { + needsNewViewRoot = false; + break; + } + } + } + if (needsNewViewRoot) { + dispose(); + for (int i = 0; i < runningTrackers.size(); i++) { + if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) { + break; + } + } + } else { + forceRedraw(); + } + } + } + + void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) { + // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ + // is still running + mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); + attachViewRootIfNeeded(viewRoot); + forceRedraw(); + } + + @Override + public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, + Rect systemInsets, Rect stableInsets) { + } + + @Override + public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, + Rect systemInsets, Rect stableInsets) { + } + + @Override + public void onWindowDragResizeEnd() { + } + + @Override + public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) { + return false; + } + + @Override + public void onRequestDraw(boolean reportNextDraw) { + } + + @Override + public void onPostDraw(RecordingCanvas canvas) { + Trace.beginSection("InteractionJankMonitor#drawDebug"); + final int padding = dipToPx(5); + final int h = canvas.getHeight(); + final int w = canvas.getWidth(); + // Draw sysui CUjs near the bottom of the screen so they don't overlap with the shade, + // and draw launcher CUJs near the top of the screen so they don't overlap with gestures + final int dy = (int) (h * mYOffset); + int packageNameFontSize = dipToPx(12); + int cujFontSize = dipToPx(18); + final float cujNameTextHeight = getTextHeight(cujFontSize); + final float packageNameTextHeight = getTextHeight(packageNameFontSize); + float maxLength = getWidthOfLongestCujName(cujFontSize); + + final int dx = (int) ((w - maxLength) / 2f); + canvas.translate(dx, dy); + // Draw background rectangle for displaying the text showing the CUJ name + mDebugPaint.setColor(mBgColor); + canvas.drawRect( + -padding * 2, // more padding on top so we can draw the package name + -padding, + padding * 2 + maxLength, + padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(), + mDebugPaint); + mDebugPaint.setTextSize(packageNameFontSize); + mDebugPaint.setColor(Color.BLACK); + mDebugPaint.setStrikeThruText(false); + canvas.translate(0, packageNameTextHeight); + canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint); + mDebugPaint.setTextSize(cujFontSize); + // Draw text for CUJ names + for (int i = 0; i < mRunningCujs.size(); i++) { + int status = mRunningCujs.valueAt(i); + if (status == REASON_STILL_RUNNING) { + mDebugPaint.setColor(Color.BLACK); + mDebugPaint.setStrikeThruText(false); + } else if (status == REASON_END_NORMAL) { + mDebugPaint.setColor(Color.GRAY); + mDebugPaint.setStrikeThruText(false); + } else { + // Cancelled, or otherwise ended for a bad reason + mDebugPaint.setColor(Color.RED); + mDebugPaint.setStrikeThruText(true); + } + String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); + canvas.translate(0, cujNameTextHeight); + canvas.drawText(cujName, 0, 0, mDebugPaint); + } + Trace.endSection(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 0be3bb69d136..0dcba50df4ca 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -50,6 +50,7 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.AssetManager; import android.content.res.Resources; +import android.graphics.Color; import android.hardware.SensorManager; import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricManager; @@ -113,13 +114,13 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.TestHarness; import com.android.systemui.shared.system.PackageManagerWrapper; +import dagger.Module; +import dagger.Provides; + import java.util.Optional; import javax.inject.Singleton; -import dagger.Module; -import dagger.Provides; - /** * Provides Non-SystemUI, Framework-Owned instances to the dependency graph. */ @@ -323,7 +324,9 @@ public class FrameworkServicesModule { @Provides @Singleton static InteractionJankMonitor provideInteractionJankMonitor() { - return InteractionJankMonitor.getInstance(); + InteractionJankMonitor jankMonitor = InteractionJankMonitor.getInstance(); + jankMonitor.configDebugOverlay(Color.YELLOW, 0.75); + return jankMonitor; } @Provides |