Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ccd43f4d authored by Peter Kalauskas's avatar Peter Kalauskas Committed by Android (Google) Code Review
Browse files

Merge "New window for drawing debug overlay" into main

parents bb77d47c b879d0d0
Loading
Loading
Loading
Loading
+0 −8
Original line number Diff line number Diff line
@@ -685,14 +685,6 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai
        }
    }

    ThreadedRendererWrapper getThreadedRenderer() {
        return mRendererWrapper;
    }

    ViewRootWrapper getViewRoot() {
        return mViewRoot;
    }

    private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) {
        boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1
                && missedFramesCount >= mTraceThresholdMissedFrames;
+24 −15
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.annotation.RequiresPermission;
import android.annotation.UiThread;
import android.annotation.WorkerThread;
import android.app.ActivityThread;
import android.app.Application;
import android.content.Context;
import android.graphics.Color;
import android.os.Build;
@@ -184,10 +185,12 @@ public class InteractionJankMonitor {
    @GuardedBy("mLock")
    private final SparseArray<RunningTracker> mRunningTrackers = new SparseArray<>();
    private final Handler mWorker;
    private final Application mCurrentApplication;
    private final DisplayResolutionTracker mDisplayResolutionTracker;
    private final Object mLock = new Object();
    private @ColorInt int mDebugBgColor = Color.CYAN;
    private double mDebugYOffset = 0.1;
    @GuardedBy("mLock")
    private InteractionMonitorDebugOverlay mDebugOverlay;

    private volatile boolean mEnabled = DEFAULT_ENABLED;
@@ -216,13 +219,15 @@ public class InteractionJankMonitor {
        mWorker = worker.getThreadHandler();
        mDisplayResolutionTracker = new DisplayResolutionTracker(mWorker);

        final Context context = ActivityThread.currentApplication();
        if (context == null || context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) != PERMISSION_GRANTED) {
        mCurrentApplication = ActivityThread.currentApplication();
        if (mCurrentApplication == null || mCurrentApplication.checkCallingOrSelfPermission(
                READ_DEVICE_CONFIG) != PERMISSION_GRANTED) {
            Log.w(TAG, "Initializing without READ_DEVICE_CONFIG permission."
                    + " enabled=" + mEnabled + ", interval=" + mSamplingInterval
                    + ", missedFrameThreshold=" + mTraceThresholdMissedFrames
                    + ", frameTimeThreshold=" + mTraceThresholdFrameTimeMillis
                    + ", package=" + (context == null ? "null" : context.getPackageName()));
                    + ", package=" + (mCurrentApplication == null ? "null"
                    : mCurrentApplication.getPackageName()));
            return;
        }

@@ -234,8 +239,8 @@ public class InteractionJankMonitor {
                        new HandlerExecutor(mWorker), this::updateProperties);
            } catch (SecurityException ex) {
                Log.d(TAG, "Can't get properties: READ_DEVICE_CONFIG granted="
                        + context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
                        + ", package=" + context.getPackageName());
                        + mCurrentApplication.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
                        + ", package=" + mCurrentApplication.getPackageName());
            }
        });
    }
@@ -538,7 +543,7 @@ public class InteractionJankMonitor {

            mRunningTrackers.put(cuj, tracker);
            if (mDebugOverlay != null) {
                mDebugOverlay.onTrackerAdded(cuj, tracker);
                mDebugOverlay.onTrackerAdded(cuj);
            }

            return tracker;
@@ -573,7 +578,7 @@ public class InteractionJankMonitor {
            running.mConfig.getHandler().removeCallbacks(running.mTimeoutAction);
            mRunningTrackers.remove(cuj);
            if (mDebugOverlay != null) {
                mDebugOverlay.onTrackerRemoved(cuj, reason, mRunningTrackers);
                mDebugOverlay.onTrackerRemoved(cuj, reason);
            }
            return false;
        }
@@ -596,16 +601,20 @@ public class InteractionJankMonitor {
                        mEnabled = properties.getBoolean(property, DEFAULT_ENABLED);
                case SETTINGS_DEBUG_OVERLAY_ENABLED_KEY -> {
                    // Never allow the debug overlay to be used on user builds
                    boolean debugOverlayEnabled = Build.IS_DEBUGGABLE
                            && properties.getBoolean(property, DEFAULT_DEBUG_OVERLAY_ENABLED);
                    if (Build.IS_USER) break;
                    boolean debugOverlayEnabled = properties.getBoolean(property,
                            DEFAULT_DEBUG_OVERLAY_ENABLED);
                    synchronized (mLock) {
                        if (debugOverlayEnabled && mDebugOverlay == null) {
                            // Use the worker thread as the UI thread for the debug overlay:
                            mDebugOverlay = new InteractionMonitorDebugOverlay(
                                mLock, mDebugBgColor, mDebugYOffset);
                                    mCurrentApplication, mWorker, mDebugBgColor, mDebugYOffset);
                        } else if (!debugOverlayEnabled && mDebugOverlay != null) {
                            mDebugOverlay.dispose();
                            mDebugOverlay = null;
                        }
                    }
                }
                default -> Log.w(TAG, "Got a change event for an unknown property: "
                        + property + " => " + properties.getString(property, ""));
            }
