Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 40244ee5 authored by Steven Terrell's avatar Steven Terrell
Browse files

Add Widget State Tracker

This change adds the state tracking portion of the detailed app jank
metrics feature. App Widgets will add/update/remove states throughout
their lifecycle; this change adds logic that will keep track of those
state changes. Several states could be active at once, when a state is
no longer active it will be moved to a previous state list.

The previously active states will eventually be processed and returned to
the state pool. States will be processed after each batch of JankData is
received. Currently JankData batches are received after 50 frames have
been rendered.

This change also adds unit tests that test the basic functionality of
the StateTracker. This includes adding/removing states as well as
retrieving states to process and returning processed states back to the
state pool.

Change-Id: I17a3f163f7b67d9451a9fe13c0d6520d2f658fb1
Bug: 368405795
Test: atest CoreAppJankTestCases
Flag: android.app.jank.detailed_app_jank_metrics_api
parent 980515dd
Loading
Loading
Loading
Loading
+206 −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.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;
    }
}
+37 −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 {
    // 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",
}
+40 −0
Original line number Diff line number Diff line
<?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
+38 −0
Original line number Diff line number Diff line
<?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
+4 −0
Original line number Diff line number Diff line
steventerrell@google.com
carmenjackson@google.com
jjaggi@google.com
pmuetschard@google.com
 No newline at end of file
Loading