blob: 73d313713aad37ae8686739a62a5c76466cd3dc7 [file] [log] [blame]
// Copyright 2021 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 androidx.annotation.VisibleForTesting;
import org.chromium.build.BuildConfig;
import java.util.ArrayList;
import java.util.HashMap;
import javax.annotation.concurrent.GuardedBy;
/**
* This class stores timestamps and frame durations related to different jank scenarios (e.g.
* Opening omnibox, Opening tab switcher). Scenarios are tracked by calling {@Link
* #startTrackingScenario} and {@Link #stopTrackingScenario}. {@Link #stopTrackingScenario} returns
* a FrameMetrics object with the timestamp and duration of all frames drawn between the start and
* end of the specified scenario.
*
* Multiple scenarios can be tracked simultaneously without duplicating data, and frame data is
* cleared as needed when scenarios end.
*/
class FrameMetricsStore {
// Guards access to the fields below.
private final Object mLock = new Object();
// Array of timestamps stored in nanoseconds, they represent the moment when each frame
// finished drawing, must always be the same size as mTotalDurationsNs.
@GuardedBy("mLock")
private final ArrayList<Long> mTimestampsNs = new ArrayList<>();
// Array of total durations stored in nanoseconds, they represent how long each frame took to
// draw, must always be the same size as mTimestampsNs.
@GuardedBy("mLock")
private final ArrayList<Long> mTotalDurationsNs = new ArrayList<>();
// Number of frames that FrameMetrics was unable to report on, due to excessive activity on its
// handler thread.
@GuardedBy("mLock")
private final ArrayList<Integer> mSkippedFrames = new ArrayList<>();
// Map of jank scenarios being currently tracked and the timestamp of the frame before tracking
// began. Each key corresponds to a JankScenario and each value corresponds to a timestamp
// present in mTimestampsNs, or 0 in case the scenario tracking started before any frames were
// recorded. If empty then no scenarios are being tracked, so calls to addFrameMeasurement won't
// store anything.
@GuardedBy("mLock")
private final HashMap<Integer, Long> mScenarioPreviousFrameTimestampNs = new HashMap<>();
/**
* Records a timestamp and total draw duration for a single frame.
*/
void addFrameMeasurement(long timestampNs, long totalDurationNs, int skippedFrames) {
synchronized (mLock) {
if (mScenarioPreviousFrameTimestampNs.isEmpty()) {
return;
}
mTimestampsNs.add(timestampNs);
mTotalDurationsNs.add(totalDurationNs);
mSkippedFrames.add(skippedFrames);
}
}
void startTrackingScenario(@JankScenario int scenario) {
synchronized (mLock) {
// Ignore multiple calls to startTrackingScenario without corresponding
// stopTrackingScenario calls.
if (mScenarioPreviousFrameTimestampNs.containsKey(scenario)) {
return;
}
// Scenarios are tracked based on the latest stored timestamp to allow fast lookups
// (find index of [timestamp] vs find first index that's >= [timestamp]). In case there
// are no stored timestamps then we hardcode the scenario's starting timestamp to 0L,
// this is handled as a special case in stopTrackingScenario by returning all stored
// frames.
Long startingTimestamp = 0L;
if (!mTimestampsNs.isEmpty()) {
startingTimestamp = mTimestampsNs.get(mTimestampsNs.size() - 1);
}
mScenarioPreviousFrameTimestampNs.put(scenario, startingTimestamp);
}
}
FrameMetrics stopTrackingScenario(@JankScenario int scenario) {
synchronized (mLock) {
// 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) {
return new FrameMetrics();
}
int startingIndex;
// Starting timestamp may be 0 if a scenario starts without any frames stored, in this
// case return all frames.
if (previousFrameTimestamp == 0) {
startingIndex = 0;
} else {
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 FrameMetrics();
}
}
// Ending index is exclusive, so this is not out of bounds.
int endingIndex = mTimestampsNs.size();
int scenarioFrameCount = endingIndex - startingIndex;
Long[] timestamps = mTimestampsNs.subList(startingIndex, endingIndex)
.toArray(new Long[scenarioFrameCount]);
Long[] durations = mTotalDurationsNs.subList(startingIndex, endingIndex)
.toArray(new Long[scenarioFrameCount]);
Integer[] skippedFrames = mSkippedFrames.subList(startingIndex, endingIndex)
.toArray(new Integer[scenarioFrameCount]);
FrameMetrics frameMetrics = new FrameMetrics(timestamps, durations, skippedFrames);
removeUnusedFrames();
return frameMetrics;
}
}
@VisibleForTesting
FrameMetrics getAllStoredMetricsForTesting() {
synchronized (mLock) {
Long[] timestamps = mTimestampsNs.toArray(new Long[mTimestampsNs.size()]);
Long[] durations = mTotalDurationsNs.toArray(new Long[mTotalDurationsNs.size()]);
Integer[] skippedFrames = mSkippedFrames.toArray(new Integer[mSkippedFrames.size()]);
FrameMetrics frameMetrics = new FrameMetrics(timestamps, durations, skippedFrames);
return frameMetrics;
}
}
@GuardedBy("mLock")
private void removeUnusedFrames() {
if (mScenarioPreviousFrameTimestampNs.isEmpty()) {
mTimestampsNs.clear();
mTotalDurationsNs.clear();
mSkippedFrames.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;
}
mTimestampsNs.subList(0, firstUsedIndex).clear();
mTotalDurationsNs.subList(0, firstUsedIndex).clear();
mSkippedFrames.subList(0, firstUsedIndex).clear();
}
@GuardedBy("mLock")
private long findFirstUsedTimestamp() {
long firstTimestamp = Long.MAX_VALUE;
for (long timestamp : mScenarioPreviousFrameTimestampNs.values()) {
if (timestamp < firstTimestamp) {
firstTimestamp = timestamp;
}
}
return firstTimestamp;
}
}