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