• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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