diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 0a0785e16f2a6..80f540cca79b2 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; @@ -686,6 +689,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; @@ -798,6 +809,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 4b9e77e83166c..c769fb95e0186 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -97,6 +97,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; @@ -104,6 +105,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; @@ -143,6 +145,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 { @@ -159,6 +169,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. */ @@ -166,6 +177,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; @@ -343,6 +355,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; @@ -533,7 +548,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); } } @@ -699,7 +714,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; } @@ -750,7 +765,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; } @@ -758,6 +773,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)); + } } } @@ -767,9 +789,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)); + } } } @@ -782,6 +811,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); } @@ -821,6 +860,39 @@ public class InteractionJankMonitor { return interactionType - 1; } + /** + * 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 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. * 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 0000000000000..99b9f2f35fd49 --- /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. + *

+ * CUJ names will be drawn as follows: + *

+ */ +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 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 0be3bb69d1364..0dcba50df4ca7 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