Add Jank Tracker
This class ties together widget state tracking and JankData
processing. As well as acting as a controller for Activities to enable
or disable app jank tracking.
It will be responsible for registering callbacks to receive JankData
batches and routing those batches to the JankDataProcessor. It will also
act as an intermediary for widgets to report state to the state tracker.
Bug: 375449447
Test: atest CoreAppJankTestCases
Flag: android.app.jank.detailed_app_jank_metrics_api
Change-Id: I64faefa141480f66c9082ed79e8776fdcd7fe283
diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java
new file mode 100644
index 0000000..df422e0
--- /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 0000000..a3e5533
--- /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());
+ }
+}