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 android.view.Display.DEFAULT_DISPLAY; 20 import static android.view.Gravity.CENTER; 21 import static android.view.View.INVISIBLE; 22 import static android.view.View.VISIBLE; 23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 24 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; 25 26 import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; 27 28 import android.annotation.AnyThread; 29 import android.annotation.ColorInt; 30 import android.annotation.NonNull; 31 import android.annotation.UiThread; 32 import android.app.Application; 33 import android.content.Context; 34 import android.graphics.Canvas; 35 import android.graphics.Color; 36 import android.graphics.Paint; 37 import android.graphics.PixelFormat; 38 import android.graphics.Rect; 39 import android.hardware.display.DisplayManager; 40 import android.os.Handler; 41 import android.os.Trace; 42 import android.util.DisplayMetrics; 43 import android.util.Log; 44 import android.view.Display; 45 import android.view.View; 46 import android.view.WindowManager; 47 48 import com.android.internal.jank.FrameTracker.Reasons; 49 50 import java.util.ArrayList; 51 52 /** 53 * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window 54 * associated with one of the CUJs being tracked. There's no guarantee which window it will 55 * draw to. Traces that use the debug overlay should not be used for performance analysis. 56 * <p> 57 * To enable the overlay, run the following: <code>adb shell device_config put 58 * interaction_jank_monitor debug_overlay_enabled true</code> 59 * <p> 60 * CUJ names will be drawn as follows: 61 * <ul> 62 * <li> Normal text indicates the CUJ is currently running 63 * <li> Grey text indicates the CUJ ended normally and is no longer running 64 * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally 65 * </ul> 66 * 67 * @hide 68 */ 69 class InteractionMonitorDebugOverlay { 70 private static final String TAG = "InteractionMonitorDebug"; 71 private static final int REASON_STILL_RUNNING = -1000; 72 private static final long HIDE_OVERLAY_DELAY = 2000L; 73 // Sparse array where the key in the CUJ and the value is the session status, or null if 74 // it's currently running 75 private final Application mCurrentApplication; 76 private final Handler mUiThread; 77 private final DebugOverlayView mDebugOverlayView; 78 private final WindowManager mWindowManager; 79 private final ArrayList<TrackerState> mRunningCujs = new ArrayList<>(); 80 InteractionMonitorDebugOverlay(@onNull Application currentApplication, @NonNull @UiThread Handler uiThread, @ColorInt int bgColor, double yOffset)81 InteractionMonitorDebugOverlay(@NonNull Application currentApplication, 82 @NonNull @UiThread Handler uiThread, @ColorInt int bgColor, double yOffset) { 83 mCurrentApplication = currentApplication; 84 mUiThread = uiThread; 85 final Display display = mCurrentApplication.getSystemService( 86 DisplayManager.class).getDisplay(DEFAULT_DISPLAY); 87 final Context windowContext = mCurrentApplication.createDisplayContext( 88 display).createWindowContext(TYPE_SYSTEM_OVERLAY, null /* options */); 89 mWindowManager = windowContext.getSystemService(WindowManager.class); 90 91 final Rect size = mWindowManager.getCurrentWindowMetrics().getBounds(); 92 93 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 94 WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY, 95 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 96 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 97 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH 98 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 99 | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE, PixelFormat.TRANSLUCENT); 100 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS 101 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; 102 103 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 104 lp.setFitInsetsTypes(0 /* types */); 105 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC; 106 107 lp.width = size.width(); 108 lp.height = size.height(); 109 lp.gravity = CENTER; 110 lp.setTitle("InteractionMonitorDebugOverlay"); 111 112 if (!mUiThread.getLooper().isCurrentThread()) { 113 Log.e(TAG, "InteractionMonitorDebugOverlay must be constructed on " 114 + "InteractionJankMonitor's worker thread"); 115 } 116 mDebugOverlayView = new DebugOverlayView(mCurrentApplication, bgColor, yOffset); 117 mWindowManager.addView(mDebugOverlayView, lp); 118 } 119 120 private final Runnable mHideOverlayRunnable = new Runnable() { 121 @Override 122 public void run() { 123 mRunningCujs.clear(); 124 mDebugOverlayView.setVisibility(INVISIBLE); 125 } 126 }; 127 128 @AnyThread onTrackerAdded(@uj.CujType int addedCuj, int cookie)129 void onTrackerAdded(@Cuj.CujType int addedCuj, int cookie) { 130 mUiThread.removeCallbacks(mHideOverlayRunnable); 131 mUiThread.post(() -> { 132 String cujName = Cuj.getNameOfCuj(addedCuj); 133 Log.i(TAG, cujName + " started (cookie=" + cookie + ")"); 134 mRunningCujs.add(new TrackerState(addedCuj, cookie)); 135 mDebugOverlayView.setVisibility(VISIBLE); 136 mDebugOverlayView.invalidate(); 137 }); 138 } 139 140 @AnyThread onTrackerRemoved(@uj.CujType int removedCuj, @Reasons int reason, int cookie)141 void onTrackerRemoved(@Cuj.CujType int removedCuj, @Reasons int reason, int cookie) { 142 mUiThread.post(() -> { 143 TrackerState foundTracker = null; 144 boolean allTrackersEnded = true; 145 for (int i = 0; i < mRunningCujs.size(); i++) { 146 TrackerState tracker = mRunningCujs.get(i); 147 if (tracker.mCuj == removedCuj && tracker.mCookie == cookie) { 148 foundTracker = tracker; 149 } else { 150 // If none of the trackers have REASON_STILL_RUNNING status, then 151 // all CUJs have ended 152 allTrackersEnded = allTrackersEnded && tracker.mState != REASON_STILL_RUNNING; 153 } 154 } 155 156 if (foundTracker != null) { 157 foundTracker.mState = reason; 158 } 159 160 String cujName = Cuj.getNameOfCuj(removedCuj); 161 Log.i(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled") 162 + " (cookie=" + cookie + ")"); 163 164 if (allTrackersEnded) { 165 Log.i(TAG, "All CUJs ended"); 166 mUiThread.postDelayed(mHideOverlayRunnable, HIDE_OVERLAY_DELAY); 167 } 168 mDebugOverlayView.invalidate(); 169 }); 170 } 171 172 @AnyThread dispose()173 void dispose() { 174 mUiThread.post(() -> { 175 mWindowManager.removeView(mDebugOverlayView); 176 }); 177 } 178 179 @AnyThread 180 private static class TrackerState { 181 final int mCookie; 182 final int mCuj; 183 int mState; 184 TrackerState(int cuj, int cookie)185 private TrackerState(int cuj, int cookie) { 186 mCuj = cuj; 187 mCookie = cookie; 188 // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ 189 // is still running 190 mState = REASON_STILL_RUNNING; 191 } 192 } 193 194 @UiThread 195 private class DebugOverlayView extends View { 196 private static final String TRACK_NAME = "InteractionJankMonitor"; 197 198 // Used to display the overlay in a different color and position for different processes. 199 // Otherwise, two overlays will overlap and be difficult to read. 200 private final int mBgColor; 201 private final double mYOffset; 202 203 private final float mDensity; 204 private final Paint mDebugPaint; 205 private final Paint.FontMetrics mDebugFontMetrics; 206 private final String mPackageNameText; 207 208 final int mPadding; 209 final int mPackageNameFontSize; 210 final int mCujFontSize; 211 final float mCujNameTextHeight; 212 final float mCujStatusWidth; 213 final float mPackageNameTextHeight; 214 final float mPackageNameWidth; 215 DebugOverlayView(Context context, @ColorInt int bgColor, double yOffset)216 private DebugOverlayView(Context context, @ColorInt int bgColor, double yOffset) { 217 super(context); 218 setVisibility(INVISIBLE); 219 mBgColor = bgColor; 220 mYOffset = yOffset; 221 final DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); 222 mDensity = displayMetrics.density; 223 mDebugPaint = new Paint(); 224 mDebugPaint.setAntiAlias(false); 225 mDebugFontMetrics = new Paint.FontMetrics(); 226 mPackageNameText = "package:" + mCurrentApplication.getPackageName(); 227 mPadding = dipToPx(5); 228 mPackageNameFontSize = dipToPx(12); 229 mCujFontSize = dipToPx(18); 230 mCujNameTextHeight = getTextHeight(mCujFontSize); 231 mCujStatusWidth = mCujNameTextHeight * 1.2f; 232 mPackageNameTextHeight = getTextHeight(mPackageNameFontSize); 233 mPackageNameWidth = getWidthOfText(mPackageNameText, mPackageNameFontSize); 234 } 235 dipToPx(int dip)236 private int dipToPx(int dip) { 237 return (int) (mDensity * dip + 0.5f); 238 } 239 getTextHeight(int textSize)240 private float getTextHeight(int textSize) { 241 mDebugPaint.setTextSize(textSize); 242 mDebugPaint.getFontMetrics(mDebugFontMetrics); 243 return mDebugFontMetrics.descent - mDebugFontMetrics.ascent; 244 } 245 getWidthOfText(String text, int fontSize)246 private float getWidthOfText(String text, int fontSize) { 247 mDebugPaint.setTextSize(fontSize); 248 return mDebugPaint.measureText(text); 249 } 250 getWidthOfLongestCujName(int cujFontSize)251 private float getWidthOfLongestCujName(int cujFontSize) { 252 mDebugPaint.setTextSize(cujFontSize); 253 float maxLength = 0; 254 for (int i = 0; i < mRunningCujs.size(); i++) { 255 String cujName = Cuj.getNameOfCuj(mRunningCujs.get(i).mCuj); 256 float textLength = mDebugPaint.measureText(cujName); 257 if (textLength > maxLength) { 258 maxLength = textLength; 259 } 260 } 261 return maxLength; 262 } 263 264 @Override onDraw(@onNull Canvas canvas)265 protected void onDraw(@NonNull Canvas canvas) { 266 super.onDraw(canvas); 267 268 // Add a trace marker so we can identify traces that were captured while the debug 269 // overlay was enabled. Traces that use the debug overlay should NOT be used for 270 // performance analysis. 271 Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0); 272 273 final int h = getHeight(); 274 final int w = getWidth(); 275 final int dy = (int) (h * mYOffset); 276 277 float maxLength = Math.max(mPackageNameWidth, getWidthOfLongestCujName(mCujFontSize)) 278 + mCujStatusWidth; 279 280 final int dx = (int) ((w - maxLength) / 2f); 281 canvas.translate(dx, dy); 282 // Draw background rectangle for displaying the text showing the CUJ name 283 mDebugPaint.setColor(mBgColor); 284 canvas.drawRect(-mPadding * 2, // more padding on top so we can draw the package name 285 -mPadding, mPadding * 2 + maxLength, mPadding * 2 + mPackageNameTextHeight 286 + mCujNameTextHeight * mRunningCujs.size(), mDebugPaint); 287 mDebugPaint.setTextSize(mPackageNameFontSize); 288 mDebugPaint.setColor(Color.BLACK); 289 mDebugPaint.setStrikeThruText(false); 290 canvas.translate(0, mPackageNameTextHeight); 291 canvas.drawText(mPackageNameText, 0, 0, mDebugPaint); 292 mDebugPaint.setTextSize(mCujFontSize); 293 // Draw text for CUJ names 294 for (int i = 0; i < mRunningCujs.size(); i++) { 295 TrackerState tracker = mRunningCujs.get(i); 296 int status = tracker.mState; 297 String statusText = switch (status) { 298 case REASON_STILL_RUNNING -> { 299 mDebugPaint.setColor(Color.BLACK); 300 mDebugPaint.setStrikeThruText(false); 301 yield "☐"; // BALLOT BOX 302 } 303 case REASON_END_NORMAL -> { 304 mDebugPaint.setColor(Color.GRAY); 305 mDebugPaint.setStrikeThruText(false); 306 yield "✅"; // WHITE HEAVY CHECK MARK 307 } 308 default -> { 309 // Cancelled, or otherwise ended for a bad reason 310 mDebugPaint.setColor(Color.RED); 311 mDebugPaint.setStrikeThruText(true); 312 yield "❌"; // CROSS MARK 313 } 314 }; 315 String cujName = Cuj.getNameOfCuj(tracker.mCuj); 316 canvas.translate(0, mCujNameTextHeight); 317 canvas.drawText(statusText, 0, 0, mDebugPaint); 318 canvas.drawText(cujName, mCujStatusWidth, 0, mDebugPaint); 319 } 320 Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0); 321 } 322 } 323 } 324