• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2023 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base.jank_tracker;
6 
7 import org.chromium.base.ThreadUtils.ThreadChecker;
8 import org.chromium.base.TimeUtils;
9 import org.chromium.base.TraceEvent;
10 import org.chromium.build.BuildConfig;
11 
12 import java.util.ArrayList;
13 import java.util.Collections;
14 import java.util.HashMap;
15 import java.util.List;
16 
17 /**
18  * This class stores relevant metrics from FrameMetrics between the calls to UMA reporting methods.
19  */
20 public class FrameMetricsStore {
21     // FrameMetricsStore can only be accessed on the handler thread (from the
22     // JankReportingScheduler.getOrCreateHandler() method). However construction occurs on a
23     // separate thread so the ThreadChecker is instead constructed later.
24     private ThreadChecker mThreadChecker;
25     // An arbitrary value from which to create a trace event async track. The only risk if this
26     // clashes with another track is trace events will show up on both potentially looking weird in
27     // the tracing UI. No other issue will occur.
28     private static final long TRACE_EVENT_TRACK_ID = 84186319646187624L;
29     // Android FrameMetrics promises in order frame metrics so this is just the latest timestamp.
30     private long mMaxTimestamp = -1;
31     // Array of timestamps stored in nanoseconds, they represent the moment when each frame
32     // began (VSYNC_TIMESTAMP), must always be the same size as mTotalDurationsNs.
33     private final ArrayList<Long> mTimestampsNs = new ArrayList<>();
34     // Array of total durations stored in nanoseconds, they represent how long each frame took to
35     // draw.
36     private final ArrayList<Long> mTotalDurationsNs = new ArrayList<>();
37     // Array of integers denoting number of vsyncs we missed for given frame. 0 missed vsyncs mean
38     // no jank, while >0 missed vsyncs mean the frame was janky. Must always be the same size as
39     // mTotalDurationsNs.
40     private final ArrayList<Integer> mNumMissedVsyncs = new ArrayList<>();
41     // Stores the timestamp (nanoseconds) of the most recent frame metric as a scenario started.
42     // Zero if no FrameMetrics have been received.
43     private final HashMap<Integer, Long> mScenarioPreviousFrameTimestampNs = new HashMap<>();
44     private final HashMap<Integer, Long> mPendingStartTimestampNs = new HashMap<>();
45 
FrameMetricsStore()46     public FrameMetricsStore() {
47         // Add 0 to mTimestampNS array. This simplifies handling edge case when starting a scenario
48         // and we don't have any frame metrics stored. Adding 0 also makes sure the array stays in
49         // sorted order since the actual metrics received will have larger vsync start timestamps.
50         mTimestampsNs.add(0L);
51         // Add arbitrary values to related arrays as well since we always want them to be of same
52         // size.
53         mTotalDurationsNs.add(0L);
54         mNumMissedVsyncs.add(0);
55     }
56 
57     // Convert an enum value to string to use as an UMA histogram name, changes to strings should be
58     // reflected in android/histograms.xml and base/android/jank_
scenarioToString(@ankScenario int scenario)59     public static String scenarioToString(@JankScenario int scenario) {
60         switch (scenario) {
61             case JankScenario.PERIODIC_REPORTING:
62                 return "Total";
63             case JankScenario.OMNIBOX_FOCUS:
64                 return "OmniboxFocus";
65             case JankScenario.NEW_TAB_PAGE:
66                 return "NewTabPage";
67             case JankScenario.STARTUP:
68                 return "Startup";
69             case JankScenario.TAB_SWITCHER:
70                 return "TabSwitcher";
71             case JankScenario.OPEN_LINK_IN_NEW_TAB:
72                 return "OpenLinkInNewTab";
73             case JankScenario.START_SURFACE_HOMEPAGE:
74                 return "StartSurfaceHomepage";
75             case JankScenario.START_SURFACE_TAB_SWITCHER:
76                 return "StartSurfaceTabSwitcher";
77             case JankScenario.FEED_SCROLLING:
78                 return "FeedScrolling";
79             case JankScenario.WEBVIEW_SCROLLING:
80                 return "WebviewScrolling";
81             default:
82                 throw new IllegalArgumentException("Invalid scenario value");
83         }
84     }
85 
86     /**
87      * initialize is the first entry point that is on the HandlerThread, so set up our thread
88      * checking.
89      */
initialize()90     void initialize() {
91         mThreadChecker = new ThreadChecker();
92     }
93 
94     /** Records the total draw duration and jankiness for a single frame. */
addFrameMeasurement(long totalDurationNs, int numMissedVsyncs, long frameStartVsyncTs)95     void addFrameMeasurement(long totalDurationNs, int numMissedVsyncs, long frameStartVsyncTs) {
96         mThreadChecker.assertOnValidThread();
97         mTotalDurationsNs.add(totalDurationNs);
98         mNumMissedVsyncs.add(numMissedVsyncs);
99         mTimestampsNs.add(frameStartVsyncTs);
100         mMaxTimestamp = frameStartVsyncTs;
101     }
102 
103     @SuppressWarnings("NoDynamicStringsInTraceEventCheck")
startTrackingScenario(@ankScenario int scenario)104     void startTrackingScenario(@JankScenario int scenario) {
105         try (TraceEvent e =
106                 TraceEvent.scoped("startTrackingScenario: " + scenarioToString(scenario))) {
107             mThreadChecker.assertOnValidThread();
108             // Ignore multiple calls to startTrackingScenario without corresponding
109             // stopTrackingScenario calls.
110             if (mScenarioPreviousFrameTimestampNs.containsKey(scenario)) {
111                 mPendingStartTimestampNs.put(
112                         scenario, TimeUtils.uptimeMillis() * TimeUtils.NANOSECONDS_PER_MILLISECOND);
113                 return;
114             }
115             // Make a unique ID for each scenario for tracing.
116             TraceEvent.startAsync(
117                     "JankCUJ:" + scenarioToString(scenario), TRACE_EVENT_TRACK_ID + scenario);
118             // Scenarios are tracked based on the latest stored timestamp to allow fast lookups
119             // (find index of [timestamp] vs find first index that's >= [timestamp]).
120             Long startingTimestamp = mTimestampsNs.get(mTimestampsNs.size() - 1);
121             mScenarioPreviousFrameTimestampNs.put(scenario, startingTimestamp);
122         }
123     }
124 
hasReceivedMetricsPast(long endScenarioTimeNs)125     boolean hasReceivedMetricsPast(long endScenarioTimeNs) {
126         mThreadChecker.assertOnValidThread();
127         return mMaxTimestamp > endScenarioTimeNs;
128     }
129 
stopTrackingScenario(@ankScenario int scenario)130     JankMetrics stopTrackingScenario(@JankScenario int scenario) {
131         return stopTrackingScenario(scenario, -1);
132     }
133 
134     // The string added is a static string.
135     @SuppressWarnings("NoDynamicStringsInTraceEventCheck")
stopTrackingScenario(@ankScenario int scenario, long endScenarioTimeNs)136     JankMetrics stopTrackingScenario(@JankScenario int scenario, long endScenarioTimeNs) {
137         try (TraceEvent e =
138                 TraceEvent.scoped(
139                         "finishTrackingScenario: " + scenarioToString(scenario),
140                         Long.toString(endScenarioTimeNs))) {
141             mThreadChecker.assertOnValidThread();
142             TraceEvent.finishAsync(
143                     "JankCUJ:" + scenarioToString(scenario), TRACE_EVENT_TRACK_ID + scenario);
144             // Get the timestamp of the latest frame before startTrackingScenario was called. This
145             // can be null if tracking never started for scenario, or 0L if tracking started when no
146             // frames were stored.
147             Long previousFrameTimestamp = mScenarioPreviousFrameTimestampNs.remove(scenario);
148 
149             // If stopTrackingScenario is called without a corresponding startTrackingScenario then
150             // return an empty FrameMetrics object.
151             if (previousFrameTimestamp == null) {
152                 removeUnusedFrames();
153                 return new JankMetrics();
154             }
155 
156             int startingIndex = mTimestampsNs.indexOf(previousFrameTimestamp);
157             // The scenario starts with the frame after the tracking timestamp.
158             startingIndex++;
159 
160             // If startingIndex is out of bounds then we haven't recorded any frames since
161             // tracking started, return an empty FrameMetrics object.
162             if (startingIndex >= mTimestampsNs.size()) {
163                 return new JankMetrics();
164             }
165 
166             // Ending index is exclusive, so this is not out of bounds.
167             int endingIndex = mTimestampsNs.size();
168             if (endScenarioTimeNs > 0) {
169                 // binarySearch returns index of the search key (non-negative value) or (-(insertion
170                 // point) - 1).
171                 // The insertion point is defined as the index of the first element greater than the
172                 // key, or a.length if all elements in the array are less than the specified key.
173                 endingIndex = Collections.binarySearch(mTimestampsNs, endScenarioTimeNs);
174                 if (endingIndex < 0) {
175                     endingIndex = -1 * (endingIndex + 1);
176                 } else {
177                     endingIndex = Math.min(endingIndex + 1, mTimestampsNs.size());
178                 }
179                 if (endingIndex <= startingIndex) {
180                     // Something went wrong reset
181                     TraceEvent.instant("FrameMetricsStore invalid endScenarioTimeNs");
182                     endingIndex = mTimestampsNs.size();
183                 }
184             }
185 
186             JankMetrics jankMetrics =
187                     convertArraysToJankMetrics(
188                             mTimestampsNs.subList(startingIndex, endingIndex),
189                             mTotalDurationsNs.subList(startingIndex, endingIndex),
190                             mNumMissedVsyncs.subList(startingIndex, endingIndex));
191             removeUnusedFrames();
192 
193             Long pendingStartTimestampNs = mPendingStartTimestampNs.remove(scenario);
194             if (pendingStartTimestampNs != null && pendingStartTimestampNs > endScenarioTimeNs) {
195                 startTrackingScenario(scenario);
196             }
197             return jankMetrics;
198         }
199     }
200 
removeUnusedFrames()201     private void removeUnusedFrames() {
202         if (mScenarioPreviousFrameTimestampNs.isEmpty()) {
203             TraceEvent.instant("removeUnusedFrames", Long.toString(mTimestampsNs.size()));
204             mTimestampsNs.subList(1, mTimestampsNs.size()).clear();
205             mTotalDurationsNs.subList(1, mTotalDurationsNs.size()).clear();
206             mNumMissedVsyncs.subList(1, mNumMissedVsyncs.size()).clear();
207             return;
208         }
209 
210         long firstUsedTimestamp = findFirstUsedTimestamp();
211         // If the earliest timestamp tracked is 0 then that scenario contains every frame
212         // stored, so we shouldn't delete anything.
213         if (firstUsedTimestamp == 0L) {
214             return;
215         }
216 
217         int firstUsedIndex = mTimestampsNs.indexOf(firstUsedTimestamp);
218         if (firstUsedIndex == -1) {
219             if (BuildConfig.ENABLE_ASSERTS) {
220                 throw new IllegalStateException("Timestamp for tracked scenario not found");
221             }
222             // This shouldn't happen.
223             return;
224         }
225         TraceEvent.instant("removeUnusedFrames", Long.toString(firstUsedIndex));
226 
227         mTimestampsNs.subList(1, firstUsedIndex).clear();
228         mTotalDurationsNs.subList(1, firstUsedIndex).clear();
229         mNumMissedVsyncs.subList(1, firstUsedIndex).clear();
230     }
231 
findFirstUsedTimestamp()232     private long findFirstUsedTimestamp() {
233         long firstTimestamp = Long.MAX_VALUE;
234         for (long timestamp : mScenarioPreviousFrameTimestampNs.values()) {
235             if (timestamp < firstTimestamp) {
236                 firstTimestamp = timestamp;
237             }
238         }
239 
240         return firstTimestamp;
241     }
242 
convertArraysToJankMetrics( List<Long> longTimestampsNs, List<Long> longDurations, List<Integer> intNumMissedVsyncs)243     private JankMetrics convertArraysToJankMetrics(
244             List<Long> longTimestampsNs,
245             List<Long> longDurations,
246             List<Integer> intNumMissedVsyncs) {
247         long[] timestamps = new long[longTimestampsNs.size()];
248         for (int i = 0; i < longTimestampsNs.size(); i++) {
249             timestamps[i] = longTimestampsNs.get(i).longValue();
250         }
251 
252         long[] durations = new long[longDurations.size()];
253         for (int i = 0; i < longDurations.size(); i++) {
254             durations[i] = longDurations.get(i).longValue();
255         }
256 
257         int[] numMissedVsyncs = new int[intNumMissedVsyncs.size()];
258         for (int i = 0; i < intNumMissedVsyncs.size(); i++) {
259             numMissedVsyncs[i] = intNumMissedVsyncs.get(i).intValue();
260         }
261 
262         JankMetrics jankMetrics = new JankMetrics(timestamps, durations, numMissedVsyncs);
263         return jankMetrics;
264     }
265 }
266