Loading core/java/com/android/internal/jank/FrameTracker.java +33 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import android.graphics.HardwareRendererObserver; import android.os.Handler; import android.os.Trace; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; Loading @@ -43,7 +44,9 @@ import android.view.FrameMetrics; import android.view.SurfaceControl; import android.view.SurfaceControl.JankData.JankType; import android.view.ThreadedRenderer; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowCallbacks; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor.Configuration; Loading Loading @@ -686,6 +689,14 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener } } ThreadedRendererWrapper getThreadedRenderer() { return mRendererWrapper; } ViewRootWrapper getViewRoot() { return mViewRoot; } private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) { boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1 && missedFramesCount >= mTraceThresholdMissedFrames; Loading Loading @@ -798,6 +809,28 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener public SurfaceControl getSurfaceControl() { return mViewRoot.getSurfaceControl(); } void requestInvalidateRootRenderNode() { mViewRoot.requestInvalidateRootRenderNode(); } void addWindowCallbacks(WindowCallbacks windowCallbacks) { mViewRoot.addWindowCallbacks(windowCallbacks); } void removeWindowCallbacks(WindowCallbacks windowCallbacks) { mViewRoot.removeWindowCallbacks(windowCallbacks); } View getView() { return mViewRoot.getView(); } int dipToPx(int dip) { final DisplayMetrics displayMetrics = mViewRoot.mContext.getResources().getDisplayMetrics(); return (int) (displayMetrics.density * dip + 0.5f); } } public static class SurfaceControlWrapper { Loading core/java/com/android/internal/jank/InteractionJankMonitor.java +76 −4 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION; import android.Manifest; import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; Loading @@ -104,6 +105,7 @@ import android.annotation.UiThread; import android.annotation.WorkerThread; import android.app.ActivityThread; import android.content.Context; import android.graphics.Color; import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; Loading Loading @@ -143,6 +145,14 @@ import java.util.concurrent.TimeUnit; * adb shell device_config put interaction_jank_monitor enabled true * adb shell device_config put interaction_jank_monitor sampling_interval 1 * * On debuggable builds, an overlay can be used to display the name of the * currently running cuj using: * * adb shell device_config put interaction_jank_monitor debug_overlay_enabled true * * NOTE: The overlay will interfere with metrics, so it should only be used * for understanding which UI events correspeond to which CUJs. * * @hide */ public class InteractionJankMonitor { Loading @@ -159,6 +169,7 @@ public class InteractionJankMonitor { "trace_threshold_missed_frames"; private static final String SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY = "trace_threshold_frame_time_millis"; private static final String SETTINGS_DEBUG_OVERLAY_ENABLED_KEY = "debug_overlay_enabled"; /** Default to being enabled on debug builds. */ private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE; /** Default to collecting data for all CUJs. */ Loading @@ -166,6 +177,7 @@ public class InteractionJankMonitor { /** Default to triggering trace if 3 frames are missed OR a frame takes at least 64ms */ private static final int DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES = 3; private static final int DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS = 64; private static final boolean DEFAULT_DEBUG_OVERLAY_ENABLED = false; @VisibleForTesting public static final int MAX_LENGTH_OF_CUJ_NAME = 80; Loading Loading @@ -343,6 +355,9 @@ public class InteractionJankMonitor { private final HandlerThread mWorker; private final DisplayResolutionTracker mDisplayResolutionTracker; private final Object mLock = new Object(); private @ColorInt int mDebugBgColor = Color.CYAN; private double mDebugYOffset = 0.1; private InteractionMonitorDebugOverlay mDebugOverlay; private volatile boolean mEnabled = DEFAULT_ENABLED; private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL; Loading Loading @@ -533,7 +548,7 @@ public class InteractionJankMonitor { if (needRemoveTasks(action, session)) { getTracker(session.getCuj()).getHandler().runWithScissors(() -> { removeTimeout(session.getCuj()); removeTracker(session.getCuj()); removeTracker(session.getCuj(), session.getReason()); }, EXECUTOR_TASK_TIMEOUT); } } Loading Loading @@ -699,7 +714,7 @@ public class InteractionJankMonitor { if (tracker == null) return false; // if the end call doesn't return true, another thread is handling end of the cuj. if (tracker.end(REASON_END_NORMAL)) { removeTracker(cujType); removeTracker(cujType, REASON_END_NORMAL); } return true; } Loading Loading @@ -750,7 +765,7 @@ public class InteractionJankMonitor { if (tracker == null) return false; // if the cancel call doesn't return true, another thread is handling cancel of the cuj. if (tracker.cancel(reason)) { removeTracker(cujType); removeTracker(cujType, reason); } return true; } Loading @@ -758,6 +773,13 @@ public class InteractionJankMonitor { private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) { synchronized (mLock) { mRunningTrackers.put(cuj, tracker); if (mDebugOverlay != null) { mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot()); } if (DEBUG) { Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj) + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers)); } } } Loading @@ -767,9 +789,16 @@ public class InteractionJankMonitor { } } private void removeTracker(@CujType int cuj) { private void removeTracker(@CujType int cuj, int reason) { synchronized (mLock) { mRunningTrackers.remove(cuj); if (mDebugOverlay != null) { mDebugOverlay.onTrackerRemoved(cuj, reason, mRunningTrackers); } if (DEBUG) { Log.d(TAG, "Removed tracker for " + getNameOfCuj(cuj) + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers)); } } } Loading @@ -782,6 +811,16 @@ public class InteractionJankMonitor { mTraceThresholdFrameTimeMillis = properties.getInt( SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY, DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS); // Never allow the debug overlay to be used on user builds boolean debugOverlayEnabled = Build.IS_DEBUGGABLE && properties.getBoolean( SETTINGS_DEBUG_OVERLAY_ENABLED_KEY, DEFAULT_DEBUG_OVERLAY_ENABLED); if (debugOverlayEnabled && mDebugOverlay == null) { mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset); } else if (!debugOverlayEnabled && mDebugOverlay != null) { mDebugOverlay.dispose(); mDebugOverlay = null; } // The memory visibility is powered by the volatile field, mEnabled. mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED); } Loading Loading @@ -821,6 +860,39 @@ public class InteractionJankMonitor { return interactionType - 1; } /** * Configures the debug overlay used for displaying interaction names on the screen while they * occur. * * @param bgColor the background color of the box used to display the CUJ names * @param yOffset number between 0 and 1 to indicate where the top of the box should be relative * to the height of the screen */ public void configDebugOverlay(@ColorInt int bgColor, double yOffset) { mDebugBgColor = bgColor; mDebugYOffset = yOffset; } /** * A helper method for getting a string representation of all running CUJs. For example, * "(LOCKSCREEN_TRANSITION_FROM_AOD, IME_INSETS_ANIMATION)" */ private static String listNamesOfCujs(SparseArray<FrameTracker> trackers) { if (!DEBUG) { return null; } StringBuilder sb = new StringBuilder(); sb.append('('); for (int i = 0; i < trackers.size(); i++) { sb.append(getNameOfCuj(trackers.keyAt(i))); if (i < trackers.size() - 1) { sb.append(", "); } } sb.append(')'); return sb.toString(); } /** * A helper method to translate CUJ type to CUJ name. * Loading core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java 0 → 100644 +240 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.jank; import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; import android.annotation.ColorInt; import android.app.ActivityThread; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.os.Trace; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.WindowCallbacks; import com.android.internal.jank.FrameTracker.Reasons; import com.android.internal.jank.InteractionJankMonitor.CujType; /** * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window * associated with one of the CUJs being tracked. There's no guarantee which window it will * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they * are actually running. * <p> * CUJ names will be drawn as follows: * <ul> * <li> Normal text indicates the CUJ is currently running * <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> */ class InteractionMonitorDebugOverlay implements WindowCallbacks { private static final int REASON_STILL_RUNNING = -1000; // Sparse array where the key in the CUJ and the value is the session status, or null if // it's currently running private final SparseIntArray mRunningCujs = new SparseIntArray(); private FrameTracker.ViewRootWrapper mViewRoot = null; private final Paint mDebugPaint; private final Paint.FontMetrics mDebugFontMetrics; // 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 String mPackageName; InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) { mBgColor = bgColor; mYOffset = yOffset; mDebugPaint = new Paint(); mDebugPaint.setAntiAlias(false); mDebugFontMetrics = new Paint.FontMetrics(); final Context context = ActivityThread.currentApplication(); mPackageName = context.getPackageName(); } void dispose() { if (mViewRoot != null) { mViewRoot.removeWindowCallbacks(this); forceRedraw(); } mViewRoot = null; } private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) { if (mViewRoot == null && viewRoot != null) { mViewRoot = viewRoot; viewRoot.addWindowCallbacks(this); forceRedraw(); return true; } return false; } private float getWidthOfLongestCujName(int cujFontSize) { mDebugPaint.setTextSize(cujFontSize); float maxLength = 0; for (int i = 0; i < mRunningCujs.size(); i++) { String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); float textLength = mDebugPaint.measureText(cujName); if (textLength > maxLength) { maxLength = textLength; } } 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; } } private void forceRedraw() { if (mViewRoot != null) { mViewRoot.requestInvalidateRootRenderNode(); mViewRoot.getView().invalidate(); } } void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason, SparseArray<FrameTracker> runningTrackers) { mRunningCujs.put(removedCuj, reason); // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { 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).getViewRoot())) { needsNewViewRoot = false; break; } } } if (needsNewViewRoot) { dispose(); for (int i = 0; i < runningTrackers.size(); i++) { if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) { break; } } } else { forceRedraw(); } } } void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) { // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ // is still running mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); attachViewRootIfNeeded(viewRoot); 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() { } @Override public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) { return false; } @Override public void onRequestDraw(boolean reportNextDraw) { } @Override public void onPostDraw(RecordingCanvas canvas) { Trace.beginSection("InteractionJankMonitor#drawDebug"); 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 dy = (int) (h * mYOffset); int packageNameFontSize = dipToPx(12); int cujFontSize = dipToPx(18); final float cujNameTextHeight = getTextHeight(cujFontSize); final float packageNameTextHeight = getTextHeight(packageNameFontSize); 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, padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(), mDebugPaint); mDebugPaint.setTextSize(packageNameFontSize); mDebugPaint.setColor(Color.BLACK); mDebugPaint.setStrikeThruText(false); canvas.translate(0, packageNameTextHeight); canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint); mDebugPaint.setTextSize(cujFontSize); // Draw text for CUJ names for (int i = 0; i < mRunningCujs.size(); i++) { int status = mRunningCujs.valueAt(i); if (status == REASON_STILL_RUNNING) { mDebugPaint.setColor(Color.BLACK); mDebugPaint.setStrikeThruText(false); } else if (status == REASON_END_NORMAL) { mDebugPaint.setColor(Color.GRAY); mDebugPaint.setStrikeThruText(false); } else { // Cancelled, or otherwise ended for a bad reason mDebugPaint.setColor(Color.RED); mDebugPaint.setStrikeThruText(true); } String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); canvas.translate(0, cujNameTextHeight); canvas.drawText(cujName, 0, 0, mDebugPaint); } Trace.endSection(); } } packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +7 −4 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Color; import android.hardware.SensorManager; import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricManager; Loading Loading @@ -113,13 +114,13 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.TestHarness; import com.android.systemui.shared.system.PackageManagerWrapper; import dagger.Module; import dagger.Provides; import java.util.Optional; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; /** * Provides Non-SystemUI, Framework-Owned instances to the dependency graph. */ Loading Loading @@ -323,7 +324,9 @@ public class FrameworkServicesModule { @Provides @Singleton static InteractionJankMonitor provideInteractionJankMonitor() { return InteractionJankMonitor.getInstance(); InteractionJankMonitor jankMonitor = InteractionJankMonitor.getInstance(); jankMonitor.configDebugOverlay(Color.YELLOW, 0.75); return jankMonitor; } @Provides Loading Loading
core/java/com/android/internal/jank/FrameTracker.java +33 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import android.graphics.HardwareRendererObserver; import android.os.Handler; import android.os.Trace; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.view.Choreographer; Loading @@ -43,7 +44,9 @@ import android.view.FrameMetrics; import android.view.SurfaceControl; import android.view.SurfaceControl.JankData.JankType; import android.view.ThreadedRenderer; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowCallbacks; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor.Configuration; Loading Loading @@ -686,6 +689,14 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener } } ThreadedRendererWrapper getThreadedRenderer() { return mRendererWrapper; } ViewRootWrapper getViewRoot() { return mViewRoot; } private boolean shouldTriggerPerfetto(int missedFramesCount, int maxFrameTimeNanos) { boolean overMissedFramesThreshold = mTraceThresholdMissedFrames != -1 && missedFramesCount >= mTraceThresholdMissedFrames; Loading Loading @@ -798,6 +809,28 @@ public class FrameTracker extends SurfaceControl.OnJankDataListener public SurfaceControl getSurfaceControl() { return mViewRoot.getSurfaceControl(); } void requestInvalidateRootRenderNode() { mViewRoot.requestInvalidateRootRenderNode(); } void addWindowCallbacks(WindowCallbacks windowCallbacks) { mViewRoot.addWindowCallbacks(windowCallbacks); } void removeWindowCallbacks(WindowCallbacks windowCallbacks) { mViewRoot.removeWindowCallbacks(windowCallbacks); } View getView() { return mViewRoot.getView(); } int dipToPx(int dip) { final DisplayMetrics displayMetrics = mViewRoot.mContext.getResources().getDisplayMetrics(); return (int) (displayMetrics.density * dip + 0.5f); } } public static class SurfaceControlWrapper { Loading
core/java/com/android/internal/jank/InteractionJankMonitor.java +76 −4 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__WALLPAPER_TRANSITION; import android.Manifest; import android.annotation.ColorInt; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; Loading @@ -104,6 +105,7 @@ import android.annotation.UiThread; import android.annotation.WorkerThread; import android.app.ActivityThread; import android.content.Context; import android.graphics.Color; import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; Loading Loading @@ -143,6 +145,14 @@ import java.util.concurrent.TimeUnit; * adb shell device_config put interaction_jank_monitor enabled true * adb shell device_config put interaction_jank_monitor sampling_interval 1 * * On debuggable builds, an overlay can be used to display the name of the * currently running cuj using: * * adb shell device_config put interaction_jank_monitor debug_overlay_enabled true * * NOTE: The overlay will interfere with metrics, so it should only be used * for understanding which UI events correspeond to which CUJs. * * @hide */ public class InteractionJankMonitor { Loading @@ -159,6 +169,7 @@ public class InteractionJankMonitor { "trace_threshold_missed_frames"; private static final String SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY = "trace_threshold_frame_time_millis"; private static final String SETTINGS_DEBUG_OVERLAY_ENABLED_KEY = "debug_overlay_enabled"; /** Default to being enabled on debug builds. */ private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE; /** Default to collecting data for all CUJs. */ Loading @@ -166,6 +177,7 @@ public class InteractionJankMonitor { /** Default to triggering trace if 3 frames are missed OR a frame takes at least 64ms */ private static final int DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES = 3; private static final int DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS = 64; private static final boolean DEFAULT_DEBUG_OVERLAY_ENABLED = false; @VisibleForTesting public static final int MAX_LENGTH_OF_CUJ_NAME = 80; Loading Loading @@ -343,6 +355,9 @@ public class InteractionJankMonitor { private final HandlerThread mWorker; private final DisplayResolutionTracker mDisplayResolutionTracker; private final Object mLock = new Object(); private @ColorInt int mDebugBgColor = Color.CYAN; private double mDebugYOffset = 0.1; private InteractionMonitorDebugOverlay mDebugOverlay; private volatile boolean mEnabled = DEFAULT_ENABLED; private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL; Loading Loading @@ -533,7 +548,7 @@ public class InteractionJankMonitor { if (needRemoveTasks(action, session)) { getTracker(session.getCuj()).getHandler().runWithScissors(() -> { removeTimeout(session.getCuj()); removeTracker(session.getCuj()); removeTracker(session.getCuj(), session.getReason()); }, EXECUTOR_TASK_TIMEOUT); } } Loading Loading @@ -699,7 +714,7 @@ public class InteractionJankMonitor { if (tracker == null) return false; // if the end call doesn't return true, another thread is handling end of the cuj. if (tracker.end(REASON_END_NORMAL)) { removeTracker(cujType); removeTracker(cujType, REASON_END_NORMAL); } return true; } Loading Loading @@ -750,7 +765,7 @@ public class InteractionJankMonitor { if (tracker == null) return false; // if the cancel call doesn't return true, another thread is handling cancel of the cuj. if (tracker.cancel(reason)) { removeTracker(cujType); removeTracker(cujType, reason); } return true; } Loading @@ -758,6 +773,13 @@ public class InteractionJankMonitor { private void putTracker(@CujType int cuj, @NonNull FrameTracker tracker) { synchronized (mLock) { mRunningTrackers.put(cuj, tracker); if (mDebugOverlay != null) { mDebugOverlay.onTrackerAdded(cuj, tracker.getViewRoot()); } if (DEBUG) { Log.d(TAG, "Added tracker for " + getNameOfCuj(cuj) + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers)); } } } Loading @@ -767,9 +789,16 @@ public class InteractionJankMonitor { } } private void removeTracker(@CujType int cuj) { private void removeTracker(@CujType int cuj, int reason) { synchronized (mLock) { mRunningTrackers.remove(cuj); if (mDebugOverlay != null) { mDebugOverlay.onTrackerRemoved(cuj, reason, mRunningTrackers); } if (DEBUG) { Log.d(TAG, "Removed tracker for " + getNameOfCuj(cuj) + ". mRunningTrackers=" + listNamesOfCujs(mRunningTrackers)); } } } Loading @@ -782,6 +811,16 @@ public class InteractionJankMonitor { mTraceThresholdFrameTimeMillis = properties.getInt( SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY, DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS); // Never allow the debug overlay to be used on user builds boolean debugOverlayEnabled = Build.IS_DEBUGGABLE && properties.getBoolean( SETTINGS_DEBUG_OVERLAY_ENABLED_KEY, DEFAULT_DEBUG_OVERLAY_ENABLED); if (debugOverlayEnabled && mDebugOverlay == null) { mDebugOverlay = new InteractionMonitorDebugOverlay(mDebugBgColor, mDebugYOffset); } else if (!debugOverlayEnabled && mDebugOverlay != null) { mDebugOverlay.dispose(); mDebugOverlay = null; } // The memory visibility is powered by the volatile field, mEnabled. mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED); } Loading Loading @@ -821,6 +860,39 @@ public class InteractionJankMonitor { return interactionType - 1; } /** * Configures the debug overlay used for displaying interaction names on the screen while they * occur. * * @param bgColor the background color of the box used to display the CUJ names * @param yOffset number between 0 and 1 to indicate where the top of the box should be relative * to the height of the screen */ public void configDebugOverlay(@ColorInt int bgColor, double yOffset) { mDebugBgColor = bgColor; mDebugYOffset = yOffset; } /** * A helper method for getting a string representation of all running CUJs. For example, * "(LOCKSCREEN_TRANSITION_FROM_AOD, IME_INSETS_ANIMATION)" */ private static String listNamesOfCujs(SparseArray<FrameTracker> trackers) { if (!DEBUG) { return null; } StringBuilder sb = new StringBuilder(); sb.append('('); for (int i = 0; i < trackers.size(); i++) { sb.append(getNameOfCuj(trackers.keyAt(i))); if (i < trackers.size() - 1) { sb.append(", "); } } sb.append(')'); return sb.toString(); } /** * A helper method to translate CUJ type to CUJ name. * Loading
core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java 0 → 100644 +240 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.jank; import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL; import android.annotation.ColorInt; import android.app.ActivityThread; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.os.Trace; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.WindowCallbacks; import com.android.internal.jank.FrameTracker.Reasons; import com.android.internal.jank.InteractionJankMonitor.CujType; /** * An overlay that uses WindowCallbacks to draw the names of all running CUJs to the window * associated with one of the CUJs being tracked. There's no guarantee which window it will * draw to. NOTE: sometimes the CUJ names will remain displayed on the screen longer than they * are actually running. * <p> * CUJ names will be drawn as follows: * <ul> * <li> Normal text indicates the CUJ is currently running * <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> */ class InteractionMonitorDebugOverlay implements WindowCallbacks { private static final int REASON_STILL_RUNNING = -1000; // Sparse array where the key in the CUJ and the value is the session status, or null if // it's currently running private final SparseIntArray mRunningCujs = new SparseIntArray(); private FrameTracker.ViewRootWrapper mViewRoot = null; private final Paint mDebugPaint; private final Paint.FontMetrics mDebugFontMetrics; // 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 String mPackageName; InteractionMonitorDebugOverlay(@ColorInt int bgColor, double yOffset) { mBgColor = bgColor; mYOffset = yOffset; mDebugPaint = new Paint(); mDebugPaint.setAntiAlias(false); mDebugFontMetrics = new Paint.FontMetrics(); final Context context = ActivityThread.currentApplication(); mPackageName = context.getPackageName(); } void dispose() { if (mViewRoot != null) { mViewRoot.removeWindowCallbacks(this); forceRedraw(); } mViewRoot = null; } private boolean attachViewRootIfNeeded(FrameTracker.ViewRootWrapper viewRoot) { if (mViewRoot == null && viewRoot != null) { mViewRoot = viewRoot; viewRoot.addWindowCallbacks(this); forceRedraw(); return true; } return false; } private float getWidthOfLongestCujName(int cujFontSize) { mDebugPaint.setTextSize(cujFontSize); float maxLength = 0; for (int i = 0; i < mRunningCujs.size(); i++) { String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); float textLength = mDebugPaint.measureText(cujName); if (textLength > maxLength) { maxLength = textLength; } } 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; } } private void forceRedraw() { if (mViewRoot != null) { mViewRoot.requestInvalidateRootRenderNode(); mViewRoot.getView().invalidate(); } } void onTrackerRemoved(@CujType int removedCuj, @Reasons int reason, SparseArray<FrameTracker> runningTrackers) { mRunningCujs.put(removedCuj, reason); // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { 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).getViewRoot())) { needsNewViewRoot = false; break; } } } if (needsNewViewRoot) { dispose(); for (int i = 0; i < runningTrackers.size(); i++) { if (attachViewRootIfNeeded(runningTrackers.valueAt(i).getViewRoot())) { break; } } } else { forceRedraw(); } } } void onTrackerAdded(@CujType int addedCuj, FrameTracker.ViewRootWrapper viewRoot) { // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ // is still running mRunningCujs.put(addedCuj, REASON_STILL_RUNNING); attachViewRootIfNeeded(viewRoot); 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() { } @Override public boolean onContentDrawn(int offsetX, int offsetY, int sizeX, int sizeY) { return false; } @Override public void onRequestDraw(boolean reportNextDraw) { } @Override public void onPostDraw(RecordingCanvas canvas) { Trace.beginSection("InteractionJankMonitor#drawDebug"); 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 dy = (int) (h * mYOffset); int packageNameFontSize = dipToPx(12); int cujFontSize = dipToPx(18); final float cujNameTextHeight = getTextHeight(cujFontSize); final float packageNameTextHeight = getTextHeight(packageNameFontSize); 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, padding * 2 + packageNameTextHeight + cujNameTextHeight * mRunningCujs.size(), mDebugPaint); mDebugPaint.setTextSize(packageNameFontSize); mDebugPaint.setColor(Color.BLACK); mDebugPaint.setStrikeThruText(false); canvas.translate(0, packageNameTextHeight); canvas.drawText("package:" + mPackageName, 0, 0, mDebugPaint); mDebugPaint.setTextSize(cujFontSize); // Draw text for CUJ names for (int i = 0; i < mRunningCujs.size(); i++) { int status = mRunningCujs.valueAt(i); if (status == REASON_STILL_RUNNING) { mDebugPaint.setColor(Color.BLACK); mDebugPaint.setStrikeThruText(false); } else if (status == REASON_END_NORMAL) { mDebugPaint.setColor(Color.GRAY); mDebugPaint.setStrikeThruText(false); } else { // Cancelled, or otherwise ended for a bad reason mDebugPaint.setColor(Color.RED); mDebugPaint.setStrikeThruText(true); } String cujName = InteractionJankMonitor.getNameOfCuj(mRunningCujs.keyAt(i)); canvas.translate(0, cujNameTextHeight); canvas.drawText(cujName, 0, 0, mDebugPaint); } Trace.endSection(); } }
packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +7 −4 Original line number Diff line number Diff line Loading @@ -50,6 +50,7 @@ import android.content.pm.PackageManager; import android.content.pm.ShortcutManager; import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.Color; import android.hardware.SensorManager; import android.hardware.SensorPrivacyManager; import android.hardware.biometrics.BiometricManager; Loading Loading @@ -113,13 +114,13 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.TestHarness; import com.android.systemui.shared.system.PackageManagerWrapper; import dagger.Module; import dagger.Provides; import java.util.Optional; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; /** * Provides Non-SystemUI, Framework-Owned instances to the dependency graph. */ Loading Loading @@ -323,7 +324,9 @@ public class FrameworkServicesModule { @Provides @Singleton static InteractionJankMonitor provideInteractionJankMonitor() { return InteractionJankMonitor.getInstance(); InteractionJankMonitor jankMonitor = InteractionJankMonitor.getInstance(); jankMonitor.configDebugOverlay(Color.YELLOW, 0.75); return jankMonitor; } @Provides Loading