1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.jank; 18 19 import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; 20 21 import android.annotation.ColorInt; 22 import android.annotation.UiThread; 23 import android.app.ActivityThread; 24 import android.content.Context; 25 import android.graphics.Color; 26 import android.graphics.Paint; 27 import android.graphics.RecordingCanvas; 28 import android.graphics.Rect; 29 import android.os.Handler; 30 import android.os.Trace; 31 import android.util.SparseArray; 32 import android.util.SparseIntArray; 33 import android.view.WindowCallbacks; 34 35 import com.android.internal.annotations.GuardedBy; 36 import com.android.internal.jank.FrameTracker.Reasons; 37 import com.android.internal.jank.InteractionJankMonitor.CujType; 38 39 /** 40 * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window 41 * associated with one of the CUJs being tracked. There's no guarantee which window it will 42 * draw to. Traces that use the debug overlay should not be used for performance analysis. 43 * <p> 44 * To enable the overlay, run the following: <code>adb shell device_config put 45 * interaction_jank_monitor debug_overlay_enabled true</code> 46 * <p> 47 * CUJ names will be drawn as follows: 48 * <ul> 49 * <li> Normal text indicates the CUJ is currently running 50 * <li> Grey text indicates the CUJ ended normally and is no longer running 51 * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally 52 * </ul> 53 * @hide 54 */ 55 class InteractionMonitorDebugOverlay implements WindowCallbacks { 56 private static final int REASON_STILL_RUNNING = -1000; 57 private final Object mLock; 58 // Sparse array where the key in the CUJ and the value is the session status, or null if 59 // it's currently running 60 @GuardedBy("mLock") 61 private final SparseIntArray mRunningCujs = new SparseIntArray(); 62 private Handler mHandler = null; 63 private FrameTracker.ViewRootWrapper mViewRoot = null; 64 private final Paint mDebugPaint; 65 private final Paint.FontMetrics mDebugFontMetrics; 66 // Used to display the overlay in a different color and position for different processes. 67 // Otherwise, two overlays will overlap and be difficult to read. 68 private final int mBgColor; 69 private final double mYOffset; 70 private final String mPackageName; 71 private static final String TRACK_NAME = "InteractionJankMonitor"; 72 InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset)73 InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset) { 74 mLock = lock; 75 mBgColor = bgColor; 76 mYOffset = yOffset; 77 mDebugPaint = new Paint(); 78 mDebugPaint.setAntiAlias(false); 79 mDebugFontMetrics = new Paint.FontMetrics(); 80 final Context context = ActivityThread.currentApplication(); 81 mPackageName = context.getPackageName(); 82 } 83 84 @UiThread dispose()85 void dispose() { 86 if (mViewRoot != null && mHandler != null) { 87 mHandler.runWithScissors(() -> mViewRoot.removeWindowCallbacks(this), 88 InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); 89 forceRedraw(); 90 } 91 mHandler = null; 92 mViewRoot = null; 93 Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0); 94 } 95 96 @UiThread attachViewRootIfNeeded(FrameTracker tracker)97 private boolean attachViewRootIfNeeded(FrameTracker tracker) { 98 FrameTracker.ViewRootWrapper viewRoot = tracker.getViewRoot(); 99 if (mViewRoot == null && viewRoot != null) { 100 // Add a trace marker so we can identify traces that were captured while the debug 101 // overlay was enabled. Traces that use the debug overlay should NOT be used for 102 // performance analysis. 103 Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0); 104 mHandler = tracker.getHandler(); 105 mViewRoot = viewRoot; 106 mHandler.runWithScissors(() -> viewRoot.addWindowCallbacks(this), 107 InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); 108 forceRedraw(); 109 return true; 110 } 111 return false; 112 } 113 getWidthOfLongestCujName(int cujFontSize)114 private float getWidthOfLongestCujName(int cujFontSize) { 115 mDebugPaint.setTextSize(cujFontSize); 116 float maxLength = 0; 117 for (int i = 0; i < mRunningCujs.size(); i++) { 118 String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); 119 float textLength = mDebugPaint.measureText(cujName); 120 if (textLength > maxLength) { 121 maxLength = textLength; 122 } 123 } 124 return maxLength; 125 } 126 getTextHeight(int textSize)127 private float getTextHeight(int textSize) { 128 mDebugPaint.setTextSize(textSize); 129 mDebugPaint.getFontMetrics(mDebugFontMetrics); 130 return mDebugFontMetrics.descent - mDebugFontMetrics.ascent; 131 } 132 dipToPx(int dip)133 private int dipToPx(int dip) { 134 if (mViewRoot != null) { 135 return mViewRoot.dipToPx(dip); 136 } else { 137 return dip; 138 } 139 } 140 141 @UiThread forceRedraw()142 private void forceRedraw() { 143 if (mViewRoot != null && mHandler != null) { 144 mHandler.runWithScissors(() -> { 145 mViewRoot.requestInvalidateRootRenderNode(); 146 mViewRoot.getView().invalidate(); 147 }, InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); 148 } 149 } 150 151 @UiThread onTrackerRemoved(@ujType int removedCuj, @Reasons int reason, SparseArray<FrameTracker> runningTrackers)152 void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason, 153 SparseArray<FrameTracker> runningTrackers) { 154 synchronized (mLock) { 155 mRunningCujs.put(removedCuj, reason); 156 // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended 157 if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { 158 mRunningCujs.clear(); 159 dispose(); 160 } else { 161 boolean needsNewViewRoot = true; 162 if (mViewRoot != null) { 163 // Check to see if this viewroot is still associated with one of the running 164 // trackers 165 for (int i = 0; i < runningTrackers.size(); i++) { 166 if (mViewRoot.equals( 167 runningTrackers.valueAt(i).getViewRoot())) { 168 needsNewViewRoot = false; 169 break; 170 } 171 } 172 } 173 if (needsNewViewRoot) { 174 dispose(); 175 for (int i = 0; i < runningTrackers.size(); i++) { 176 if (attachViewRootIfNeeded(runningTrackers.valueAt(i))) { 177 break; 178 } 179 } 180 } else { 181 forceRedraw(); 182 } 183 } 184 } 185 } 186 187 @UiThread onTrackerAdded(@ujType int addedCuj, FrameTracker tracker)188 void onTrackerAdded(@CujType int addedCuj, FrameTracker tracker) { 189 synchronized (mLock) { 190 // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ 191 // is still running 192 mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); 193 attachViewRootIfNeeded(tracker); 194 forceRedraw(); 195 } 196 } 197 198 @Override onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets, Rect stableInsets)199 public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, 200 Rect systemInsets, Rect stableInsets) { 201 } 202 203 @Override onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets, Rect stableInsets)204 public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, 205 Rect systemInsets, Rect stableInsets) { 206 } 207 208 @Override onWindowDragResizeEnd()209 public void onWindowDragResizeEnd() { 210 } 211 212 @Override onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY)213 public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) { 214 return false; 215 } 216 217 @Override onRequestDraw(boolean reportNextDraw)218 public void onRequestDraw(boolean reportNextDraw) { 219 } 220 221 @Override onPostDraw(RecordingCanvas canvas)222 public void onPostDraw(RecordingCanvas canvas) { 223 final int padding = dipToPx(5); 224 final int h = canvas.getHeight(); 225 final int w = canvas.getWidth(); 226 // Draw sysui CUjs near the bottom of the screen so they don't overlap with the shade, 227 // and draw launcher CUJs near the top of the screen so they don't overlap with gestures 228 final int dy = (int) (h * mYOffset); 229 int packageNameFontSize = dipToPx(12); 230 int cujFontSize = dipToPx(18); 231 final float cujNameTextHeight = getTextHeight(cujFontSize); 232 final float packageNameTextHeight = getTextHeight(packageNameFontSize); 233 float maxLength = getWidthOfLongestCujName(cujFontSize); 234 235 final int dx = (int) ((w - maxLength) / 2f); 236 canvas.translate(dx, dy); 237 // Draw background rectangle for displaying the text showing the CUJ name 238 mDebugPaint.setColor(mBgColor); 239 canvas.drawRect( 240 -padding * 2, // more padding on top so we can draw the package name 241 -padding, 242 padding * 2 + maxLength, 243 padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(), 244 mDebugPaint); 245 mDebugPaint.setTextSize(packageNameFontSize); 246 mDebugPaint.setColor(Color.BLACK); 247 mDebugPaint.setStrikeThruText(false); 248 canvas.translate(0, packageNameTextHeight); 249 canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint); 250 mDebugPaint.setTextSize(cujFontSize); 251 // Draw text for CUJ names 252 for (int i = 0; i < mRunningCujs.size(); i++) { 253 int status = mRunningCujs.valueAt(i); 254 if (status == REASON_STILL_RUNNING) { 255 mDebugPaint.setColor(Color.BLACK); 256 mDebugPaint.setStrikeThruText(false); 257 } else if (status == REASON_END_NORMAL) { 258 mDebugPaint.setColor(Color.GRAY); 259 mDebugPaint.setStrikeThruText(false); 260 } else { 261 // Cancelled, or otherwise ended for a bad reason 262 mDebugPaint.setColor(Color.RED); 263 mDebugPaint.setStrikeThruText(true); 264 } 265 String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); 266 canvas.translate(0, cujNameTextHeight); 267 canvas.drawText(cujName, 0, 0, mDebugPaint); 268 } 269 } 270 } 271