package com.frogermcs.androiddevmetrics.internal.metrics; import android.annotation.TargetApi; import android.os.Build; import android.view.Choreographer; import com.frogermcs.androiddevmetrics.internal.metrics.model.FpsDropMetric; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; /** * Created by Miroslaw Stanek on 16.02.2016. */ public class ChoreographerMetrics { private static class Holder { static final ChoreographerMetrics INSTANCE = new ChoreographerMetrics(); } public static ChoreographerMetrics getInstance() { return Holder.INSTANCE; } public final List<FpsDropMetric> dropMetricsList = new ArrayList<>(); public final Set<OnMetricsDataListener> dataListeners = new HashSet<>(); private Choreographer choreographer; private long frameStartTime = 0; private int framesRendered = 0; private int intervalMillis; private double maxFpsForFrameDrop; private int dropIntervalMillis = 1000 * 10; private long firstDropRegistered = 0; private List<Double> tempDrops = new LinkedList<>(); private String currentActivityName; private FrameDropsMetrics frameDropsMetrics; ChoreographerMetrics() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { choreographer = Choreographer.getInstance(); frameDropsMetrics = new FrameDropsMetrics(); } } public void start() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { choreographer.postFrameCallback(frameDropsMetrics); } } public void stop() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { frameStartTime = 0; framesRendered = 0; choreographer.removeFrameCallback(frameDropsMetrics); } } public void setIntervalMillis(int interval) { this.intervalMillis = interval; } public void setMaxFpsForFrameDrop(double fps) { this.maxFpsForFrameDrop = fps; } private void onDropRegistered(double fps, long registeredTime) { String activityName = ActivityLaunchMetrics.getInstance().getCurrentActivityName(); tempDrops.add(fps); if (firstDropRegistered == 0) { firstDropRegistered = registeredTime; currentActivityName = activityName; } long dropTimeSpan = registeredTime - firstDropRegistered; if (dropTimeSpan > dropIntervalMillis || !activityName.equals(currentActivityName)) { collectDropsIfAny(); currentActivityName = activityName; } } public void collectDropsIfAny() { if (tempDrops.size() > 0) { FpsDropMetric dropMetric = FpsDropMetric.fromDrops(tempDrops, currentActivityName); firstDropRegistered = 0; broadcastNewDropMetrics(dropMetric); dropMetricsList.add(dropMetric); tempDrops.clear(); } } private void broadcastNewDropMetrics(FpsDropMetric dropMetric) { for (OnMetricsDataListener dataListener : dataListeners) { dataListener.onFrameDropRegistered(dropMetric); } } public void addMetricsDataListener(OnMetricsDataListener onDataListener) { dataListeners.add(onDataListener); } public void removeMetricsDataListener(OnMetricsDataListener onDataListener) { dataListeners.remove(onDataListener); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private class FrameDropsMetrics implements Choreographer.FrameCallback { @Override public void doFrame(long frameTimeNanos) { long currentTimeMillis = TimeUnit.NANOSECONDS.toMillis(frameTimeNanos); if (frameStartTime > 0) { final long timeSpan = currentTimeMillis - frameStartTime; framesRendered++; if (timeSpan > intervalMillis) { double fps = framesRendered * 1000 / (double) timeSpan; frameStartTime = currentTimeMillis; framesRendered = 0; if (fps < maxFpsForFrameDrop) { onDropRegistered(fps, currentTimeMillis); } } if (firstDropRegistered > 0) { long dropIntervalTimeSpan = currentTimeMillis - firstDropRegistered; if (dropIntervalTimeSpan > dropIntervalMillis) { collectDropsIfAny(); } } } else { frameStartTime = currentTimeMillis; } choreographer.postFrameCallback(this); } } }