• 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.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 }