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.app.jank.StateTracker.StateData; 22 import android.util.Log; 23 import android.util.Pools.SimplePool; 24 import android.view.SurfaceControl.JankData; 25 26 import androidx.annotation.VisibleForTesting; 27 28 import com.android.internal.util.FrameworkStatsLog; 29 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.List; 33 34 /** 35 * This class is responsible for associating frames received from SurfaceFlinger to active widget 36 * states and logging those states back to the platform. 37 * 38 * @hide 39 */ 40 @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) 41 public class JankDataProcessor { 42 private static final String TAG = "JankDataProcessor"; 43 private static final boolean DEBUG_LOGGING = false; 44 private static final int MAX_IN_MEMORY_STATS = 25; 45 private static final int LOG_BATCH_FREQUENCY = 50; 46 private int mCurrentBatchCount = 0; 47 private StateTracker mStateTracker = null; 48 private ArrayList<StateData> mPendingStates = new ArrayList<>(); 49 private SimplePool<PendingJankStat> mPendingJankStatsPool = 50 new SimplePool<>(MAX_IN_MEMORY_STATS); 51 private HashMap<String, PendingJankStat> mPendingJankStats = new HashMap<>(); 52 JankDataProcessor(@onNull StateTracker stateTracker)53 public JankDataProcessor(@NonNull StateTracker stateTracker) { 54 mStateTracker = stateTracker; 55 } 56 57 /** 58 * Called once per batch of JankData. 59 * 60 * @param jankData data received from SurfaceFlinger to be processed 61 * @param activityName name of the activity that is tracking jank metrics. 62 * @param appUid the uid of the app. 63 */ processJankData(List<JankData> jankData, String activityName, int appUid)64 public void processJankData(List<JankData> jankData, String activityName, int appUid) { 65 // add all the previous and active states to the pending states list. 66 mStateTracker.retrieveAllStates(mPendingStates); 67 68 // TODO b/376332122 Look to see if this logic can be optimized. 69 for (int i = 0; i < jankData.size(); i++) { 70 JankData frame = jankData.get(i); 71 // for each frame we need to check if the state was active during that time. 72 for (int j = 0; j < mPendingStates.size(); j++) { 73 StateData pendingState = mPendingStates.get(j); 74 // This state was active during the frame 75 if (frame.getVsyncId() >= pendingState.mVsyncIdStart 76 && frame.getVsyncId() <= pendingState.mVsyncIdEnd) { 77 recordFrameCount(frame, pendingState, activityName, appUid); 78 79 pendingState.mProcessed = true; 80 } 81 } 82 } 83 // At this point we have attributed all frames to a state. 84 incrementBatchCountAndMaybeLogStats(); 85 86 // return the StatData object back to the pool to be reused. 87 jankDataProcessingComplete(); 88 } 89 90 /** 91 * Merges app jank stats reported by components outside the platform to the current pending 92 * stats 93 */ mergeJankStats(AppJankStats jankStats, String activityName)94 public void mergeJankStats(AppJankStats jankStats, String activityName) { 95 // Each state has a key which is a combination of widget category, widget id and widget 96 // state, this key is also used to identify pending stats, a pending stat is essentially a 97 // state with frames associated with it. 98 String stateKey = mStateTracker.getStateKey(jankStats.getWidgetCategory(), 99 jankStats.getWidgetId(), jankStats.getWidgetState()); 100 101 if (mPendingJankStats.containsKey(stateKey)) { 102 mergeExistingStat(stateKey, jankStats); 103 } else { 104 mergeNewStat(stateKey, activityName, jankStats); 105 } 106 107 incrementBatchCountAndMaybeLogStats(); 108 } 109 mergeExistingStat(String stateKey, AppJankStats jankStat)110 private void mergeExistingStat(String stateKey, AppJankStats jankStat) { 111 PendingJankStat pendingStat = mPendingJankStats.get(stateKey); 112 113 pendingStat.mJankyFrames += jankStat.getJankyFrameCount(); 114 pendingStat.mTotalFrames += jankStat.getTotalFrameCount(); 115 116 mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets, 117 jankStat.getRelativeFrameTimeHistogram().getBucketCounters()); 118 } 119 mergeNewStat(String stateKey, String activityName, AppJankStats jankStats)120 private void mergeNewStat(String stateKey, String activityName, AppJankStats jankStats) { 121 // Check if we have space for a new stat 122 if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) { 123 return; 124 } 125 126 PendingJankStat pendingStat = mPendingJankStatsPool.acquire(); 127 if (pendingStat == null) { 128 pendingStat = new PendingJankStat(); 129 130 } 131 pendingStat.clearStats(); 132 133 pendingStat.mActivityName = activityName; 134 pendingStat.mUid = jankStats.getUid(); 135 pendingStat.mWidgetId = jankStats.getWidgetId(); 136 pendingStat.mWidgetCategory = jankStats.getWidgetCategory(); 137 pendingStat.mWidgetState = jankStats.getWidgetState(); 138 pendingStat.mTotalFrames = jankStats.getTotalFrameCount(); 139 pendingStat.mJankyFrames = jankStats.getJankyFrameCount(); 140 141 mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets, 142 jankStats.getRelativeFrameTimeHistogram().getBucketCounters()); 143 144 mPendingJankStats.put(stateKey, pendingStat); 145 } 146 mergeOverrunHistograms(int[] mergeTarget, int[] mergeSource)147 private void mergeOverrunHistograms(int[] mergeTarget, int[] mergeSource) { 148 // The length of each histogram should be identical, if they are not then its possible the 149 // buckets are not in sync, these records should not be recorded. 150 if (mergeTarget.length != mergeSource.length) return; 151 152 for (int i = 0; i < mergeTarget.length; i++) { 153 mergeTarget[i] += mergeSource[i]; 154 } 155 } 156 incrementBatchCountAndMaybeLogStats()157 private void incrementBatchCountAndMaybeLogStats() { 158 mCurrentBatchCount++; 159 if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) { 160 logMetricCounts(); 161 } 162 } 163 164 /** 165 * Returns the aggregate map of different pending jank stats. 166 */ 167 @VisibleForTesting getPendingJankStats()168 public HashMap<String, PendingJankStat> getPendingJankStats() { 169 return mPendingJankStats; 170 } 171 jankDataProcessingComplete()172 private void jankDataProcessingComplete() { 173 mStateTracker.stateProcessingComplete(); 174 mPendingStates.clear(); 175 } 176 177 /** 178 * Determine if frame is Janky and add to existing memory counter or create a new one. 179 */ recordFrameCount(JankData frameData, StateData stateData, String activityName, int appUid)180 private void recordFrameCount(JankData frameData, StateData stateData, String activityName, 181 int appUid) { 182 // Check if we have an existing Jank state 183 PendingJankStat jankStats = mPendingJankStats.get(stateData.mStateDataKey); 184 185 if (jankStats == null) { 186 // Check if we have space for another pending stat 187 if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) { 188 return; 189 } 190 191 jankStats = mPendingJankStatsPool.acquire(); 192 if (jankStats == null) { 193 jankStats = new PendingJankStat(); 194 } 195 jankStats.clearStats(); 196 jankStats.mActivityName = activityName; 197 jankStats.mUid = appUid; 198 mPendingJankStats.put(stateData.mStateDataKey, jankStats); 199 } 200 // This state has already been accounted for 201 if (jankStats.processedVsyncId == frameData.getVsyncId()) return; 202 203 jankStats.mTotalFrames += 1; 204 if ((frameData.getJankType() & JankData.JANK_APPLICATION) != 0) { 205 jankStats.mJankyFrames += 1; 206 } 207 jankStats.recordFrameOverrun(frameData.getActualAppFrameTimeNanos()); 208 jankStats.processedVsyncId = frameData.getVsyncId(); 209 210 } 211 212 /** 213 * When called will log pending Jank stats currently stored in memory to the platform. Will not 214 * clear any pending widget states. 215 */ logMetricCounts()216 public void logMetricCounts() { 217 try { 218 mPendingJankStats.values().forEach(stat -> { 219 FrameworkStatsLog.write( 220 FrameworkStatsLog.JANK_FRAME_COUNT_BY_WIDGET_REPORTED, 221 /*app uid*/ stat.getUid(), 222 /*activity name*/ stat.getActivityName(), 223 /*widget id*/ stat.getWidgetId(), 224 /*refresh rate*/ stat.getRefreshRate(), 225 /*widget category*/ widgetCategoryToInt(stat.getWidgetCategory()), 226 /*widget state*/ widgetStateToInt(stat.getWidgetState()), 227 /*total frames*/ stat.getTotalFrames(), 228 /*janky frames*/ stat.getJankyFrames(), 229 /*histogram*/ stat.getFrameOverrunBuckets()); 230 Log.d(stat.mActivityName, stat.toString()); 231 // return the pending stat to the pool it will be reset the next time its 232 // used. 233 mPendingJankStatsPool.release(stat); 234 235 } 236 ); 237 // All stats have been recorded and added back to the pool for reuse, clear the pending 238 // stats. 239 mPendingJankStats.clear(); 240 mCurrentBatchCount = 0; 241 } catch (Exception exception) { 242 // TODO b/374608358 handle logging exceptions. 243 } 244 } 245 widgetCategoryToInt(String widgetCategory)246 private int widgetCategoryToInt(String widgetCategory) { 247 switch (widgetCategory) { 248 case AppJankStats.WIDGET_CATEGORY_SCROLL -> { 249 return FrameworkStatsLog 250 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__SCROLLING; 251 } 252 case AppJankStats.WIDGET_CATEGORY_ANIMATION -> { 253 return FrameworkStatsLog 254 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__ANIMATION; 255 } 256 case AppJankStats.WIDGET_CATEGORY_MEDIA -> { 257 return FrameworkStatsLog 258 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__MEDIA; 259 } 260 case AppJankStats.WIDGET_CATEGORY_NAVIGATION -> { 261 return FrameworkStatsLog 262 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__NAVIGATION; 263 } 264 case AppJankStats.WIDGET_CATEGORY_KEYBOARD -> { 265 return FrameworkStatsLog 266 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__KEYBOARD; 267 } 268 case AppJankStats.WIDGET_CATEGORY_OTHER -> { 269 return FrameworkStatsLog 270 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__OTHER; 271 } 272 default -> { 273 if (DEBUG_LOGGING) { 274 Log.d(TAG, "Default Category Logged: " 275 + AppJankStats.WIDGET_CATEGORY_UNSPECIFIED); 276 } 277 return FrameworkStatsLog 278 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_TYPE__WIDGET_CATEGORY_UNSPECIFIED; 279 } 280 } 281 } 282 widgetStateToInt(String widgetState)283 private int widgetStateToInt(String widgetState) { 284 switch (widgetState) { 285 case AppJankStats.WIDGET_STATE_NONE -> { 286 return FrameworkStatsLog 287 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__NONE; 288 } 289 case AppJankStats.WIDGET_STATE_SCROLLING -> { 290 return FrameworkStatsLog 291 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__SCROLLING; 292 } 293 case AppJankStats.WIDGET_STATE_FLINGING -> { 294 return FrameworkStatsLog 295 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__FLINGING; 296 } 297 case AppJankStats.WIDGET_STATE_SWIPING -> { 298 return FrameworkStatsLog 299 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__SWIPING; 300 } 301 case AppJankStats.WIDGET_STATE_DRAGGING -> { 302 return FrameworkStatsLog 303 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__DRAGGING; 304 } 305 case AppJankStats.WIDGET_STATE_ZOOMING -> { 306 return FrameworkStatsLog 307 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__ZOOMING; 308 } 309 case AppJankStats.WIDGET_STATE_ANIMATING -> { 310 return FrameworkStatsLog 311 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__ANIMATING; 312 } 313 case AppJankStats.WIDGET_STATE_PLAYBACK -> { 314 return FrameworkStatsLog 315 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__PLAYBACK; 316 } 317 case AppJankStats.WIDGET_STATE_TAPPING -> { 318 return FrameworkStatsLog 319 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__TAPPING; 320 } 321 case AppJankStats.WIDGET_STATE_PREDICTIVE_BACK -> { 322 return FrameworkStatsLog 323 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__PREDICTIVE_BACK; 324 } 325 default -> { 326 if (DEBUG_LOGGING) { 327 Log.d(TAG, "Default State Logged: " 328 + AppJankStats.WIDGET_STATE_UNSPECIFIED); 329 } 330 return FrameworkStatsLog 331 .JANK_FRAME_COUNT_BY_WIDGET_REPORTED__WIDGET_STATE__WIDGET_STATE_UNSPECIFIED; 332 } 333 } 334 } 335 336 public static final class PendingJankStat { 337 private static final int NANOS_PER_MS = 1000000; 338 public long processedVsyncId = -1; 339 340 // UID of the app 341 private int mUid; 342 343 // The name of the activity that is currently collecting frame metrics. 344 private String mActivityName; 345 346 // The id that has been set for the widget. 347 private String mWidgetId; 348 349 // A general category that the widget applies to. 350 private String mWidgetCategory; 351 352 // The states that the UI elements can report 353 private String mWidgetState; 354 355 // The number of frames reported during this state. 356 private long mTotalFrames; 357 358 // Total number of frames determined to be janky during the reported state. 359 private long mJankyFrames; 360 361 private int mRefreshRate; 362 363 private static final int[] sFrameOverrunHistogramBounds = { 364 Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, 365 -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, 366 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 367 Integer.MAX_VALUE 368 }; 369 private final int[] mFrameOverrunBuckets = new int[sFrameOverrunHistogramBounds.length - 1]; 370 371 // Histogram of frame duration overruns encoded in predetermined buckets. PendingJankStat()372 public PendingJankStat() { 373 } 374 getProcessedVsyncId()375 public long getProcessedVsyncId() { 376 return processedVsyncId; 377 } 378 setProcessedVsyncId(long processedVsyncId)379 public void setProcessedVsyncId(long processedVsyncId) { 380 this.processedVsyncId = processedVsyncId; 381 } 382 getUid()383 public int getUid() { 384 return mUid; 385 } 386 setUid(int uid)387 public void setUid(int uid) { 388 mUid = uid; 389 } 390 getActivityName()391 public String getActivityName() { 392 return mActivityName; 393 } 394 setActivityName(String activityName)395 public void setActivityName(String activityName) { 396 mActivityName = activityName; 397 } 398 getWidgetId()399 public String getWidgetId() { 400 return mWidgetId; 401 } 402 setWidgetId(String widgetId)403 public void setWidgetId(String widgetId) { 404 mWidgetId = widgetId; 405 } 406 getWidgetCategory()407 public String getWidgetCategory() { 408 return mWidgetCategory; 409 } 410 setWidgetCategory(String widgetCategory)411 public void setWidgetCategory(String widgetCategory) { 412 mWidgetCategory = widgetCategory; 413 } 414 getWidgetState()415 public String getWidgetState() { 416 return mWidgetState; 417 } 418 setWidgetState(String widgetState)419 public void setWidgetState(String widgetState) { 420 mWidgetState = widgetState; 421 } 422 getTotalFrames()423 public long getTotalFrames() { 424 return mTotalFrames; 425 } 426 setTotalFrames(long totalFrames)427 public void setTotalFrames(long totalFrames) { 428 mTotalFrames = totalFrames; 429 } 430 getJankyFrames()431 public long getJankyFrames() { 432 return mJankyFrames; 433 } 434 setJankyFrames(long jankyFrames)435 public void setJankyFrames(long jankyFrames) { 436 mJankyFrames = jankyFrames; 437 } 438 getFrameOverrunBuckets()439 public int[] getFrameOverrunBuckets() { 440 return mFrameOverrunBuckets; 441 } 442 getRefreshRate()443 public int getRefreshRate() { 444 return mRefreshRate; 445 } 446 setRefreshRate(int refreshRate)447 public void setRefreshRate(int refreshRate) { 448 mRefreshRate = refreshRate; 449 } 450 451 /** 452 * Will convert the frame time from ns to ms and record how long the frame took to render. 453 */ recordFrameOverrun(long frameTimeNano)454 public void recordFrameOverrun(long frameTimeNano) { 455 try { 456 // TODO b/375650163 calculate frame overrun from refresh rate. 457 int frameTimeMillis = (int) frameTimeNano / NANOS_PER_MS; 458 mFrameOverrunBuckets[indexForFrameOverrun(frameTimeMillis)]++; 459 } catch (IndexOutOfBoundsException exception) { 460 // TODO b/375650163 figure out how to handle this if it happens. 461 } 462 } 463 464 /** 465 * resets all fields in the object back to defaults. 466 */ clearStats()467 public void clearStats() { 468 this.mUid = -1; 469 this.mActivityName = ""; 470 this.processedVsyncId = -1; 471 this.mJankyFrames = 0; 472 this.mTotalFrames = 0; 473 this.mWidgetCategory = ""; 474 this.mWidgetState = ""; 475 this.mRefreshRate = 0; 476 clearHistogram(); 477 } 478 clearHistogram()479 private void clearHistogram() { 480 for (int i = 0; i < mFrameOverrunBuckets.length; i++) { 481 mFrameOverrunBuckets[i] = 0; 482 } 483 } 484 485 // This takes the overrun time and returns what bucket it belongs to in the histogram. indexForFrameOverrun(int overrunTime)486 private int indexForFrameOverrun(int overrunTime) { 487 if (overrunTime < 20) { 488 if (overrunTime >= -20) { 489 return (overrunTime + 20) / 2 + 12; 490 } 491 if (overrunTime >= -30) { 492 return (overrunTime + 30) / 5 + 10; 493 } 494 if (overrunTime >= -100) { 495 return (overrunTime + 100) / 10 + 3; 496 } 497 if (overrunTime >= -200) { 498 return (overrunTime + 200) / 50 + 1; 499 } 500 return 0; 501 } 502 if (overrunTime < 30) { 503 return (overrunTime - 20) / 5 + 32; 504 } 505 if (overrunTime < 100) { 506 return (overrunTime - 30) / 10 + 34; 507 } 508 if (overrunTime < 200) { 509 return (overrunTime - 50) / 100 + 41; 510 } 511 if (overrunTime <= 1000) { 512 return (overrunTime - 200) / 100 + 43; 513 } 514 return mFrameOverrunBuckets.length - 1; 515 } 516 517 } 518 }