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