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