1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.jank; 18 19 import android.annotation.FlaggedApi; 20 import android.annotation.NonNull; 21 import android.util.Pools.SimplePool; 22 import android.view.Choreographer; 23 24 import androidx.annotation.VisibleForTesting; 25 26 import java.util.ArrayList; 27 import java.util.List; 28 import java.util.concurrent.ConcurrentHashMap; 29 30 /** 31 * StateTracker is responsible for keeping track of currently active states as well as 32 * previously encountered states. States are added, updated or removed by widgets that support state 33 * tracking. When a state is first added it will get a vsyncid associated to it, when that state 34 * is removed or updated to a different state it will have a second vsyncid associated with it. The 35 * two vsyncids create a range of ids where that particular state was active. 36 * @hide 37 */ 38 @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) 39 public class StateTracker { 40 41 // Used to synchronize access to mPreviousStates. 42 private final Object mLock = new Object(); 43 private Choreographer mChoreographer; 44 45 // The max number of StateData objects that will be stored in the pool for reuse. 46 private static final int MAX_POOL_SIZE = 500; 47 // The max number of currently active states to track. 48 protected static final int MAX_CONCURRENT_STATE_COUNT = 25; 49 // The maximum number of previously seen states that will be counted. 50 protected static final int MAX_PREVIOUSLY_ACTIVE_STATE_COUNT = 1000; 51 52 // Pool to store the previously used StateData objects to save recreating them each time. 53 private final SimplePool<StateData> mStateDataObjectPool = new SimplePool<>(MAX_POOL_SIZE); 54 // Previously encountered states that have not been associated to a frame. 55 private ArrayList<StateData> mPreviousStates = new ArrayList<>(); 56 // Currently active widgets and widget states 57 private ConcurrentHashMap<String, StateData> mActiveStates = new ConcurrentHashMap<>(); 58 StateTracker(@onNull Choreographer choreographer)59 public StateTracker(@NonNull Choreographer choreographer) { 60 mChoreographer = choreographer; 61 } 62 63 /** 64 * Updates the currentState to the nextState. 65 * @param widgetCategory preselected general widget category. 66 * @param widgetId developer defined widget id if available. 67 * @param currentState current state of the widget. 68 * @param nextState the state the widget will be in. 69 */ updateState(@onNull String widgetCategory, @NonNull String widgetId, @NonNull String currentState, @NonNull String nextState)70 public void updateState(@NonNull String widgetCategory, @NonNull String widgetId, 71 @NonNull String currentState, @NonNull String nextState) { 72 // remove the now inactive state from the active states list 73 removeState(widgetCategory, widgetId, currentState); 74 75 // add the updated state to the active states list 76 putState(widgetCategory, widgetId, nextState); 77 } 78 79 /** 80 * Removes the state from the active state list and adds it to the previously encountered state 81 * list. Associates an end vsync id to the state. 82 * @param widgetCategory preselected general widget category. 83 * @param widgetId developer defined widget id if available. 84 * @param widgetState no longer active widget state. 85 */ removeState(@onNull String widgetCategory, @NonNull String widgetId, @NonNull String widgetState)86 public void removeState(@NonNull String widgetCategory, @NonNull String widgetId, 87 @NonNull String widgetState) { 88 89 String stateKey = getStateKey(widgetCategory, widgetId, widgetState); 90 // Check if we have the active state 91 StateData stateData = mActiveStates.remove(stateKey); 92 93 // If there are no states that match just return. 94 // This can happen if mActiveStates is at MAX_CONCURRENT_STATE_COUNT and a widget tries to 95 // remove a state that was never added or if a widget tries to remove the same state twice. 96 if (stateData == null) return; 97 98 synchronized (mLock) { 99 stateData.mVsyncIdEnd = mChoreographer.getVsyncId(); 100 // Add the StateData to the previous state list. We need to keep a list of all the 101 // previously active states until we can process the next batch of frame data. 102 if (mPreviousStates.size() < MAX_PREVIOUSLY_ACTIVE_STATE_COUNT) { 103 mPreviousStates.add(stateData); 104 } 105 } 106 } 107 108 /** 109 * Adds a new state to the active state list. Associates a start vsync id to the state. 110 * @param widgetCategory preselected general widget category. 111 * @param widgetId developer defined widget id if available. 112 * @param widgetState the current active widget state. 113 */ putState(@onNull String widgetCategory, @NonNull String widgetId, @NonNull String widgetState)114 public void putState(@NonNull String widgetCategory, @NonNull String widgetId, 115 @NonNull String widgetState) { 116 117 // Check if we can accept a new state 118 if (mActiveStates.size() >= MAX_CONCURRENT_STATE_COUNT) return; 119 120 String stateKey = getStateKey(widgetCategory, widgetId, widgetState); 121 122 // Check if there is currently any active states 123 // if there is already a state that matches then its presumed as still active. 124 if (mActiveStates.containsKey(stateKey)) return; 125 126 // Check if we have am unused state object in the pool 127 StateData stateData = mStateDataObjectPool.acquire(); 128 if (stateData == null) { 129 stateData = new StateData(); 130 } 131 stateData.mVsyncIdStart = mChoreographer.getVsyncId(); 132 stateData.mStateDataKey = stateKey; 133 stateData.mWidgetState = widgetState; 134 stateData.mWidgetCategory = widgetCategory; 135 stateData.mWidgetId = widgetId; 136 stateData.mVsyncIdEnd = Long.MAX_VALUE; 137 mActiveStates.put(stateKey, stateData); 138 139 } 140 141 /** 142 * Will add all previously encountered states as well as all currently active states to the list 143 * that was passed in. 144 * @param allStates the list that will be populated with the widget states. 145 */ retrieveAllStates(ArrayList<StateData> allStates)146 public void retrieveAllStates(ArrayList<StateData> allStates) { 147 synchronized (mLock) { 148 allStates.addAll(mPreviousStates); 149 allStates.addAll(mActiveStates.values()); 150 } 151 } 152 153 /** 154 * Call after processing a batch of JankData, will remove any processed states from the 155 * previous state list. 156 */ stateProcessingComplete()157 public void stateProcessingComplete() { 158 synchronized (mLock) { 159 for (int i = mPreviousStates.size() - 1; i >= 0; i--) { 160 StateData stateData = mPreviousStates.get(i); 161 if (stateData.mProcessed) { 162 mPreviousStates.remove(stateData); 163 mStateDataObjectPool.release(stateData); 164 } 165 } 166 } 167 } 168 169 /** 170 * Only intended to be used for testing, this enables test methods to submit pending states 171 * with known start and end vsyncids. This allows testing methods to know the exact ranges 172 * of vysncid and calculate exactly how many states should or should not be processed. 173 * @param stateData the data that will be added. 174 * 175 */ 176 @VisibleForTesting addPendingStateData(List<StateData> stateData)177 public void addPendingStateData(List<StateData> stateData) { 178 synchronized (mLock) { 179 mPreviousStates.addAll(stateData); 180 } 181 } 182 183 /** 184 * Returns a concatenated string of the inputs. This key can be used to retrieve both pending 185 * stats and the state that was used to create the pending stat. 186 */ getStateKey(String widgetCategory, String widgetId, String widgetState)187 public String getStateKey(String widgetCategory, String widgetId, String widgetState) { 188 return widgetCategory + widgetId + widgetState; 189 } 190 191 /** 192 * @hide 193 */ 194 public static class StateData { 195 196 // Concatenated string of widget category, widget state and widget id. 197 public String mStateDataKey; 198 public String mWidgetCategory; 199 public String mWidgetState; 200 public String mWidgetId; 201 // vsyncid when the state was first added. 202 public long mVsyncIdStart; 203 // vsyncid for when the state was removed. 204 public long mVsyncIdEnd; 205 // Used to indicate whether this state has been processed and can be returned to the pool. 206 public boolean mProcessed; 207 } 208 } 209