• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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