diff options
| -rw-r--r-- | core/java/android/app/jank/StateTracker.java | 206 | ||||
| -rw-r--r-- | tests/AppJankTest/Android.bp | 37 | ||||
| -rw-r--r-- | tests/AppJankTest/AndroidManifest.xml | 40 | ||||
| -rw-r--r-- | tests/AppJankTest/AndroidTest.xml | 38 | ||||
| -rw-r--r-- | tests/AppJankTest/OWNERS | 4 | ||||
| -rw-r--r-- | tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java | 22 | ||||
| -rw-r--r-- | tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java | 227 |
7 files changed, 574 insertions, 0 deletions
diff --git a/core/java/android/app/jank/StateTracker.java b/core/java/android/app/jank/StateTracker.java new file mode 100644 index 000000000000..cb457ff64430 --- /dev/null +++ b/core/java/android/app/jank/StateTracker.java @@ -0,0 +1,206 @@ +/* + * 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.util.Pools.SimplePool; +import android.view.Choreographer; + +import androidx.annotation.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** + * StateTracker is responsible for keeping track of currently active states as well as + * previously encountered states. States are added, updated or removed by widgets that support state + * tracking. When a state is first added it will get a vsyncid associated to it, when that state + * is removed or updated to a different state it will have a second vsyncid associated with it. The + * two vsyncids create a range of ids where that particular state was active. + * @hide + */ +@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) +@VisibleForTesting +public class StateTracker { + + // Used to synchronize access to mPreviousStates. + private final Object mLock = new Object(); + private Choreographer mChoreographer; + + // The max number of StateData objects that will be stored in the pool for reuse. + private static final int MAX_POOL_SIZE = 500; + // The max number of currently active states to track. + protected static final int MAX_CONCURRENT_STATE_COUNT = 25; + // The maximum number of previously seen states that will be counted. + protected static final int MAX_PREVIOUSLY_ACTIVE_STATE_COUNT = 1000; + + // Pool to store the previously used StateData objects to save recreating them each time. + private final SimplePool<StateData> mStateDataObjectPool = new SimplePool<>(MAX_POOL_SIZE); + // Previously encountered states that have not been associated to a frame. + private ArrayList<StateData> mPreviousStates = new ArrayList<>(); + // Currently active widgets and widget states + private ConcurrentHashMap<String, StateData> mActiveStates = new ConcurrentHashMap<>(); + + public StateTracker(@NonNull Choreographer choreographer) { + mChoreographer = choreographer; + } + + /** + * Updates the currentState to the nextState. + * @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 updateState(@NonNull String widgetCategory, @NonNull String widgetId, + @NonNull String currentState, @NonNull String nextState) { + // remove the now inactive state from the active states list + removeState(widgetCategory, widgetId, currentState); + + // add the updated state to the active states list + putState(widgetCategory, widgetId, nextState); + } + + /** + * Removes the state from the active state list and adds it to the previously encountered state + * list. Associates an end vsync id to the state. + * @param widgetCategory preselected general widget category. + * @param widgetId developer defined widget id if available. + * @param widgetState no longer active widget state. + */ + public void removeState(@NonNull String widgetCategory, @NonNull String widgetId, + @NonNull String widgetState) { + + String stateKey = getStateKey(widgetCategory, widgetId, widgetState); + // Check if we have the active state + StateData stateData = mActiveStates.remove(stateKey); + + // If there are no states that match just return. + // This can happen if mActiveStates is at MAX_CONCURRENT_STATE_COUNT and a widget tries to + // remove a state that was never added or if a widget tries to remove the same state twice. + if (stateData == null) return; + + synchronized (mLock) { + stateData.mVsyncIdEnd = mChoreographer.getVsyncId(); + // Add the StateData to the previous state list. We need to keep a list of all the + // previously active states until we can process the next batch of frame data. + if (mPreviousStates.size() < MAX_PREVIOUSLY_ACTIVE_STATE_COUNT) { + mPreviousStates.add(stateData); + } + } + } + + /** + * Adds a new state to the active state list. Associates a start vsync id to the state. + * @param widgetCategory preselected general widget category. + * @param widgetId developer defined widget id if available. + * @param widgetState the current active widget state. + */ + public void putState(@NonNull String widgetCategory, @NonNull String widgetId, + @NonNull String widgetState) { + + // Check if we can accept a new state + if (mActiveStates.size() >= MAX_CONCURRENT_STATE_COUNT) return; + + String stateKey = getStateKey(widgetCategory, widgetId, widgetState); + + // Check if there is currently any active states + // if there is already a state that matches then its presumed as still active. + if (mActiveStates.containsKey(stateKey)) return; + + // Check if we have am unused state object in the pool + StateData stateData = mStateDataObjectPool.acquire(); + if (stateData == null) { + stateData = new StateData(); + } + stateData.mVsyncIdStart = mChoreographer.getVsyncId(); + stateData.mStateDataKey = stateKey; + stateData.mWidgetState = widgetState; + stateData.mWidgetCategory = widgetCategory; + stateData.mWidgetId = widgetId; + stateData.mVsyncIdEnd = Long.MAX_VALUE; + mActiveStates.put(stateKey, stateData); + + } + + /** + * Will add all previously encountered states as well as all currently active states to the list + * that was passed in. + * @param allStates the list that will be populated with the widget states. + */ + public void retrieveAllStates(ArrayList<StateData> allStates) { + synchronized (mLock) { + allStates.addAll(mPreviousStates); + allStates.addAll(mActiveStates.values()); + } + } + + /** + * Call after processing a batch of JankData, will remove any processed states from the + * previous state list. + */ + public void stateProcessingComplete() { + synchronized (mLock) { + for (int i = mPreviousStates.size() - 1; i >= 0; i--) { + StateData stateData = mPreviousStates.get(i); + if (stateData.mProcessed) { + mPreviousStates.remove(stateData); + mStateDataObjectPool.release(stateData); + } + } + } + } + + /** + * Only intended to be used for testing, this enables test methods to submit pending states + * with known start and end vsyncids. This allows testing methods to know the exact ranges + * of vysncid and calculate exactly how many states should or should not be processed. + * @param stateData the data that will be added. + * + */ + @VisibleForTesting + public void addPendingStateData(List<StateData> stateData) { + synchronized (mLock) { + mPreviousStates.addAll(stateData); + } + } + + private String getStateKey(String widgetCategory, String widgetId, String widgetState) { + return widgetCategory + widgetId + widgetState; + } + + /** + * @hide + */ + @VisibleForTesting + public static class StateData { + + // Concatenated string of widget category, widget state and widget id. + public String mStateDataKey; + public String mWidgetCategory; + public String mWidgetState; + public String mWidgetId; + // vsyncid when the state was first added. + public long mVsyncIdStart; + // vsyncid for when the state was removed. + public long mVsyncIdEnd; + // Used to indicate whether this state has been processed and can be returned to the pool. + public boolean mProcessed; + } +} diff --git a/tests/AppJankTest/Android.bp b/tests/AppJankTest/Android.bp new file mode 100644 index 000000000000..acf8dc9aca47 --- /dev/null +++ b/tests/AppJankTest/Android.bp @@ -0,0 +1,37 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "CoreAppJankTestCases", + team: "trendy_team_system_performance", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.rules", + "androidx.test.core", + "platform-test-annotations", + "flag-junit", + ], + platform_apis: true, + test_suites: ["device-tests"], + certificate: "platform", +} diff --git a/tests/AppJankTest/AndroidManifest.xml b/tests/AppJankTest/AndroidManifest.xml new file mode 100644 index 000000000000..ae973393b90e --- /dev/null +++ b/tests/AppJankTest/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.app.jank.tests"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity android:name=".EmptyActivity" + android:label="EmptyActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <action android:name="android.intent.action.VIEW_PERMISSION_USAGE"/> + </intent-filter> + </activity> + </application> + + <!-- self-instrumenting test package. --> + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.app.jank.tests" + android:label="Core tests of App Jank Tracking"> + </instrumentation> + +</manifest>
\ No newline at end of file diff --git a/tests/AppJankTest/AndroidTest.xml b/tests/AppJankTest/AndroidTest.xml new file mode 100644 index 000000000000..c01c75c9695c --- /dev/null +++ b/tests/AppJankTest/AndroidTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<configuration description="Config for Core App Jank Tests"> + <option name="test-suite-tag" value="apct"/> + + <option name="config-descriptor:metadata" key="component" value="systems"/> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + <option name="config-descriptor:metadata" key="parameter" value="no_foldable_states" /> + + <option name="not-shardable" value="true" /> + <option name="install-arg" value="-t" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="CoreAppJankTestCases.apk"/> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="android.app.jank.tests"/> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner"/> + </test> +</configuration>
\ No newline at end of file diff --git a/tests/AppJankTest/OWNERS b/tests/AppJankTest/OWNERS new file mode 100644 index 000000000000..806de574b071 --- /dev/null +++ b/tests/AppJankTest/OWNERS @@ -0,0 +1,4 @@ +steventerrell@google.com +carmenjackson@google.com +jjaggi@google.com +pmuetschard@google.com
\ No newline at end of file diff --git a/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java b/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java new file mode 100644 index 000000000000..b326765ab097 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/EmptyActivity.java @@ -0,0 +1,22 @@ +/* + * 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 android.app.Activity; + +public class EmptyActivity extends Activity { +} diff --git a/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java b/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java new file mode 100644 index 000000000000..541009e05e55 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/StateTrackerTest.java @@ -0,0 +1,227 @@ +/* + * 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 static org.junit.Assert.assertTrue; + +import android.app.jank.Flags; +import android.app.jank.StateTracker; +import android.app.jank.StateTracker.StateData; +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 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 StateTrackerTest { + + private static final String WIDGET_CATEGORY_NONE = "None"; + private static final String WIDGET_CATEGORY_SCROLL = "Scroll"; + private static final String WIDGET_STATE_IDLE = "Idle"; + private static final String WIDGET_STATE_SCROLLING = "Scrolling"; + private StateTracker mStateTracker; + private Choreographer mChoreographer; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + /** + * Start an empty activity so choreographer won't return -1 for vsyncid. + */ + private static ActivityScenario<EmptyActivity> sEmptyActivityRule; + + @BeforeClass + public static void classSetup() { + sEmptyActivityRule = ActivityScenario.launch(EmptyActivity.class); + } + + @AfterClass + public static void classTearDown() { + sEmptyActivityRule.close(); + } + + @Before + @UiThreadTest + public void setup() { + mChoreographer = Choreographer.getInstance(); + mStateTracker = new StateTracker(mChoreographer); + } + + /** + * Check that the start vsyncid is added when the state is first added and end vsyncid is + * set to the default value, indicating it has not been updated. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void addWidgetState_VerifyStateHasStartVsyncId() { + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addWidgetState_VerifyStateHasStartVsyncId"); + + ArrayList<StateData> stateList = new ArrayList<StateData>(); + mStateTracker.retrieveAllStates(stateList); + StateData stateData = stateList.get(0); + + assertTrue(stateData.mVsyncIdStart > 0); + assertTrue(stateData.mVsyncIdEnd == Long.MAX_VALUE); + } + + /** + * Check that the end vsyncid is added when the state is removed. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void removeWidgetState_VerifyStateHasEndVsyncId() { + + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "removeWidgetState_VerifyStateHasEndVsyncId"); + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "removeWidgetState_VerifyStateHasEndVsyncId"); + + ArrayList<StateData> stateList = new ArrayList<StateData>(); + mStateTracker.retrieveAllStates(stateList); + StateData stateData = stateList.get(0); + + assertTrue(stateData.mVsyncIdStart > 0); + assertTrue(stateData.mVsyncIdEnd != Long.MAX_VALUE); + } + + /** + * Check that duplicate states are aggregated into only one active instance. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void addDuplicateStates_ConfirmStateCountOnlyOne() { + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addDuplicateStates_ConfirmStateCountOnlyOne"); + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + assertEquals(stateList.size(), 1); + + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addDuplicateStates_ConfirmStateCountOnlyOne"); + + stateList.clear(); + + mStateTracker.retrieveAllStates(stateList); + + assertEquals(stateList.size(), 1); + } + + /** + * Check that correct distinct states are returned when all states are retrieved. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void addThreeStateChanges_ConfirmThreeStatesReturned() { + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addThreeStateChanges_ConfirmThreeStatesReturned"); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addThreeStateChanges_ConfirmThreeStatesReturned_01"); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + "addThreeStateChanges_ConfirmThreeStatesReturned_02"); + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + assertEquals(stateList.size(), 3); + } + + /** + * Confirm when states are added and removed the removed states are moved to the previousStates + * list and returned when retrieveAllStates is called. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void simulateAddingSeveralStates() { + for (int i = 0; i < 20; i++) { + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i - 1)); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i)); + } + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + int countStatesWithEndVsync = 0; + for (int i = 0; i < stateList.size(); i++) { + if (stateList.get(i).mVsyncIdEnd != Long.MAX_VALUE) { + countStatesWithEndVsync++; + } + } + + // The last state that was added would be an active state and should not have an associated + // end vsyncid. + assertEquals(19, countStatesWithEndVsync); + } + + /** + * Confirm once a state has been attributed to a frame it has been removed from the previous + * state list. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void confirmProcessedStates_RemovedFromPreviousStateList() { + for (int i = 0; i < 20; i++) { + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i - 1)); + mStateTracker.putState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i)); + + if (i == 19) { + mStateTracker.removeState(WIDGET_CATEGORY_SCROLL, WIDGET_STATE_SCROLLING, + String.format("simulateAddingSeveralStates_%s", i)); + } + } + + ArrayList<StateData> stateList = new ArrayList<>(); + mStateTracker.retrieveAllStates(stateList); + + assertEquals(20, stateList.size()); + + // Simulate processing all the states. + for (int i = 0; i < stateList.size(); i++) { + stateList.get(i).mProcessed = true; + } + // Clear out all processed states. + mStateTracker.stateProcessingComplete(); + + stateList.clear(); + + mStateTracker.retrieveAllStates(stateList); + + assertEquals(0, stateList.size()); + } +} |