/* * 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 android.support.v4.app; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import android.app.Activity; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.RestrictTo; import android.util.SparseIntArray; import android.view.Window; 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()}. *

* For more information on the various metrics tracked, see the documentation for the * FrameMetrics * API added in API 24 as well as the * GPU Profiling * guide. */ 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. *

* 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. *

* 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; private int mTrackingFlags; private SparseIntArray[] mMetrics = new SparseIntArray[LAST_INDEX + 1]; private ArrayList> 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 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 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; } } }