+144 −170
Original line number Diff line number Diff line
@@ -16,24 +16,36 @@

package com.android.internal.jank;

import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Gravity.CENTER;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;

import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;

import android.annotation.AnyThread;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.UiThread;
import android.app.ActivityThread;
import android.app.Application;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RecordingCanvas;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Trace;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.WindowCallbacks;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.jank.FrameTracker.Reasons;

/**
@@ -50,69 +62,133 @@ import com.android.internal.jank.FrameTracker.Reasons;
 * <li> Grey text indicates the CUJ ended normally and is no longer running
 * <li> Red text with a strikethrough indicates the CUJ was canceled or ended abnormally
 * </ul>
 *
 * @hide
 */
class InteractionMonitorDebugOverlay implements WindowCallbacks {
class InteractionMonitorDebugOverlay {
    private static final String TAG = "InteractionMonitorDebug";
    private static final int REASON_STILL_RUNNING = -1000;
    private final Object mLock;
    // Sparse array where the key in the CUJ and the value is the session status, or null if
    // it's currently running
    @GuardedBy("mLock")
    private final Application mCurrentApplication;
    private final Handler mUiThread;
    private final DebugOverlayView mDebugOverlayView;
    private final WindowManager mWindowManager;
    private final SparseIntArray mRunningCujs = new SparseIntArray();
    private Handler mHandler = null;
    private FrameTracker.ViewRootWrapper mViewRoot = null;
    private final Paint mDebugPaint;
    private final Paint.FontMetrics mDebugFontMetrics;

    InteractionMonitorDebugOverlay(@NonNull Application currentApplication,
            @NonNull @UiThread Handler uiThread, @ColorInt int bgColor, double yOffset) {
        mCurrentApplication = currentApplication;
        mUiThread = uiThread;
        final Display display = mCurrentApplication.getSystemService(
                DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
        final Context windowContext = mCurrentApplication.createDisplayContext(
                display).createWindowContext(TYPE_SYSTEM_OVERLAY, null /* options */);
        mWindowManager = windowContext.getSystemService(WindowManager.class);

        final Rect size = mWindowManager.getCurrentWindowMetrics().getBounds();

        final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
                PixelFormat.TRANSLUCENT);
        lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
                | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;

        lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
        lp.setFitInsetsTypes(0 /* types */);
        lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;

        lp.width = size.width();
        lp.height = size.height();
        lp.gravity = CENTER;
        lp.setTitle("InteractionMonitorDebugOverlay");

        if (!mUiThread.getLooper().isCurrentThread()) {
            Log.e(TAG, "InteractionMonitorDebugOverlay must be constructed on "
                    + "InteractionJankMonitor's worker thread");
        }
        mDebugOverlayView = new DebugOverlayView(mCurrentApplication, bgColor, yOffset);
        mWindowManager.addView(mDebugOverlayView, lp);
    }

    @AnyThread
    void onTrackerAdded(@Cuj.CujType int addedCuj) {
        mUiThread.post(() -> {
            String cujName = Cuj.getNameOfCuj(addedCuj);
            Log.i(TAG, cujName + " started");
            // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
            // is still running
            mRunningCujs.put(addedCuj, REASON_STILL_RUNNING);
            mDebugOverlayView.setVisibility(VISIBLE);
            mDebugOverlayView.invalidate();
        });
    }

