GfxMonitorImpl.java

/*
 * Copyright (C) 2015 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.test.jank.internal;

import android.app.Instrumentation;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.VisibleForTesting;
import androidx.test.jank.GfxMonitor;
import androidx.test.jank.IMonitor;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.Map;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import junit.framework.Assert;

/**
 * Monitors dumpsys gfxinfo to detect janky frames.
 *
 * Reports average and max jank. Additionally reports summary statistics for common problems that
 * can lead to dropped frames.
 */
class GfxMonitorImpl implements IMonitor {

    // Patterns used for parsing dumpsys gfxinfo output
    public enum JankStat {
        TOTAL_FRAMES(Pattern.compile("\s*Total frames rendered: (\d+)"), 1, Integer.class),
        NUM_JANKY(Pattern.compile("\s*Janky frames: (\d+) \((\d+(\.\d+))%\)"), 2,
                Double.class),
        FRAME_TIME_50TH(Pattern.compile("\s*50th percentile: (\d+)ms"), 1, Integer.class, true),
        FRAME_TIME_90TH(Pattern.compile("\s*90th percentile: (\d+)ms"), 1, Integer.class),
        FRAME_TIME_95TH(Pattern.compile("\s*95th percentile: (\d+)ms"), 1, Integer.class),
        FRAME_TIME_99TH(Pattern.compile("\s*99th percentile: (\d+)ms"), 1, Integer.class),
        NUM_MISSED_VSYNC(Pattern.compile("\s*Number Missed Vsync: (\d+)"), 1, Integer.class),
        NUM_HIGH_INPUT_LATENCY(Pattern.compile("\s*Number High input latency: (\d+)"), 1,
                Integer.class),
        NUM_SLOW_UI_THREAD(Pattern.compile("\s*Number Slow UI thread: (\d+)"), 1, Integer.class),
        NUM_SLOW_BITMAP_UPLOADS(Pattern.compile("\s*Number Slow bitmap uploads: (\d+)"), 1,
                Integer.class),
        NUM_SLOW_DRAW(Pattern.compile("\s*Number Slow issue draw commands: (\d+)"), 1,
                Integer.class),
        NUM_FRAME_DEADLINE_MISSED(Pattern.compile("\s*Number Frame deadline missed: (\d+)"), 1,
                Integer.class);

        private boolean mSuccessfulParse = false;
        private boolean mOptional = false;
        private Pattern mParsePattern;
        private int mGroupIndex;
        private Class mType;

        JankStat(Pattern pattern, int groupIndex, Class type) {
            this(pattern, groupIndex, type, false);
        }

        JankStat(Pattern pattern, int groupIndex, Class type, boolean optional) {
            mParsePattern = pattern;
            mGroupIndex = groupIndex;
            mType = type;
            mOptional = optional;
        }

        String parse(String line) {
            String ret = null;
            Matcher matcher = mParsePattern.matcher(line);
            if (matcher.matches()) {
                ret = matcher.group(mGroupIndex);
                mSuccessfulParse = true;
            }
            return ret;
        }

        boolean wasParsedSuccessfully() {
            return mSuccessfulParse;
        }

        boolean isOptional() {
            return mOptional;
        }

        void reset() {
            mSuccessfulParse = false;
        }

        Class getType() {
            return mType;
        }
    }

    // Metrics accumulated for each iteration
    private Map<JankStat, List<? extends Number>> mAccumulatedStats =
            new EnumMap<JankStat, List<? extends Number>>(JankStat.class);


    // Used to invoke dumpsys gfxinfo
    private Instrumentation mInstrumentation;
    private String mProcess;


