Loading core/java/android/app/jank/JankTracker.java 0 → 100644 +219 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.os.Handler; import android.os.HandlerThread; import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.View; import android.view.ViewTreeObserver; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; /** * This class is responsible for registering callbacks that will receive JankData batches. * It handles managing the background thread that JankData will be processed on. As well as acting * as an intermediary between widgets and the state tracker, routing state changes to the tracker. * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public class JankTracker { // Tracks states reported by widgets. private StateTracker mStateTracker; // Processes JankData batches and associates frames to widget states. private JankDataProcessor mJankDataProcessor; // Background thread responsible for processing JankData batches. private HandlerThread mHandlerThread = new HandlerThread("AppJankTracker"); private Handler mHandler = null; // Needed so we know when the view is attached to a window. private ViewTreeObserver mViewTreeObserver; // Handle to a registered OnJankData listener. private SurfaceControl.OnJankDataListenerRegistration mJankDataListenerRegistration; // The interface to the windowing system that enables us to register for JankData. private AttachedSurfaceControl mSurfaceControl; // Name of the activity that is currently tracking Jank metrics. private String mActivityName; // The apps uid. private int mAppUid; // View that gives us access to ViewTreeObserver. private View mDecorView; /** * Set by the activity to enable or disable jank tracking. Activities may disable tracking if * they are paused or not enable tracking if they are not visible or if the app category is not * set. */ private boolean mTrackingEnabled = false; /** * Set to true once listeners are registered and JankData will start to be received. Both * mTrackingEnabled and mListenersRegistered need to be true for JankData to be processed. */ private boolean mListenersRegistered = false; public JankTracker(Choreographer choreographer, View decorView) { mStateTracker = new StateTracker(choreographer); mJankDataProcessor = new JankDataProcessor(mStateTracker); mDecorView = decorView; mHandlerThread.start(); registerWindowListeners(); } public void setActivityName(@NonNull String activityName) { mActivityName = activityName; } public void setAppUid(int uid) { mAppUid = uid; } /** * Will add the widget category, id and state as a UI state to associate frames to it. * @param widgetCategory preselected general widget category * @param widgetId developer defined widget id if available. * @param widgetState the current active widget state. */ public void addUiState(String widgetCategory, String widgetId, String widgetState) { if (!shouldTrack()) return; mStateTracker.putState(widgetCategory, widgetId, widgetState); } /** * Will remove the widget category, id and state as a ui state and no longer attribute frames * to it. * @param widgetCategory preselected general widget category * @param widgetId developer defined widget id if available. * @param widgetState no longer active widget state. */ public void removeUiState(String widgetCategory, String widgetId, String widgetState) { if (!shouldTrack()) return; mStateTracker.removeState(widgetCategory, widgetId, widgetState); } /** * Call to update a jank state to a different state. * @param widgetCategory preselected general widget category. * @param widgetId developer defined widget id if available. * @param currentState current state of the widget. * @param nextState the state the widget will be in. */ public void updateUiState(String widgetCategory, String widgetId, String currentState, String nextState) { if (!shouldTrack()) return; mStateTracker.updateState(widgetCategory, widgetId, currentState, nextState); } /** * Will enable jank tracking, and add the activity as a state to associate frames to. */ public void enableAppJankTracking() { // Add the activity as a state, this will ensure we track frames to the activity without the // need of a decorated widget to be used. // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. mStateTracker.putState("NONE", mActivityName, "NONE"); mTrackingEnabled = true; } /** * Will disable jank tracking, and remove the activity as a state to associate frames to. */ public void disableAppJankTracking() { mTrackingEnabled = false; // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. mStateTracker.removeState("NONE", mActivityName, "NONE"); } /** * Retrieve all pending widget states, this is intended for testing purposes only. * @param stateDataList the ArrayList that will be populated with the pending states. */ @VisibleForTesting public void getAllUiStates(@NonNull ArrayList<StateTracker.StateData> stateDataList) { mStateTracker.retrieveAllStates(stateDataList); } /** * Only intended to be used by tests, the runnable that registers the listeners may not run * in time for tests to pass. This forces them to run immediately. */ @VisibleForTesting public void forceListenerRegistration() { mSurfaceControl = mDecorView.getRootSurfaceControl(); registerForJankData(); // TODO b/376116199 Check if registration is good. mListenersRegistered = true; } private void registerForJankData() { if (mSurfaceControl == null) return; /* TODO b/376115668 Register for JankData batches from new JankTracking API */ } private boolean shouldTrack() { return mTrackingEnabled && mListenersRegistered; } /** * Need to know when the decor view gets attached to the window in order to get * AttachedSurfaceControl. In order to register a callback for OnJankDataListener * AttachedSurfaceControl needs to be created which only happens after onWindowAttached is * called. This is why there is a delay in posting the runnable. */ private void registerWindowListeners() { if (mDecorView == null) return; mViewTreeObserver = mDecorView.getViewTreeObserver(); mViewTreeObserver.addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() { @Override public void onWindowAttached() { getHandler().postDelayed(new Runnable() { @Override public void run() { forceListenerRegistration(); } }, 1000); } @Override public void onWindowDetached() { // TODO b/376116199 do we un-register the callback or just not process the data. } }); } private Handler getHandler() { if (mHandler == null) { mHandler = new Handler(mHandlerThread.getLooper()); } return mHandler; } } tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java 0 → 100644 +156 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank.tests; import static org.junit.Assert.assertEquals; import android.app.jank.Flags; import android.app.jank.JankTracker; import android.app.jank.StateTracker; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.Choreographer; import android.view.View; import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ActivityScenario; import androidx.test.runner.AndroidJUnit4; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class JankTrackerTest { private Choreographer mChoreographer; private JankTracker mJankTracker; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); /** * Start an empty activity so decore view is not null when creating the JankTracker instance. */ private static ActivityScenario<EmptyActivity> sEmptyActivityRule; private static String sActivityName; private static View sActivityDecorView; @BeforeClass public static void classSetup() { sEmptyActivityRule = ActivityScenario.launch(EmptyActivity.class); sEmptyActivityRule.onActivity(activity -> { sActivityDecorView = activity.getWindow().getDecorView(); sActivityName = activity.toString(); }); } @AfterClass public static void classTearDown() { sEmptyActivityRule.close(); } @Before @UiThreadTest public void setup() { mChoreographer = Choreographer.getInstance(); mJankTracker = new JankTracker(mChoreographer, sActivityDecorView); mJankTracker.setActivityName(sActivityName); } /** * When jank tracking is enabled the activity name should be added as a state to associate * frames to it. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTracking_WhenEnabled_ActivityAdded() { mJankTracker.enableAppJankTracking(); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(1, stateData.size()); StateTracker.StateData firstState = stateData.getFirst(); assertEquals(sActivityName, firstState.mWidgetId); } /** * No states should be added when tracking is disabled. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTrackingDisabled_StatesShouldNot_BeAddedToTracker() { mJankTracker.disableAppJankTracking(); mJankTracker.addUiState("FAKE_CATEGORY", "FAKE_ID", "FAKE_STATE"); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(0, stateData.size()); } /** * The activity name as well as the test state should be added for frame association. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTrackingEnabled_StatesShould_BeAddedToTracker() { mJankTracker.forceListenerRegistration(); mJankTracker.enableAppJankTracking(); mJankTracker.addUiState("FAKE_CATEGORY", "FAKE_ID", "FAKE_STATE"); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(2, stateData.size()); } /** * Activity state should only be added once even if jank tracking is enabled multiple times. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTrackingEnabled_EnabledCalledTwice_ActivityStateOnlyAddedOnce() { mJankTracker.enableAppJankTracking(); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(1, stateData.size()); stateData.clear(); mJankTracker.enableAppJankTracking(); mJankTracker.getAllUiStates(stateData); assertEquals(1, stateData.size()); } } Loading
core/java/android/app/jank/JankTracker.java 0 → 100644 +219 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.os.Handler; import android.os.HandlerThread; import android.view.AttachedSurfaceControl; import android.view.Choreographer; import android.view.SurfaceControl; import android.view.View; import android.view.ViewTreeObserver; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; /** * This class is responsible for registering callbacks that will receive JankData batches. * It handles managing the background thread that JankData will be processed on. As well as acting * as an intermediary between widgets and the state tracker, routing state changes to the tracker. * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public class JankTracker { // Tracks states reported by widgets. private StateTracker mStateTracker; // Processes JankData batches and associates frames to widget states. private JankDataProcessor mJankDataProcessor; // Background thread responsible for processing JankData batches. private HandlerThread mHandlerThread = new HandlerThread("AppJankTracker"); private Handler mHandler = null; // Needed so we know when the view is attached to a window. private ViewTreeObserver mViewTreeObserver; // Handle to a registered OnJankData listener. private SurfaceControl.OnJankDataListenerRegistration mJankDataListenerRegistration; // The interface to the windowing system that enables us to register for JankData. private AttachedSurfaceControl mSurfaceControl; // Name of the activity that is currently tracking Jank metrics. private String mActivityName; // The apps uid. private int mAppUid; // View that gives us access to ViewTreeObserver. private View mDecorView; /** * Set by the activity to enable or disable jank tracking. Activities may disable tracking if * they are paused or not enable tracking if they are not visible or if the app category is not * set. */ private boolean mTrackingEnabled = false; /** * Set to true once listeners are registered and JankData will start to be received. Both * mTrackingEnabled and mListenersRegistered need to be true for JankData to be processed. */ private boolean mListenersRegistered = false; public JankTracker(Choreographer choreographer, View decorView) { mStateTracker = new StateTracker(choreographer); mJankDataProcessor = new JankDataProcessor(mStateTracker); mDecorView = decorView; mHandlerThread.start(); registerWindowListeners(); } public void setActivityName(@NonNull String activityName) { mActivityName = activityName; } public void setAppUid(int uid) { mAppUid = uid; } /** * Will add the widget category, id and state as a UI state to associate frames to it. * @param widgetCategory preselected general widget category * @param widgetId developer defined widget id if available. * @param widgetState the current active widget state. */ public void addUiState(String widgetCategory, String widgetId, String widgetState) { if (!shouldTrack()) return; mStateTracker.putState(widgetCategory, widgetId, widgetState); } /** * Will remove the widget category, id and state as a ui state and no longer attribute frames * to it. * @param widgetCategory preselected general widget category * @param widgetId developer defined widget id if available. * @param widgetState no longer active widget state. */ public void removeUiState(String widgetCategory, String widgetId, String widgetState) { if (!shouldTrack()) return; mStateTracker.removeState(widgetCategory, widgetId, widgetState); } /** * Call to update a jank state to a different state. * @param widgetCategory preselected general widget category. * @param widgetId developer defined widget id if available. * @param currentState current state of the widget. * @param nextState the state the widget will be in. */ public void updateUiState(String widgetCategory, String widgetId, String currentState, String nextState) { if (!shouldTrack()) return; mStateTracker.updateState(widgetCategory, widgetId, currentState, nextState); } /** * Will enable jank tracking, and add the activity as a state to associate frames to. */ public void enableAppJankTracking() { // Add the activity as a state, this will ensure we track frames to the activity without the // need of a decorated widget to be used. // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. mStateTracker.putState("NONE", mActivityName, "NONE"); mTrackingEnabled = true; } /** * Will disable jank tracking, and remove the activity as a state to associate frames to. */ public void disableAppJankTracking() { mTrackingEnabled = false; // TODO b/376116199 replace "NONE" with UNSPECIFIED once the API changes are merged. mStateTracker.removeState("NONE", mActivityName, "NONE"); } /** * Retrieve all pending widget states, this is intended for testing purposes only. * @param stateDataList the ArrayList that will be populated with the pending states. */ @VisibleForTesting public void getAllUiStates(@NonNull ArrayList<StateTracker.StateData> stateDataList) { mStateTracker.retrieveAllStates(stateDataList); } /** * Only intended to be used by tests, the runnable that registers the listeners may not run * in time for tests to pass. This forces them to run immediately. */ @VisibleForTesting public void forceListenerRegistration() { mSurfaceControl = mDecorView.getRootSurfaceControl(); registerForJankData(); // TODO b/376116199 Check if registration is good. mListenersRegistered = true; } private void registerForJankData() { if (mSurfaceControl == null) return; /* TODO b/376115668 Register for JankData batches from new JankTracking API */ } private boolean shouldTrack() { return mTrackingEnabled && mListenersRegistered; } /** * Need to know when the decor view gets attached to the window in order to get * AttachedSurfaceControl. In order to register a callback for OnJankDataListener * AttachedSurfaceControl needs to be created which only happens after onWindowAttached is * called. This is why there is a delay in posting the runnable. */ private void registerWindowListeners() { if (mDecorView == null) return; mViewTreeObserver = mDecorView.getViewTreeObserver(); mViewTreeObserver.addOnWindowAttachListener(new ViewTreeObserver.OnWindowAttachListener() { @Override public void onWindowAttached() { getHandler().postDelayed(new Runnable() { @Override public void run() { forceListenerRegistration(); } }, 1000); } @Override public void onWindowDetached() { // TODO b/376116199 do we un-register the callback or just not process the data. } }); } private Handler getHandler() { if (mHandler == null) { mHandler = new Handler(mHandlerThread.getLooper()); } return mHandler; } }
tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java 0 → 100644 +156 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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 android.app.jank.tests; import static org.junit.Assert.assertEquals; import android.app.jank.Flags; import android.app.jank.JankTracker; import android.app.jank.StateTracker; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.Choreographer; import android.view.View; import androidx.test.annotation.UiThreadTest; import androidx.test.core.app.ActivityScenario; import androidx.test.runner.AndroidJUnit4; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class JankTrackerTest { private Choreographer mChoreographer; private JankTracker mJankTracker; @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); /** * Start an empty activity so decore view is not null when creating the JankTracker instance. */ private static ActivityScenario<EmptyActivity> sEmptyActivityRule; private static String sActivityName; private static View sActivityDecorView; @BeforeClass public static void classSetup() { sEmptyActivityRule = ActivityScenario.launch(EmptyActivity.class); sEmptyActivityRule.onActivity(activity -> { sActivityDecorView = activity.getWindow().getDecorView(); sActivityName = activity.toString(); }); } @AfterClass public static void classTearDown() { sEmptyActivityRule.close(); } @Before @UiThreadTest public void setup() { mChoreographer = Choreographer.getInstance(); mJankTracker = new JankTracker(mChoreographer, sActivityDecorView); mJankTracker.setActivityName(sActivityName); } /** * When jank tracking is enabled the activity name should be added as a state to associate * frames to it. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTracking_WhenEnabled_ActivityAdded() { mJankTracker.enableAppJankTracking(); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(1, stateData.size()); StateTracker.StateData firstState = stateData.getFirst(); assertEquals(sActivityName, firstState.mWidgetId); } /** * No states should be added when tracking is disabled. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTrackingDisabled_StatesShouldNot_BeAddedToTracker() { mJankTracker.disableAppJankTracking(); mJankTracker.addUiState("FAKE_CATEGORY", "FAKE_ID", "FAKE_STATE"); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(0, stateData.size()); } /** * The activity name as well as the test state should be added for frame association. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTrackingEnabled_StatesShould_BeAddedToTracker() { mJankTracker.forceListenerRegistration(); mJankTracker.enableAppJankTracking(); mJankTracker.addUiState("FAKE_CATEGORY", "FAKE_ID", "FAKE_STATE"); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(2, stateData.size()); } /** * Activity state should only be added once even if jank tracking is enabled multiple times. */ @Test @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) public void jankTrackingEnabled_EnabledCalledTwice_ActivityStateOnlyAddedOnce() { mJankTracker.enableAppJankTracking(); ArrayList<StateTracker.StateData> stateData = new ArrayList<>(); mJankTracker.getAllUiStates(stateData); assertEquals(1, stateData.size()); stateData.clear(); mJankTracker.enableAppJankTracking(); mJankTracker.getAllUiStates(stateData); assertEquals(1, stateData.size()); } }