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