    @AnyThread
    void onTrackerRemoved(@Cuj.CujType int removedCuj, @Reasons int reason) {
        mUiThread.post(() -> {
            mRunningCujs.put(removedCuj, reason);
            String cujName = Cuj.getNameOfCuj(removedCuj);
            Log.i(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled"));
            // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
            if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
                Log.i(TAG, "All CUJs ended");
                mRunningCujs.clear();
                mDebugOverlayView.setVisibility(INVISIBLE);
            }
            mDebugOverlayView.invalidate();
        });
    }

    @AnyThread
    void dispose() {
        mUiThread.post(() -> {
            mWindowManager.removeView(mDebugOverlayView);
        });
    }

    @UiThread
    private class DebugOverlayView extends View {
        private static final String TRACK_NAME = "InteractionJankMonitor";

        // Used to display the overlay in a different color and position for different processes.
        // Otherwise, two overlays will overlap and be difficult to read.
        private final int mBgColor;
        private final double mYOffset;

        private final float mDensity;
        private final Paint mDebugPaint;
        private final Paint.FontMetrics mDebugFontMetrics;
        private final String mPackageName;
    private static final String TRACK_NAME = "InteractionJankMonitor";

    InteractionMonitorDebugOverlay(Object lock, @ColorInt int bgColor, double yOffset) {
        mLock = lock;
        private DebugOverlayView(Context context, @ColorInt int bgColor, double yOffset) {
            super(context);
            setVisibility(INVISIBLE);
            mBgColor = bgColor;
            mYOffset = yOffset;
            final DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
            mDensity = displayMetrics.density;
            mDebugPaint = new Paint();
            mDebugPaint.setAntiAlias(false);
            mDebugFontMetrics = new Paint.FontMetrics();
        final Context context = ActivityThread.currentApplication();
        mPackageName = context == null ? "null" : context.getPackageName();
            mPackageName = mCurrentApplication.getPackageName();
        }

    @UiThread
    void dispose() {
        if (mViewRoot != null && mHandler != null) {
            mHandler.runWithScissors(() ->  mViewRoot.removeWindowCallbacks(this),
                    InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT);
            forceRedraw();
        }
        mHandler = null;
        mViewRoot = null;
        Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0);
        private int dipToPx(int dip) {
            return (int) (mDensity * dip + 0.5f);
        }

    @UiThread
    private boolean attachViewRootIfNeeded(InteractionJankMonitor.RunningTracker tracker) {
        FrameTracker.ViewRootWrapper viewRoot = tracker.mTracker.getViewRoot();
        if (mViewRoot == null && viewRoot != null) {
            // Add a trace marker so we can identify traces that were captured while the debug
            // overlay was enabled. Traces that use the debug overlay should NOT be used for
            // performance analysis.
            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0);
            mHandler = tracker.mConfig.getHandler();
            mViewRoot = viewRoot;
            mHandler.runWithScissors(() -> viewRoot.addWindowCallbacks(this),
                    InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT);
            forceRedraw();
            return true;
        }
        return false;
        private float getTextHeight(int textSize) {
            mDebugPaint.setTextSize(textSize);
            mDebugPaint.getFontMetrics(mDebugFontMetrics);
            return mDebugFontMetrics.descent - mDebugFontMetrics.ascent;
        }

    @GuardedBy("mLock")
        private float getWidthOfLongestCujName(int cujFontSize) {
            mDebugPaint.setTextSize(cujFontSize);
            float maxLength = 0;
@@ -126,134 +202,31 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks {
            return maxLength;
        }

    private float getTextHeight(int textSize) {
        mDebugPaint.setTextSize(textSize);
        mDebugPaint.getFontMetrics(mDebugFontMetrics);
        return mDebugFontMetrics.descent - mDebugFontMetrics.ascent;
    }

    private int dipToPx(int dip) {
        if (mViewRoot != null) {
            return mViewRoot.dipToPx(dip);
        } else {
            return dip;
        }
    }

    @UiThread
    private void forceRedraw() {
        if (mViewRoot != null && mHandler != null) {
            mHandler.runWithScissors(() -> {
                mViewRoot.requestInvalidateRootRenderNode();
                mViewRoot.getView().invalidate();
            }, InteractionJankMonitor.EXECUTOR_TASK_TIMEOUT);
        }
    }

    @UiThread
    void onTrackerRemoved(@Cuj.CujType int removedCuj, @Reasons int reason,
                          SparseArray<InteractionJankMonitor.RunningTracker> runningTrackers) {
        synchronized (mLock) {
            mRunningCujs.put(removedCuj, reason);
            boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
            if (isLoggable) {
                String cujName = Cuj.getNameOfCuj(removedCuj);
                Log.d(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled"));
            }
            // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
            if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
                if (isLoggable) Log.d(TAG, "All CUJs ended");
                mRunningCujs.clear();
                dispose();
            } else {
                boolean needsNewViewRoot = true;
                if (mViewRoot != null) {
                    // Check to see if this viewroot is still associated with one of the running
                    // trackers
                    for (int i = 0; i < runningTrackers.size(); i++) {
                        if (mViewRoot.equals(
                                runningTrackers.valueAt(i).mTracker.getViewRoot())) {
                            needsNewViewRoot = false;
                            break;
                        }
                    }
                }
                if (needsNewViewRoot) {
                    dispose();
                    for (int i = 0; i < runningTrackers.size(); i++) {
                        if (attachViewRootIfNeeded(runningTrackers.valueAt(i))) {
                            break;
                        }
                    }
                } else {
                    forceRedraw();
                }
            }
        }
    }

    @UiThread
    void onTrackerAdded(@Cuj.CujType int addedCuj, InteractionJankMonitor.RunningTracker tracker) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            String cujName = Cuj.getNameOfCuj(addedCuj);
            Log.d(TAG, cujName + " started");
        }
        synchronized (mLock) {
            // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
            // is still running
            mRunningCujs.put(addedCuj, REASON_STILL_RUNNING);
            attachViewRootIfNeeded(tracker);
            forceRedraw();
        }
    }

