1 /*
2  * Copyright 2021 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 androidx.metrics.performance
18 
19 import android.app.Activity
20 import android.view.View
21 import androidx.annotation.UiThread
22 
23 /**
24  * This class is used to store information about the state of an application that can be retrieved
25  * later to associate state with performance timing data.
26  *
27  * For example, PerformanceMetricsState is used in conjunction with [JankStats] to enable JankStats
28  * to report per-frame performance characteristics along with the application state that was present
29  * at the time that the frame data was logged.
30  *
31  * There is only one PerformanceMetricsState available per view hierarchy. That instance can be
32  * retrieved from the holder returned by [PerformanceMetricsState.getHolderForHierarchy]. Limiting
33  * PerformanceMetricsState to a single object per hierarchy makes it possible for code outside the
34  * core application logic, such as in a library, to store application state that can be useful for
35  * the application to know about.
36  */
37 class PerformanceMetricsState private constructor() {
38 
39     /**
40      * Data to track UI and user state in this JankStats object.
41      *
42      * @see putState
43      * @see markStateForRemoval
44      */
45     private var states = mutableListOf<StateData>()
46 
47     /**
48      * Temporary per-frame to track UI and user state. Unlike the states tracked in `states`, any
49      * state in this structure is only valid until the next frame, at which point it is cleared. Any
50      * state data added here is automatically removed; there is no matching "remove" method for
51      * [.putSingleFrameState]
52      *
53      * @see putSingleFrameState
54      */
55     private var singleFrameStates = mutableListOf<StateData>()
56 
57     /**
58      * Temporary list to hold states that will be added to for any given frame in addFrameState().
59      * It is used to avoid adding duplicate states by storing all data for states being considered.
60      */
61     private val statesHolder = mutableListOf<StateData>()
62     private val statesToBeCleared = mutableListOf<Int>()
63 
64     /**
65      * StateData objects are stored and retrieved from an object pool, to avoid re-allocating for
66      * new state pairs, since it is expected that most states will share names/states
67      */
68     private val stateDataPool = mutableListOf<StateData>()
69 
addFrameStatenull70     private fun addFrameState(
71         frameStartTime: Long,
72         frameEndTime: Long,
73         frameStates: MutableList<StateInfo>,
74         activeStates: MutableList<StateData>
75     ) {
76         for (i in activeStates.indices.reversed()) {
77             // idea: add state if state was active during this frame
78             // so state start time must be before vsync+duration
79             // also, if state end time < vsync, delete it
80             val item = activeStates[i]
81             if (item.timeRemoved > 0 && item.timeRemoved < frameStartTime) {
82                 // remove states that have already been marked for removal
83                 returnStateDataToPool(activeStates.removeAt(i))
84             } else if (item.timeAdded < frameEndTime) {
85                 // Only add unique state. There may be several states added in
86                 // a given frame (especially during heavy jank periods). Only the
87                 // most recently added should be logged, as it replaces the earlier ones.
88                 statesHolder.add(item)
89                 if (activeStates == singleFrameStates && item.timeRemoved == -1L) {
90                     // This marks a single frame state for removal now that it has logged data.
91                     // It will actually be removed on the next frame, with the removal logic
92                     // above, to give it a chance to log data for multiple listeners on this frame.
93                     item.timeRemoved = frameStartTime
94                 }
95             }
96         }
97         // It's possible to have multiple versions with the same key active on a given
98         // frame. This should result in only using the latest state added, which is what
99         // this block ensures.
100         if (statesHolder.size > 0) {
101             for (i in 0 until statesHolder.size) {
102                 if (i !in statesToBeCleared) {
103                     val item = statesHolder.get(i)
104                     for (j in (i + 1) until statesHolder.size) {
105                         val otherItem = statesHolder.get(j)
106                         if (item.state.key == otherItem.state.key) {
107                             // If state names are the same, remove the one added earlier.
108                             // Note that we are only marking them for removal here since we
109                             // cannot alter the structure while iterating through it.
110                             if (item.timeAdded < otherItem.timeAdded) statesToBeCleared.add(i)
111                             else statesToBeCleared.add(j)
112                         }
113                     }
114                 }
115             }
116             // This block actually removes the duplicate items
117             for (i in statesToBeCleared.size - 1 downTo 0) {
118                 statesHolder.removeAt(statesToBeCleared[i])
119             }
120             // Finally, process all items left in the holder list and add them to frameStates
121             for (i in 0 until statesHolder.size) {
122                 frameStates.add(statesHolder[i].state)
123             }
124             statesHolder.clear()
125             statesToBeCleared.clear()
126         }
127     }
128 
129     /**
130      * This method doesn't actually remove it from the given list of states, but instead logs the
131      * time at which removal was requested. This enables more accurate sync'ing of states with
132      * specific frames, depending on when states are added/removed and when frames start/end. States
133      * will actually be removed from the list later, as they fall out of the current frame start
134      * times and stop being a factor in logging.
135      *
136      * @param key The name used for this state, should match the name used when [putting][putState]
137      *   the state previously.
138      * @param states The list of states to remove this from (either the regular state info or the
139      *   singleFrame info)
140      * @param removalTime The timestamp of this request. This will be used to log the time that this
141      *   state stopped being active, which will be used later to sync states with frame boundaries.
142      */
markStateForRemovalnull143     private fun markStateForRemoval(key: String, states: List<StateData>, removalTime: Long) {
144         synchronized(singleFrameStates) {
145             for (i in 0 until states.size) {
146                 val item = states[i]
147                 if (item.state.key == key && item.timeRemoved < 0) {
148                     item.timeRemoved = removalTime
149                 }
150             }
151         }
152     }
153 
154     /**
155      * Adds information about the state of the application that may be useful in future JankStats
156      * report logs.
157      *
158      * State information can be about UI elements that are currently active (such as the current
159      * [Activity] or layout) or a user interaction like flinging a list. If the
160      * PerformanceMetricsState object already contains an entry with the same key, the old value is
161      * replaced by the new one. Note that this means apps with several instances of similar objects
162      * (such as multipe `RecyclerView`s) should therefore use unique keys for these instances to
163      * avoid clobbering state values for other instances and to provide enough information for later
164      * analysis which allows for disambiguation between these objects. For example, using
165      * "RVHeaders" and "RVContent" might be more helpful than just "RecyclerView" for a messaging
166      * app using `RecyclerView` objects for both a headers list and a list of message contents.
167      *
168      * Some state may be provided automatically by other AndroidX libraries. But applications are
169      * encouraged to add user state specific to those applications to provide more context and more
170      * actionable information in JankStats logs.
171      *
172      * For example, an app that wanted to track jank data about a specific transition in a
173      * picture-gallery view might provide state like this:
174      *
175      * `state.putState("GalleryTransition", "Running")`
176      *
177      * @param key An arbitrary name used for this state, used as a key for storing the state value.
178      * @param value The value of this state.
179      * @see removeState
180      */
putStatenull181     fun putState(key: String, value: String) {
182         synchronized(singleFrameStates) {
183             val nowTime = System.nanoTime()
184             markStateForRemoval(key, states, nowTime)
185             states.add(getStateData(nowTime, -1, StateInfo(key, value)))
186         }
187     }
188 
189     /**
190      * [putSingleFrameState] is like [putState], except the state persists only for the current
191      * frame and will be automatically removed after it is logged for that frame.
192      *
193      * This method can be used for very short-lived state, or state for which it may be difficult to
194      * determine when it should be removed (leading to erroneous data if state is left present long
195      * after it actually stopped happening in the app).
196      *
197      * @param key An arbitrary name used for this state, used as a key for storing the state value.
198      * @param value The value of this state.
199      * @see putState
200      */
putSingleFrameStatenull201     fun putSingleFrameState(key: String, value: String) {
202         synchronized(singleFrameStates) {
203             val nowTime = System.nanoTime()
204             markStateForRemoval(key, singleFrameStates, nowTime)
205             singleFrameStates.add(getStateData(nowTime, -1, StateInfo(key, value)))
206         }
207     }
208 
markStateForRemovalnull209     private fun markStateForRemoval(key: String) {
210         markStateForRemoval(key, states, System.nanoTime())
211     }
212 
removeStateNownull213     internal fun removeStateNow(stateName: String) {
214         synchronized(singleFrameStates) {
215             for (i in 0 until states.size) {
216                 val item = states[i]
217                 if (item.state.key == stateName) {
218                     states.remove(item)
219                     returnStateDataToPool(item)
220                 }
221             }
222         }
223     }
224 
225     /**
226      * Internal representation of state information. timeAdded/Removed allows synchronizing states
227      * with frame boundaries during the FrameMetrics callback, when we can compare which states were
228      * active during any given frame start/end period.
229      */
230     internal class StateData(var timeAdded: Long, var timeRemoved: Long, var state: StateInfo)
231 
getStateDatanull232     internal fun getStateData(timeAdded: Long, timeRemoved: Long, state: StateInfo): StateData {
233         synchronized(stateDataPool) {
234             if (stateDataPool.isEmpty()) {
235                 // This new item will be added to the pool when it is removed, later
236                 return StateData(timeAdded, timeRemoved, state)
237             } else {
238                 val stateData = stateDataPool.removeAt(0)
239                 stateData.timeAdded = timeAdded
240                 stateData.timeRemoved = timeRemoved
241                 stateData.state = state
242                 return stateData
243             }
244         }
245     }
246 
247     /**
248      * Once the StateData is done being used, it can be returned to the pool for later reuse, which
249      * happens in getStateData()
250      */
returnStateDataToPoolnull251     internal fun returnStateDataToPool(stateData: StateData) {
252         synchronized(stateDataPool) {
253             try {
254                 stateDataPool.add(stateData)
255             } catch (e: OutOfMemoryError) {
256                 // App must either be creating more unique states than expected or is having
257                 // unrelated memory pressure. Clear the pool and start over.
258                 stateDataPool.clear()
259                 stateDataPool.add(stateData)
260             }
261         }
262     }
263 
264     /**
265      * Removes information about a specified state.
266      *
267      * [removeState] is typically called when the user stops being in that state, such as leaving a
268      * container previously put in the state, or stopping some interaction that was similarly saved.
269      *
270      * @param key The name used for this state, should match the name used when [putting][putState]
271      *   the state previously.
272      * @see putState
273      */
removeStatenull274     fun removeState(key: String) {
275         markStateForRemoval(key)
276     }
277 
278     /**
279      * Retrieve the states current in the period defined by `startTime` and `endTime`. When a state
280      * is added via [putState] or [putSingleFrameState], the time at which it is added is noted when
281      * storing it. This time is used later in calls to [getIntervalStates] to determine whether that
282      * state was active during the given window of time.
283      *
284      * Note that states are also managed implicitly in this function. Specifically, states added via
285      * [putSingleFrameState] are removed, since they have been used exactly once to retrieve the
286      * state for this interval.
287      */
getIntervalStatesnull288     internal fun getIntervalStates(
289         startTime: Long,
290         endTime: Long,
291         frameStates: MutableList<StateInfo>
292     ) {
293         synchronized(singleFrameStates) {
294             frameStates.clear()
295             addFrameState(startTime, endTime, frameStates, states)
296             addFrameState(startTime, endTime, frameStates, singleFrameStates)
297         }
298     }
299 
cleanupSingleFrameStatesnull300     internal fun cleanupSingleFrameStates() {
301         synchronized(singleFrameStates) {
302             // Remove all states intended for just one frame
303             for (i in singleFrameStates.size - 1 downTo 0) {
304                 // SFStates are marked with timeRemoved during processing so we know when
305                 // they have logged data and can actually be removed
306                 if (singleFrameStates[i].timeRemoved != -1L) {
307                     returnStateDataToPool(singleFrameStates.removeAt(i))
308                 }
309             }
310         }
311     }
312 
313     companion object {
314 
315         /**
316          * This function gets the single PerformanceMetricsState.Holder object for the view
317          * hierarchy in which `view' exists. If there is no such object yet, this function will
318          * create and store one.
319          *
320          * Note that the function will not create a PerformanceMetricsState object if the Holder's
321          * `state` is null; that object is created when a [JankStats] object is created. This is
322          * done to avoid recording performance state if it is not being tracked.
323          *
324          * Note also that this function should only be called with a view that is added to the view
325          * hierarchy, since information about the holder is cached at the root of that hierarchy.
326          * The recommended approach is to set up the holder in
327          * [View.OnAttachStateChangeListener.onViewAttachedToWindow].
328          */
329         @JvmStatic
330         @UiThread
getHolderForHierarchynull331         fun getHolderForHierarchy(view: View): Holder {
332             val rootView = view.getRootView()
333             var metricsStateHolder = rootView.getTag(R.id.metricsStateHolder)
334             if (metricsStateHolder == null) {
335                 metricsStateHolder = Holder()
336                 rootView.setTag(R.id.metricsStateHolder, metricsStateHolder)
337             }
338             return metricsStateHolder as Holder
339         }
340 
341         /**
342          * This function returns the single PerformanceMetricsState.Holder object for the view
343          * hierarchy in which `view' exists. Unlike [getHolderForHierarchy], this function will
344          * create the underlying [PerformanceMetricsState] object if it does not yet exist, and will
345          * set it on the holder object.
346          *
347          * This function exists mainly for internal use by [JankStats]; most callers should use
348          * [getHolderForHierarchy] instead to simply retrieve the existing state information, not to
349          * create it. Creation is reserved for JankStats because there is no sense storing state
350          * information if it is not being tracked by JankStats.
351          */
352         @JvmStatic
353         @UiThread
createnull354         internal fun create(view: View): Holder {
355             val holder = getHolderForHierarchy(view)
356             if (holder.state == null) {
357                 holder.state = PerformanceMetricsState()
358             }
359             return holder
360         }
361     }
362 
363     /**
364      * This class holds the current [PerformanceMetricsState] for a given view hierarchy. Callers
365      * should request the holder for a hierarchy via [getHolderForHierarchy], and check the value of
366      * the [state] property to see whether state is being tracked by JankStats for the hierarchy.
367      */
368     class Holder internal constructor() {
369 
370         /**
371          * The current PerformanceMetricsState for the view hierarchy where this Holder object was
372          * retrieved. A null value indicates that state is not currently being tracked (or stored).
373          */
374         var state: PerformanceMetricsState? = null
375             internal set
376     }
377 }
378