diff options
| -rw-r--r-- | core/java/android/app/jank/JankTracker.java | 219 | ||||
| -rw-r--r-- | tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java | 156 |
2 files changed, 375 insertions, 0 deletions
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java new file mode 100644 index 000000000000..df422e0069c5 --- /dev/null +++ b/core/java/android/app/jank/JankTracker.java @@ -0,0 +1,219 @@ +/* + * 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; + } +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java new file mode 100644 index 000000000000..a3e5533599bc --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankTrackerTest.java @@ -0,0 +1,156 @@ +/* + * 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()); + } +} |