    @Override
    public void onWindowSizeIsChanging(Rect newBounds, boolean fullscreen,
                                       Rect systemInsets, Rect stableInsets) {
    }

    @Override
    public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen,
                                        Rect systemInsets, Rect stableInsets) {
    }

        @Override
    public void onWindowDragResizeEnd() {
    }
        protected void onDraw(@NonNull Canvas canvas) {
            super.onDraw(canvas);

    @Override
    public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) {
        return false;
    }

    @Override
    public void onRequestDraw(boolean reportNextDraw) {
    }
            // Add a trace marker so we can identify traces that were captured while the debug
            // overlay was enabled. Traces that use the debug overlay should NOT be used for
            // performance analysis.
            Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, TRACK_NAME, "DEBUG_OVERLAY_DRAW", 0);

    @Override
    public void onPostDraw(RecordingCanvas canvas) {
            final int padding = dipToPx(5);
        final int h = canvas.getHeight();
        final int w = canvas.getWidth();
        // Draw sysui CUjs near the bottom of the screen so they don't overlap with the shade,
        // and draw launcher CUJs near the top of the screen so they don't overlap with gestures
            final int h = getHeight();
            final int w = getWidth();
            final int dy = (int) (h * mYOffset);
            int packageNameFontSize = dipToPx(12);
            int cujFontSize = dipToPx(18);
            final float cujNameTextHeight = getTextHeight(cujFontSize);
            final float packageNameTextHeight = getTextHeight(packageNameFontSize);

        synchronized (mLock) {
            float maxLength = getWidthOfLongestCujName(cujFontSize);

            final int dx = (int) ((w - maxLength) / 2f);
            canvas.translate(dx, dy);
            // Draw background rectangle for displaying the text showing the CUJ name
            mDebugPaint.setColor(mBgColor);
            canvas.drawRect(
                    -padding * 2, // more padding on top so we can draw the package name
                    -padding,
                    padding * 2 + maxLength,
            canvas.drawRect(-padding * 2, // more padding on top so we can draw the package name
                    -padding, padding * 2 + maxLength,
                    padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(),
                    mDebugPaint);
            mDebugPaint.setTextSize(packageNameFontSize);
@@ -280,6 +253,7 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks {
                canvas.translate(0, cujNameTextHeight);
                canvas.drawText(cujName, 0, 0, mDebugPaint);
            }
            Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, TRACK_NAME, 0);
        }
    }
}