    /**
     * Constructs a GfxMonitorImpl instance which can be used to monitor the given {@code process}
     * for jank.
     */
    public GfxMonitorImpl(Instrumentation instrumentation, String process) {
        mInstrumentation = instrumentation;
        mProcess = process;

        for (JankStat stat : JankStat.values()) {
            if (stat.getType().equals(Integer.class)) {
                mAccumulatedStats.put(stat, new ArrayList<Integer>());
            } else if (stat.getType().equals(Double.class)) {
                mAccumulatedStats.put(stat, new ArrayList<Double>());
            } else {
                // Shouldn't get here
                throw new IllegalStateException("Unsupported JankStat type");
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void startIteration() throws IOException {
        // Clear out any previous data
        InputStream stdout = executeShellCommand(
                String.format("dumpsys gfxinfo %s reset", getProcess()));
        BufferedReader reader = new BufferedReader(new InputStreamReader(stdout));

        try {
            // Read the output, but don't do anything with it
            while (reader.readLine() != null) {
            }
        } finally {
            reader.close();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Bundle stopIteration() throws IOException {
        // Dump the latest stats
        InputStream stdout = executeShellCommand(String.format("dumpsys gfxinfo %s", getProcess()));
        BufferedReader reader = new BufferedReader(new InputStreamReader(stdout));

        // The frame stats section has the following output:
        // Total frames rendered: ###
        // Janky frames: ### (##.##%)
        // 50th percentile: ##ms
        // 90th percentile: ##ms
        // 95th percentile: ##ms
        // 99th percentile: ##ms
        // Number Missed Vsync: #
        // Number High input latency: #
        // Number Slow UI thread: #
        // Number Slow bitmap uploads: #
        // Number Slow draw: #
        // Number Frame deadline missed: #

        try {
            String line;
            while ((line = reader.readLine()) != null) {

                // Attempt to parse the line as a frame stat value
                for (JankStat stat : JankStat.values()) {
                    String part;
                    if ((part = stat.parse(line)) != null) {
                        // Parse was successful. Add the numeric value to the accumulated list of
                        // values for that stat.
                        if (stat.getType().equals(Integer.class)) {
                            List<Integer> stats = (List<Integer>)mAccumulatedStats.get(stat);
                            stats.add(Integer.valueOf(part));
                        } else if (stat.getType().equals(Double.class)) {
                            List<Double> stats = (List<Double>)mAccumulatedStats.get(stat);
                            stats.add(Double.valueOf(part));
                        } else {
                            // Shouldn't get here
                            throw new IllegalStateException("Unsupported JankStat type");
                        }
                        break;
                    }
                }
            }
        } finally {
            reader.close();
        }

        // Make sure we found all the stats
        for (JankStat stat : JankStat.values()) {
            if (!stat.wasParsedSuccessfully() && !stat.isOptional()) {
                Assert.fail(String.format("Failed to parse %s", stat.name()));
            }
            stat.reset();
        }

        List<Integer> totalFrames = (List<Integer>)mAccumulatedStats.get(JankStat.TOTAL_FRAMES);

        // TODO(allenhair): Return full itermediate results.
        Bundle ret = new Bundle();
        ret.putInt("num-frames", totalFrames.get(totalFrames.size() - 1));
        return ret;
    }

    private void putAvgMaxInteger(Bundle metrics, String averageKey, String maxKey,
            List<Integer> values) {

        metrics.putDouble(averageKey, MetricsHelper.computeAverageInt(values));
        metrics.putInt(maxKey, Collections.max(values));
    }

    private void putAvgMaxDouble(Bundle metrics, String averageKey, String maxKey,
            List<Double> values) {

        metrics.putDouble(averageKey, MetricsHelper.computeAverageDouble(values));
        metrics.putDouble(maxKey, Collections.max(values));
    }

    private List<Double> transformToPercentage(List<Integer> values, List<Integer> totals) {
        List<Double> ret = new ArrayList<Double>(values.size());

        Iterator<Integer> valuesItr = values.iterator();
        Iterator<Integer> totalsItr = totals.iterator();
        while (valuesItr.hasNext()) {
            double value = (double)valuesItr.next().intValue();
            double total = (double)totalsItr.next().intValue();

            ret.add(value / total * 100.0f);
        }

        return ret;
    }

    private int computeAverage(List<Integer> values) {
        int sum = 0;

        for (Integer value : values) {
            sum += value;
        }

        return (sum / values.size());
    }

    /**
     * {@inheritDoc}
     */
    public Bundle getMetrics() {
        Bundle metrics = new Bundle();

        // Retrieve the total number of frames
        List<Integer> totals = (List<Integer>)mAccumulatedStats.get(JankStat.TOTAL_FRAMES);

        // Store avg, min and max of total frames
        metrics.putInt(GfxMonitor.KEY_AVG_TOTAL_FRAMES, computeAverage(totals));
        metrics.putInt(GfxMonitor.KEY_MAX_TOTAL_FRAMES, Collections.max(totals));
        metrics.putInt(GfxMonitor.KEY_MIN_TOTAL_FRAMES, Collections.min(totals));

        // Store average and max jank
        putAvgMaxDouble(metrics, GfxMonitor.KEY_AVG_NUM_JANKY, GfxMonitor.KEY_MAX_NUM_JANKY,
                (List<Double>)mAccumulatedStats.get(JankStat.NUM_JANKY));

        // Store average and max percentile frame times
        List<Integer> statsFor50TH = (List<Integer>)mAccumulatedStats.get(JankStat.FRAME_TIME_50TH);
        if (!statsFor50TH.isEmpty()) {
            // 50th percentile frame is optional, because it wasn't available in the initial version
            // of the gfxinfo service
            putAvgMaxInteger(metrics, GfxMonitor.KEY_AVG_FRAME_TIME_50TH_PERCENTILE,
                    GfxMonitor.KEY_MAX_FRAME_TIME_50TH_PERCENTILE, statsFor50TH);
        }
        putAvgMaxInteger(metrics, GfxMonitor.KEY_AVG_FRAME_TIME_90TH_PERCENTILE,
                GfxMonitor.KEY_MAX_FRAME_TIME_90TH_PERCENTILE,
                (List<Integer>)mAccumulatedStats.get(JankStat.FRAME_TIME_90TH));
        putAvgMaxInteger(metrics, GfxMonitor.KEY_AVG_FRAME_TIME_95TH_PERCENTILE,
                GfxMonitor.KEY_MAX_FRAME_TIME_95TH_PERCENTILE,
                (List<Integer>)mAccumulatedStats.get(JankStat.FRAME_TIME_95TH));
        putAvgMaxInteger(metrics, GfxMonitor.KEY_AVG_FRAME_TIME_99TH_PERCENTILE,
                GfxMonitor.KEY_MAX_FRAME_TIME_99TH_PERCENTILE,
                (List<Integer>)mAccumulatedStats.get(JankStat.FRAME_TIME_99TH));

        // Store average and max missed vsync
        List<Double> missedVsyncPercent = transformToPercentage(
                (List<Integer>)mAccumulatedStats.get(JankStat.NUM_MISSED_VSYNC), totals);
        putAvgMaxDouble(metrics, GfxMonitor.KEY_AVG_MISSED_VSYNC, GfxMonitor.KEY_MAX_MISSED_VSYNC,
                missedVsyncPercent);

        // Store average and max high input latency
        List<Double> highInputLatencyPercent = transformToPercentage(
                (List<Integer>)mAccumulatedStats.get(JankStat.NUM_HIGH_INPUT_LATENCY), totals);
        putAvgMaxDouble(metrics, GfxMonitor.KEY_AVG_HIGH_INPUT_LATENCY,
                GfxMonitor.KEY_MAX_HIGH_INPUT_LATENCY, highInputLatencyPercent);

        // Store average and max slow ui thread
        List<Double> slowUiThreadPercent = transformToPercentage(
                (List<Integer>)mAccumulatedStats.get(JankStat.NUM_SLOW_UI_THREAD), totals);
        putAvgMaxDouble(metrics, GfxMonitor.KEY_AVG_SLOW_UI_THREAD,
                GfxMonitor.KEY_MAX_SLOW_UI_THREAD, slowUiThreadPercent);

        // Store average and max slow bitmap uploads
        List<Double> slowBitMapUploadsPercent = transformToPercentage(
                (List<Integer>)mAccumulatedStats.get(JankStat.NUM_SLOW_BITMAP_UPLOADS), totals);
        putAvgMaxDouble(metrics, GfxMonitor.KEY_AVG_SLOW_BITMAP_UPLOADS,
                GfxMonitor.KEY_MAX_SLOW_BITMAP_UPLOADS, slowBitMapUploadsPercent);

        // Store average and max slow draw
        List<Double> slowDrawPercent = transformToPercentage(
                (List<Integer>)mAccumulatedStats.get(JankStat.NUM_SLOW_DRAW), totals);
        putAvgMaxDouble(metrics, GfxMonitor.KEY_AVG_SLOW_DRAW, GfxMonitor.KEY_MAX_SLOW_DRAW,
                slowDrawPercent);

        // Store average and max of number of frame deadline missed
        putAvgMaxInteger(metrics, GfxMonitor.KEY_AVG_NUM_FRAME_MISSED,
                GfxMonitor.KEY_MAX_NUM_FRAME_MISSED,
                (List<Integer>)mAccumulatedStats.get(JankStat.NUM_FRAME_DEADLINE_MISSED));

        return metrics;
    }

    private String getMatchGroup(String input, Pattern pattern, int groupIndex) {
        String ret = null;
        Matcher matcher = pattern.matcher(input);
        if (matcher.matches()) {
            ret = matcher.group(groupIndex);
        }
        return ret;
    }

    /** Returns the name of the process that this monitor is observing. */
    @VisibleForTesting
    protected String getProcess() {
        return mProcess;
    }

    /**
     * Executes the given {@code command} as the shell user and returns an {@link InputStream}
     * containing the command's standard output.
     */
    @VisibleForTesting
    protected InputStream executeShellCommand(String command) {
        ParcelFileDescriptor stdout = mInstrumentation.getUiAutomation()
                .executeShellCommand(command);
        return new ParcelFileDescriptor.AutoCloseInputStream(stdout);
    }
}