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.Log; 32 import android.util.SparseArray; 33 import android.util.SparseIntArray; 34 import android.view.WindowCallbacks; 35 36 import com.android.internal.annotations.GuardedBy; 37 import com.android.internal.jank.FrameTracker.Reasons; 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 String TAG = "InteractionMonitorDebug"; 57 private static final int REASON_STILL_RUNNING = -1000; 58 private final Object mLock; 59 // Sparse array where the key in the CUJ and the value is the session status, or null if 60 // it's currently running 61 @GuardedBy("mLock") 62 private final SparseIntArray mRunningCujs = new SparseIntArray(); 63 private Handler mHandler = null; 64 private FrameTracker.ViewRootWrapper mViewRoot = null; 65 private final Paint mDebugPaint; 66 private final Paint.FontMetrics mDebugFontMetrics; 67 // Used to display the overlay in a different color and position for different processes. 68 // Otherwise, two overlays will overlap and be difficult to read. 69 private final int mBgColor; 70 private final double mYOffset; 71 private final String mPackageName; 72 private static final String TRACK_NAME = "InteractionJankMonitor"; 73 InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset)74 InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset) { 75 mLock = lock; 76 mBgColor = bgColor; 77 mYOffset = yOffset; 78 mDebugPaint = new Paint(); 79 mDebugPaint.setAntiAlias(false); 80 mDebugFontMetrics = new Paint.FontMetrics(); 81 final Context context = ActivityThread.currentApplication(); 82 mPackageName = context == null ? "null" : context.getPackageName(); 83 } 84 85 @UiThread dispose()86 void dispose() { 87 if (mViewRoot != null && mHandler != null) { 88 mHandler.runWithScissors(() -> mViewRoot.removeWindowCallbacks(this), 89 InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); 90 forceRedraw(); 91 } 92 mHandler = null; 93 mViewRoot = null; 94 Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0); 95 } 96 97 @UiThread attachViewRootIfNeeded(InteractionJankMonitor.RunningTracker tracker)98 private boolean attachViewRootIfNeeded(InteractionJankMonitor.RunningTracker tracker) { 99 FrameTracker.ViewRootWrapper viewRoot = tracker.mTracker.getViewRoot(); 100 if (mViewRoot == null && viewRoot != null) { 101 // Add a trace marker so we can identify traces that were captured while the debug 102 // overlay was enabled. Traces that use the debug overlay should NOT be used for 103 // performance analysis. 104 Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0); 105 mHandler = tracker.mConfig.getHandler(); 106 mViewRoot = viewRoot; 107 mHandler.runWithScissors(() -> viewRoot.addWindowCallbacks(this), 108 InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); 109 forceRedraw(); 110 return true; 111 } 112 return false; 113 } 114 115 @GuardedBy("mLock") getWidthOfLongestCujName(int cujFontSize)116 private float getWidthOfLongestCujName(int cujFontSize) { 117 mDebugPaint.setTextSize(cujFontSize); 118 float maxLength = 0; 119 for (int i = 0; i < mRunningCujs.size(); i++) { 120 String cujName = Cuj.getNameOfCuj(mRunningCujs.keyAt(i)); 121 float textLength = mDebugPaint.measureText(cujName); 122 if (textLength > maxLength) { 123 maxLength = textLength; 124 } 125 } 126 return maxLength; 127 } 128 getTextHeight(int textSize)129 private float getTextHeight(int textSize) { 130 mDebugPaint.setTextSize(textSize); 131 mDebugPaint.getFontMetrics(mDebugFontMetrics); 132 return mDebugFontMetrics.descent - mDebugFontMetrics.ascent; 133 } 134 dipToPx(int dip)135 private int dipToPx(int dip) { 136 if (mViewRoot != null) { 137 return mViewRoot.dipToPx(dip); 138 } else { 139 return dip; 140 } 141 } 142 143 @UiThread forceRedraw()144 private void forceRedraw() { 145 if (mViewRoot != null && mHandler != null) { 146 mHandler.runWithScissors(() -> { 147 mViewRoot.requestInvalidateRootRenderNode(); 148 mViewRoot.getView().invalidate(); 149 }, InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT); 150 } 151 } 152 153 @UiThread onTrackerRemoved(@uj.CujType int removedCuj, @Reasons int reason, SparseArray<InteractionJankMonitor.RunningTracker> runningTrackers)154 void onTrackerRemoved(@Cuj.CujType int removedCuj, @Reasons int reason, 155 SparseArray<InteractionJankMonitor.RunningTracker> runningTrackers) { 156 synchronized (mLock) { 157 mRunningCujs.put(removedCuj, reason); 158 boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG); 159 if (isLoggable) { 160 String cujName = Cuj.getNameOfCuj(removedCuj); 161 Log.d(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled")); 162 } 163 // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended 164 if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { 165 if (isLoggable) Log.d(TAG, "All CUJs ended"); 166 mRunningCujs.clear(); 167 dispose(); 168 } else { 169 boolean needsNewViewRoot = true; 170 if (mViewRoot != null) { 171 // Check to see if this viewroot is still associated with one of the running 172 // trackers 173 for (int i = 0; i < runningTrackers.size(); i++) { 174 if (mViewRoot.equals( 175 runningTrackers.valueAt(i).mTracker.getViewRoot())) { 176 needsNewViewRoot = false; 177 break; 178 } 179 } 180 } 181 if (needsNewViewRoot) { 182 dispose(); 183 for (int i = 0; i < runningTrackers.size(); i++) { 184 if (attachViewRootIfNeeded(runningTrackers.valueAt(i))) { 185 break; 186 } 187 } 188 } else { 189 forceRedraw(); 190 } 191 } 192 } 193 } 194 195 @UiThread onTrackerAdded(@uj.CujType int addedCuj, InteractionJankMonitor.RunningTracker tracker)196 void onTrackerAdded(@Cuj.CujType int addedCuj, InteractionJankMonitor.RunningTracker tracker) { 197 if (Log.isLoggable(TAG, Log.DEBUG)) { 198 String cujName = Cuj.getNameOfCuj(addedCuj); 199 Log.d(TAG, cujName + " started"); 200 } 201 synchronized (mLock) { 202 // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ 203 // is still running 204 mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); 205 attachViewRootIfNeeded(tracker); 206 forceRedraw(); 207 } 208 } 209 210 @Override onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, Rect systemInsets, Rect stableInsets)211 public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen, 212 Rect systemInsets, Rect stableInsets) { 213 } 214 215 @Override onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets, Rect stableInsets)216 public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, 217 Rect systemInsets, Rect stableInsets) { 218 } 219 220 @Override onWindowDragResizeEnd()221 public void onWindowDragResizeEnd() { 222 } 223 224 @Override onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY)225 public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) { 226 return false; 227 } 228 229 @Override onRequestDraw(boolean reportNextDraw)230 public void onRequestDraw(boolean reportNextDraw) { 231 } 232 233 @Override onPostDraw(RecordingCanvas canvas)234 public void onPostDraw(RecordingCanvas canvas) { 235 final int padding = dipToPx(5); 236 final int h = canvas.getHeight(); 237 final int w = canvas.getWidth(); 238 // Draw sysui CUjs near the bottom of the screen so they don't overlap with the shade, 239 // and draw launcher CUJs near the top of the screen so they don't overlap with gestures 240 final int dy = (int) (h * mYOffset); 241 int packageNameFontSize = dipToPx(12); 242 int cujFontSize = dipToPx(18); 243 final float cujNameTextHeight = getTextHeight(cujFontSize); 244 final float packageNameTextHeight = getTextHeight(packageNameFontSize); 245 246 synchronized (mLock) { 247 float maxLength = getWidthOfLongestCujName(cujFontSize); 248 249 final int dx = (int) ((w - maxLength) / 2f); 250 canvas.translate(dx, dy); 251 // Draw background rectangle for displaying the text showing the CUJ name 252 mDebugPaint.setColor(mBgColor); 253 canvas.drawRect( 254 -padding * 2, // more padding on top so we can draw the package name 255 -padding, 256 padding * 2 + maxLength, 257 padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(), 258 mDebugPaint); 259 mDebugPaint.setTextSize(packageNameFontSize); 260 mDebugPaint.setColor(Color.BLACK); 261 mDebugPaint.setStrikeThruText(false); 262 canvas.translate(0, packageNameTextHeight); 263 canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint); 264 mDebugPaint.setTextSize(cujFontSize); 265 // Draw text for CUJ names 266 for (int i = 0; i < mRunningCujs.size(); i++) { 267 int status = mRunningCujs.valueAt(i); 268 if (status == REASON_STILL_RUNNING) { 269 mDebugPaint.setColor(Color.BLACK); 270 mDebugPaint.setStrikeThruText(false); 271 } else if (status == REASON_END_NORMAL) { 272 mDebugPaint.setColor(Color.GRAY); 273 mDebugPaint.setStrikeThruText(false); 274 } else { 275 // Cancelled, or otherwise ended for a bad reason 276 mDebugPaint.setColor(Color.RED); 277 mDebugPaint.setStrikeThruText(true); 278 } 279 String cujName = Cuj.getNameOfCuj(mRunningCujs.keyAt(i)); 280 canvas.translate(0, cujNameTextHeight); 281 canvas.drawText(cujName, 0, 0, mDebugPaint); 282 } 283 } 284 } 285 } 286