| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.base.jank_tracker; |
| |
| import org.chromium.base.ThreadUtils.ThreadChecker; |
| import org.chromium.base.TimeUtils; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.build.BuildConfig; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * This class stores relevant metrics from FrameMetrics between the calls to UMA reporting methods. |
| */ |
| public class FrameMetricsStore { |
| // FrameMetricsStore can only be accessed on the handler thread (from the |
| // JankReportingScheduler.getOrCreateHandler() method). However construction occurs on a |
| // separate thread so the ThreadChecker is instead constructed later. |
| private ThreadChecker mThreadChecker; |
| // An arbitrary value from which to create a trace event async track. The only risk if this |
| // clashes with another track is trace events will show up on both potentially looking weird in |
| // the tracing UI. No other issue will occur. |
| private static final long TRACE_EVENT_TRACK_ID = 84186319646187624L; |
| // Android FrameMetrics promises in order frame metrics so this is just the latest timestamp. |
| private long mMaxTimestamp = -1; |
| // Array of timestamps stored in nanoseconds, they represent the moment when each frame |
| // began (VSYNC_TIMESTAMP), must always be the same size as mTotalDurationsNs. |
| private final ArrayList<Long> mTimestampsNs = new ArrayList<>(); |
| // Array of total durations stored in nanoseconds, they represent how long each frame took to |
| // draw. |
| private final ArrayList<Long> mTotalDurationsNs = new ArrayList<>(); |
| // Array of integers denoting number of vsyncs we missed for given frame. 0 missed vsyncs mean |
| // no jank, while >0 missed vsyncs mean the frame was janky. Must always be the same size as |
| // mTotalDurationsNs. |
| private final ArrayList<Integer> mNumMissedVsyncs = new ArrayList<>(); |
| // Stores the timestamp (nanoseconds) of the most recent frame metric as a scenario started. |
| // Zero if no FrameMetrics have been received. |
| private final HashMap<Integer, Long> mScenarioPreviousFrameTimestampNs = new HashMap<>(); |
| private final HashMap<Integer, Long> mPendingStartTimestampNs = new HashMap<>(); |
| |
| public FrameMetricsStore() { |
| // Add 0 to mTimestampNS array. This simplifies handling edge case when starting a scenario |
| // and we don't have any frame metrics stored. Adding 0 also makes sure the array stays in |
| // sorted order since the actual metrics received will have larger vsync start timestamps. |
| mTimestampsNs.add(0L); |
| // Add arbitrary values to related arrays as well since we always want them to be of same |
| // size. |
| mTotalDurationsNs.add(0L); |
| mNumMissedVsyncs.add(0); |
| } |
| |
| // Convert an enum value to string to use as an UMA histogram name, changes to strings should be |
| // reflected in android/histograms.xml and base/android/jank_ |
| public static String scenarioToString(@JankScenario int scenario) { |
| switch (scenario) { |
| case JankScenario.PERIODIC_REPORTING: |
| return "Total"; |
| case JankScenario.OMNIBOX_FOCUS: |
| return "OmniboxFocus"; |
| case JankScenario.NEW_TAB_PAGE: |
| return "NewTabPage"; |
| case JankScenario.STARTUP: |
| return "Startup"; |
| case JankScenario.TAB_SWITCHER: |
| return "TabSwitcher"; |
| case JankScenario.OPEN_LINK_IN_NEW_TAB: |
| return "OpenLinkInNewTab"; |
| case JankScenario.START_SURFACE_HOMEPAGE: |
| return "StartSurfaceHomepage"; |
| case JankScenario.START_SURFACE_TAB_SWITCHER: |
| return "StartSurfaceTabSwitcher"; |
| case JankScenario.FEED_SCROLLING: |
| return "FeedScrolling"; |
| case JankScenario.WEBVIEW_SCROLLING: |
| return "WebviewScrolling"; |
| default: |
| throw new IllegalArgumentException("Invalid scenario value"); |
| } |
| } |
| |
| /** |
| * initialize is the first entry point that is on the HandlerThread, so set up our thread |
| * checking. |
| */ |
| void initialize() { |
| mThreadChecker = new ThreadChecker(); |
| } |
| |
| /** Records the total draw duration and jankiness for a single frame. */ |
| void addFrameMeasurement(long totalDurationNs, int numMissedVsyncs, long frameStartVsyncTs) { |
| mThreadChecker.assertOnValidThread(); |
| mTotalDurationsNs.add(totalDurationNs); |
| mNumMissedVsyncs.add(numMissedVsyncs); |
| mTimestampsNs.add(frameStartVsyncTs); |
| mMaxTimestamp = frameStartVsyncTs; |
| } |
| |
| @SuppressWarnings("NoDynamicStringsInTraceEventCheck") |
| void startTrackingScenario(@JankScenario int scenario) { |
| try (TraceEvent e = |
| TraceEvent.scoped("startTrackingScenario: " + scenarioToString(scenario))) { |
| mThreadChecker.assertOnValidThread(); |
| // Ignore multiple calls to startTrackingScenario without corresponding |
| // stopTrackingScenario calls. |
| if (mScenarioPreviousFrameTimestampNs.containsKey(scenario)) { |
| mPendingStartTimestampNs.put( |
| scenario, TimeUtils.uptimeMillis() * TimeUtils.NANOSECONDS_PER_MILLISECOND); |
| return; |
| } |
| // Make a unique ID for each scenario for tracing. |
| TraceEvent.startAsync( |
| "JankCUJ:" + scenarioToString(scenario), TRACE_EVENT_TRACK_ID + scenario); |
| // Scenarios are tracked based on the latest stored timestamp to allow fast lookups |
| // (find index of [timestamp] vs find first index that's >= [timestamp]). |
| Long startingTimestamp = mTimestampsNs.get(mTimestampsNs.size() - 1); |
| mScenarioPreviousFrameTimestampNs.put(scenario, startingTimestamp); |
| } |
| } |
| |
| boolean hasReceivedMetricsPast(long endScenarioTimeNs) { |
| mThreadChecker.assertOnValidThread(); |
| return mMaxTimestamp > endScenarioTimeNs; |
| } |
| |
| JankMetrics stopTrackingScenario(@JankScenario int scenario) { |
| return stopTrackingScenario(scenario, -1); |
| } |
| |
| // The string added is a static string. |
| @SuppressWarnings("NoDynamicStringsInTraceEventCheck") |
| JankMetrics stopTrackingScenario(@JankScenario int scenario, long endScenarioTimeNs) { |
| try (TraceEvent e = |
| TraceEvent.scoped( |
| "finishTrackingScenario: " + scenarioToString(scenario), |
| Long.toString(endScenarioTimeNs))) { |
| mThreadChecker.assertOnValidThread(); |
| TraceEvent.finishAsync( |
| "JankCUJ:" + scenarioToString(scenario), TRACE_EVENT_TRACK_ID + scenario); |
| // Get the timestamp of the latest frame before startTrackingScenario was called. This |
| // can be null if tracking never started for scenario, or 0L if tracking started when no |
| // frames were stored. |
| Long previousFrameTimestamp = mScenarioPreviousFrameTimestampNs.remove(scenario); |
| |
| // If stopTrackingScenario is called without a corresponding startTrackingScenario then |
| // return an empty FrameMetrics object. |
| if (previousFrameTimestamp == null) { |
| removeUnusedFrames(); |
| return new JankMetrics(); |
| } |
| |
| int startingIndex = mTimestampsNs.indexOf(previousFrameTimestamp); |
| // The scenario starts with the frame after the tracking timestamp. |
| startingIndex++; |
| |
| // If startingIndex is out of bounds then we haven't recorded any frames since |
| // tracking started, return an empty FrameMetrics object. |
| if (startingIndex >= mTimestampsNs.size()) { |
| return new JankMetrics(); |
| } |
| |
| // Ending index is exclusive, so this is not out of bounds. |
| int endingIndex = mTimestampsNs.size(); |
| if (endScenarioTimeNs > 0) { |
| // binarySearch returns index of the search key (non-negative value) or (-(insertion |
| // point) - 1). |
| // The insertion point is defined as the index of the first element greater than the |
| // key, or a.length if all elements in the array are less than the specified key. |
| endingIndex = Collections.binarySearch(mTimestampsNs, endScenarioTimeNs); |
| if (endingIndex < 0) { |
| endingIndex = -1 * (endingIndex + 1); |
| } else { |
| endingIndex = Math.min(endingIndex + 1, mTimestampsNs.size()); |
| } |
| if (endingIndex <= startingIndex) { |
| // Something went wrong reset |
| TraceEvent.instant("FrameMetricsStore invalid endScenarioTimeNs"); |
| endingIndex = mTimestampsNs.size(); |
| } |
| } |
| |
| JankMetrics jankMetrics = |
| convertArraysToJankMetrics( |
| mTimestampsNs.subList(startingIndex, endingIndex), |
| mTotalDurationsNs.subList(startingIndex, endingIndex), |
| mNumMissedVsyncs.subList(startingIndex, endingIndex)); |
| removeUnusedFrames(); |
| |
| Long pendingStartTimestampNs = mPendingStartTimestampNs.remove(scenario); |
| if (pendingStartTimestampNs != null && pendingStartTimestampNs > endScenarioTimeNs) { |
| startTrackingScenario(scenario); |
| } |
| return jankMetrics; |
| } |
| } |
| |
| private void removeUnusedFrames() { |
| if (mScenarioPreviousFrameTimestampNs.isEmpty()) { |
| TraceEvent.instant("removeUnusedFrames", Long.toString(mTimestampsNs.size())); |
| mTimestampsNs.subList(1, mTimestampsNs.size()).clear(); |
| mTotalDurationsNs.subList(1, mTotalDurationsNs.size()).clear(); |
| mNumMissedVsyncs.subList(1, mNumMissedVsyncs.size()).clear(); |
| return; |
| } |
| |
| long firstUsedTimestamp = findFirstUsedTimestamp(); |
| // If the earliest timestamp tracked is 0 then that scenario contains every frame |
| // stored, so we shouldn't delete anything. |
| if (firstUsedTimestamp == 0L) { |
| return; |
| } |
| |
| int firstUsedIndex = mTimestampsNs.indexOf(firstUsedTimestamp); |
| if (firstUsedIndex == -1) { |
| if (BuildConfig.ENABLE_ASSERTS) { |
| throw new IllegalStateException("Timestamp for tracked scenario not found"); |
| } |
| // This shouldn't happen. |
| return; |
| } |
| TraceEvent.instant("removeUnusedFrames", Long.toString(firstUsedIndex)); |
| |
| mTimestampsNs.subList(1, firstUsedIndex).clear(); |
| mTotalDurationsNs.subList(1, firstUsedIndex).clear(); |
| mNumMissedVsyncs.subList(1, firstUsedIndex).clear(); |
| } |
| |
| private long findFirstUsedTimestamp() { |
| long firstTimestamp = Long.MAX_VALUE; |
| for (long timestamp : mScenarioPreviousFrameTimestampNs.values()) { |
| if (timestamp < firstTimestamp) { |
| firstTimestamp = timestamp; |
| } |
| } |
| |
| return firstTimestamp; |
| } |
| |
| private JankMetrics convertArraysToJankMetrics( |
| List<Long> longTimestampsNs, |
| List<Long> longDurations, |
| List<Integer> intNumMissedVsyncs) { |
| long[] timestamps = new long[longTimestampsNs.size()]; |
| for (int i = 0; i < longTimestampsNs.size(); i++) { |
| timestamps[i] = longTimestampsNs.get(i).longValue(); |
| } |
| |
| long[] durations = new long[longDurations.size()]; |
| for (int i = 0; i < longDurations.size(); i++) { |
| durations[i] = longDurations.get(i).longValue(); |
| } |
| |
| int[] numMissedVsyncs = new int[intNumMissedVsyncs.size()]; |
| for (int i = 0; i < intNumMissedVsyncs.size(); i++) { |
| numMissedVsyncs[i] = intNumMissedVsyncs.get(i).intValue(); |
| } |
| |
| JankMetrics jankMetrics = new JankMetrics(timestamps, durations, numMissedVsyncs); |
| return jankMetrics; |
| } |
| } |