blob: dd2824a1926c9d9ea94cb04ea27707cfffa0ea11 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.app.Activity;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.SparseIntArray;
import android.view.Window;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
/**
* This class can be used to record and return data about per-frame durations. It returns those
* results in an array per metric type, with the results indicating how many samples were
* recorded for each duration value. The details of the durations data are described in
* {@link #getMetrics()}.
* <p>
* For more information on the various metrics tracked, see the documentation for the
* <a href="https://developer.android.com/reference/android/view/FrameMetrics.html">FrameMetrics
* </a> API added in API 24 as well as the
* <a href="https://developer.android.com/studio/profile/dev-options-rendering.html">GPU Profiling
* guide</a>.
*/
public class FrameMetricsAggregator {
private static final String TAG = "FrameMetrics";
private static final boolean DBG = false;
/**
* The index in the metrics array where the data for {@link #TOTAL_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int TOTAL_INDEX = 0;
/**
* The index in the metrics array where the data for {@link #INPUT_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int INPUT_INDEX = 1;
/**
* The index in the metrics array where the data for {@link #LAYOUT_MEASURE_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int LAYOUT_MEASURE_INDEX = 2;
/**
* The index in the metrics array where the data for {@link #DRAW_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int DRAW_INDEX = 3;
/**
* The index in the metrics array where the data for {@link #SYNC_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int SYNC_INDEX = 4;
/**
* The index in the metrics array where the data for {@link #SYNC_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int COMMAND_INDEX = 5;
/**
* The index in the metrics array where the data for {@link #COMMAND_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int SWAP_INDEX = 6;
/**
* The index in the metrics array where the data for {@link #DELAY_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int DELAY_INDEX = 7;
/**
* The index in the metrics array where the data for {@link #ANIMATION_DURATION}
* is stored.
* @see #getMetrics()
*/
public static final int ANIMATION_INDEX = 8;
private static final int LAST_INDEX = 8;
/**
* A flag indicating that the metrics should track the total duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int TOTAL_DURATION = 1 << TOTAL_INDEX;
/**
* A flag indicating that the metrics should track the input duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int INPUT_DURATION = 1 << INPUT_INDEX;
/**
* A flag indicating that the metrics should track the layout duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int LAYOUT_MEASURE_DURATION = 1 << LAYOUT_MEASURE_INDEX;
/**
* A flag indicating that the metrics should track the draw duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int DRAW_DURATION = 1 << DRAW_INDEX;
/**
* A flag indicating that the metrics should track the sync duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int SYNC_DURATION = 1 << SYNC_INDEX;
/**
* A flag indicating that the metrics should track the command duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int COMMAND_DURATION = 1 << COMMAND_INDEX;
/**
* A flag indicating that the metrics should track the swap duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int SWAP_DURATION = 1 << SWAP_INDEX;
/**
* A flag indicating that the metrics should track the delay duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int DELAY_DURATION = 1 << DELAY_INDEX;
/**
* A flag indicating that the metrics should track the animation duration. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate all of the metrics that should be tracked for that activity.
*/
public static final int ANIMATION_DURATION = 1 << ANIMATION_INDEX;
/**
* A flag indicating that the metrics should track all durations. This is
* a shorthand for OR'ing all of the duration flags. This
* flag may be OR'd with the other flags here when calling {@link #FrameMetricsAggregator(int)}
* to indicate the metrics that should be tracked for that activity.
*/
public static final int EVERY_DURATION = 0x1ff;
private FrameMetricsBaseImpl mInstance;
/** @hide */
@RestrictTo(LIBRARY_GROUP)
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
value = {
TOTAL_DURATION,
INPUT_DURATION,
LAYOUT_MEASURE_DURATION,
DRAW_DURATION,
SYNC_DURATION,
COMMAND_DURATION,
SWAP_DURATION,
DELAY_DURATION,
ANIMATION_DURATION,
EVERY_DURATION
})
public @interface MetricType {}
/**
* Constructs a FrameMetricsAggregator object that will track {@link #TOTAL_DURATION}
* metrics. If more fine-grained metrics are needed, use {@link #FrameMetricsAggregator(int)}
* instead.
*/
public FrameMetricsAggregator() {
this(TOTAL_DURATION);
}
/**
* Constructs a FrameMetricsAggregator object that will track the metrics specified bty
* {@code metricTypeFlags}, which is a value derived by OR'ing together metrics constants
* such as {@link #TOTAL_DURATION} to specify all metrics that should be tracked. For example,
* {@code TOTAL_DURATION | DRAW_DURATION} will track both the total and draw durations
* for every frame.
*
* @param metricTypeFlags A bitwise collection of flags indicating which metrics should
* be recorded.
*/
public FrameMetricsAggregator(@MetricType int metricTypeFlags) {
if (Build.VERSION.SDK_INT >= 24) {
mInstance = new FrameMetricsApi24Impl(metricTypeFlags);
} else {
mInstance = new FrameMetricsBaseImpl();
}
}
/**
* Starts recording frame metrics for the given activity.
*
* @param activity The Activity object which will have its metrics measured.
*/
public void add(@NonNull Activity activity) {
mInstance.add(activity);
}
/**
* Stops recording metrics for {@code activity} and returns the collected metrics so far.
* Recording will continue if there are still other activities being tracked. Calling
* remove() does not reset the metrics array; you must call {@link #reset()} to clear the
* data.
*
* @param activity The Activity to stop tracking metrics for.
* @return An array whose index refers to the type of metric stored in that item's
* SparseIntArray object, e.g., data for {@code TOTAL_DURATION} is stored in
* the {@code [TOTAL_INDEX]} item.
* @see #getMetrics()
*/
@Nullable
public SparseIntArray[] remove(@NonNull Activity activity) {
return mInstance.remove(activity);
}
/**
* Stops recording metrics for all Activities currently being tracked. Like {@link
* #remove(Activity)}, this method returns the currently-collected metrics. Calling
* stop() does not reset the metrics array; you must call {@link #reset()} to clear the
* data.
*
* @return An array whose index refers to the type of metric stored in that item's
* SparseIntArray object, e.g., data for {@code TOTAL_DURATION} is stored in
* the {@code [TOTAL_INDEX]} item.
* @see #remove(Activity)
* @see #getMetrics()
*/
@Nullable
public SparseIntArray[] stop() {
return mInstance.stop();
}
/**
* Resets the metrics data and returns the currently-collected metrics.
*
* @return An array whose index refers to the type of metric stored in that item's
* SparseIntArray object, e.g., data for {@code TOTAL_DURATION} is stored in
* the {@code [TOTAL_INDEX]} item.
* @see #getMetrics()
*/
@Nullable
public SparseIntArray[] reset() {
return mInstance.reset();
}
/**
* Returns the currently-collected metrics in an array of SparseIntArray objects.
* The index of the array indicates which metric's data is stored in that
* SparseIntArray object. For example, results for total duration will be in
* the {@code [TOTAL_INDEX]} item.
* <p>
* The return value may be null if no metrics were tracked. This is especially true on releases
* earlier than API 24, as the FrameMetrics system does not exist on these earlier release.
* If the return value is not null, any of the objects at a given index in the array
* may still be null, which indicates that data was not being tracked for that type of metric.
* For example, if the FrameMetricsAggregator was created with a call to
* {@code new FrameMetricsAggregator(TOTAL_DURATION | DRAW_DURATION)}, then the SparseIntArray
* at index {@code INPUT_INDEX} will be null.
* <p>
* For a given non-null SparseIntArray, the results stored are the number of samples at
* each millisecond value (rounded). For example, if a data sample consisted of total
* durations of 5.1ms, 5.8ms, 6.1ms, and 8.2ms, the SparseIntArray at {@code [TOTAL_DURATION]}
* would have key-value pairs (5, 1), (6, 2), (8, 1).
*
* @return An array whose index refers to the type of metric stored in that item's
* SparseIntArray object, e.g., data for {@code TOTAL_DURATION} is stored in
* the {@code [TOTAL_INDEX]} item.
*/
@Nullable
public SparseIntArray[] getMetrics() {
return mInstance.getMetrics();
}
/**
* Base implementation noops everything - there's no data to return on pre-API24 releases.
*/
private static class FrameMetricsBaseImpl {
public void add(Activity activity) {
}
public SparseIntArray[] remove(Activity activity) {
return null;
}
public SparseIntArray[] stop() {
return null;
}
public SparseIntArray[] getMetrics() {
return null;
}
public SparseIntArray[] reset() {
return null;
}
}
@RequiresApi(24)
private static class FrameMetricsApi24Impl extends FrameMetricsBaseImpl {
private static final int NANOS_PER_MS = 1000000;
// rounding value adds half a millisecond, for rounding to nearest ms
private static final int NANOS_ROUNDING_VALUE = NANOS_PER_MS / 2;
int mTrackingFlags;
SparseIntArray[] mMetrics = new SparseIntArray[LAST_INDEX + 1];
private ArrayList<WeakReference<Activity>> mActivities = new ArrayList<>();
private static HandlerThread sHandlerThread = null;
private static Handler sHandler = null;
FrameMetricsApi24Impl(int trackingFlags) {
mTrackingFlags = trackingFlags;
}
Window.OnFrameMetricsAvailableListener mListener =
new Window.OnFrameMetricsAvailableListener() {
@Override
public void onFrameMetricsAvailable(Window window,
android.view.FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {
if ((mTrackingFlags & TOTAL_DURATION) != 0) {
addDurationItem(mMetrics[TOTAL_INDEX],
frameMetrics.getMetric(android.view.FrameMetrics.TOTAL_DURATION));
}
if ((mTrackingFlags & INPUT_DURATION) != 0) {
addDurationItem(mMetrics[INPUT_INDEX],
frameMetrics.getMetric(
android.view.FrameMetrics.INPUT_HANDLING_DURATION));
}
if ((mTrackingFlags & LAYOUT_MEASURE_DURATION) != 0) {
addDurationItem(mMetrics[LAYOUT_MEASURE_INDEX],
frameMetrics.getMetric(
android.view.FrameMetrics.LAYOUT_MEASURE_DURATION));
}
if ((mTrackingFlags & DRAW_DURATION) != 0) {
addDurationItem(mMetrics[DRAW_INDEX],
frameMetrics.getMetric(android.view.FrameMetrics.DRAW_DURATION));
}
if ((mTrackingFlags & SYNC_DURATION) != 0) {
addDurationItem(mMetrics[SYNC_INDEX],
frameMetrics.getMetric(android.view.FrameMetrics.SYNC_DURATION));
}
if ((mTrackingFlags & SWAP_DURATION) != 0) {
addDurationItem(mMetrics[SWAP_INDEX],
frameMetrics.getMetric(
android.view.FrameMetrics.SWAP_BUFFERS_DURATION));
}
if ((mTrackingFlags & COMMAND_DURATION) != 0) {
addDurationItem(mMetrics[COMMAND_INDEX],
frameMetrics.getMetric(
android.view.FrameMetrics.COMMAND_ISSUE_DURATION));
}
if ((mTrackingFlags & DELAY_DURATION) != 0) {
addDurationItem(mMetrics[DELAY_INDEX],
frameMetrics.getMetric(
android.view.FrameMetrics.UNKNOWN_DELAY_DURATION));
}
if ((mTrackingFlags & ANIMATION_DURATION) != 0) {
addDurationItem(mMetrics[ANIMATION_INDEX],
frameMetrics.getMetric(
android.view.FrameMetrics.ANIMATION_DURATION));
}
}
};
void addDurationItem(SparseIntArray buckets, long duration) {
if (buckets != null) {
int durationMs = (int) ((duration + NANOS_ROUNDING_VALUE) / NANOS_PER_MS);
if (duration >= 0) {
// ignore values < 0; something must have gone wrong
int oldValue = buckets.get(durationMs);
buckets.put(durationMs, (oldValue + 1));
}
}
}
@Override
public void add(Activity activity) {
if (sHandlerThread == null) {
sHandlerThread = new HandlerThread("FrameMetricsAggregator");
sHandlerThread.start();
sHandler = new Handler(sHandlerThread.getLooper());
}
for (int i = 0; i <= LAST_INDEX; ++i) {
if (mMetrics[i] == null && (mTrackingFlags & (1 << i)) != 0) {
mMetrics[i] = new SparseIntArray();
}
}
activity.getWindow().addOnFrameMetricsAvailableListener(mListener, sHandler);
mActivities.add(new WeakReference<>(activity));
}
@Override
public SparseIntArray[] remove(Activity activity) {
for (WeakReference<Activity> activityRef : mActivities) {
if (activityRef.get() == activity) {
mActivities.remove(activityRef);
break;
}
}
activity.getWindow().removeOnFrameMetricsAvailableListener(mListener);
return mMetrics;
}
@Override
public SparseIntArray[] stop() {
int size = mActivities.size();
for (int i = size - 1; i >= 0; i--) {
WeakReference<Activity> ref = mActivities.get(i);
Activity activity = ref.get();
if (ref.get() != null) {
activity.getWindow().removeOnFrameMetricsAvailableListener(mListener);
mActivities.remove(i);
}
}
return mMetrics;
}
@Override
public SparseIntArray[] getMetrics() {
return mMetrics;
}
@Override
public SparseIntArray[] reset() {
SparseIntArray[] returnVal = mMetrics;
mMetrics = new SparseIntArray[LAST_INDEX + 1];
return returnVal;
}
}
}