com.google.caliper.ConsoleReport.java Source code

Java tutorial

Introduction

Here is the source code for com.google.caliper.ConsoleReport.java

Source

/**
 * Copyright (C) 2009 Google Inc.
 *
 * 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 com.google.caliper;

import com.google.caliper.util.LinearTranslation;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Prints a report containing the tested values and the corresponding
 * measurements. Measurements are grouped by variable using indentation.
 * Alongside numeric values, quick-glance ascii art bar charts are printed.
 * Sample output (this may not represent the exact form that is produced):
 * <pre>
 *              benchmark          d     ns linear runtime
 * ConcatenationBenchmark 3.14159265   4397 ========================
 * ConcatenationBenchmark       -0.0    223 ===============
 *     FormatterBenchmark 3.14159265  33999 ==============================
 *     FormatterBenchmark       -0.0  26399 =============================
 * </pre>
 */
final class ConsoleReport {

    private static final int barGraphWidth = 30;

    private static final int UNITS_FOR_SCORE_100 = 1;
    private static final int UNITS_FOR_SCORE_10 = 1000000000; // 1 s

    private static final LinearTranslation scoreTranslation = new LinearTranslation(Math.log(UNITS_FOR_SCORE_10),
            10, Math.log(UNITS_FOR_SCORE_100), 100);

    public static final Ordering<Entry<String, Integer>> UNIT_ORDERING = new Ordering<Entry<String, Integer>>() {
        @Override
        public int compare(Entry<String, Integer> a, Entry<String, Integer> b) {
            return a.getValue().compareTo(b.getValue());
        }
    };

    private final List<Variable> variables;
    private final Run run;
    private final List<Scenario> scenarios;

    private final List<MeasurementType> orderedMeasurementTypes;
    private final MeasurementType type;
    private final double maxValue;
    private final double logMinValue;
    private final double logMaxValue;
    private final EnumMap<MeasurementType, Integer> decimalDigitsMap = new EnumMap<MeasurementType, Integer>(
            MeasurementType.class);
    private final EnumMap<MeasurementType, Double> divideByMap = new EnumMap<MeasurementType, Double>(
            MeasurementType.class);
    private final EnumMap<MeasurementType, String> unitMap = new EnumMap<MeasurementType, String>(
            MeasurementType.class);
    private final EnumMap<MeasurementType, Integer> measurementColumnLengthMap = new EnumMap<MeasurementType, Integer>(
            MeasurementType.class);
    private boolean printScore;

    ConsoleReport(Run run, Arguments arguments) {
        this.run = run;
        unitMap.put(MeasurementType.TIME, arguments.getTimeUnit());
        unitMap.put(MeasurementType.INSTANCE, arguments.getInstanceUnit());
        unitMap.put(MeasurementType.MEMORY, arguments.getMemoryUnit());

        if (arguments.getMeasureMemory()) {
            orderedMeasurementTypes = Arrays.asList(MeasurementType.TIME, MeasurementType.INSTANCE,
                    MeasurementType.MEMORY);
        } else {
            orderedMeasurementTypes = Arrays.asList(MeasurementType.TIME);
        }

        if (arguments.getPrimaryMeasurementType() != null) {
            this.type = arguments.getPrimaryMeasurementType();
        } else {
            this.type = MeasurementType.TIME;
        }

        double min = Double.POSITIVE_INFINITY;
        double max = 0;

        Multimap<String, String> nameToValues = LinkedHashMultimap.create();
        List<Variable> variablesBuilder = new ArrayList<Variable>();
        for (Entry<Scenario, ScenarioResult> entry : this.run.getMeasurements().entrySet()) {
            Scenario scenario = entry.getKey();
            double d = entry.getValue().getMeasurementSet(type).medianUnits();

            min = Math.min(min, d);
            max = Math.max(max, d);

            for (Entry<String, String> variable : scenario.getVariables().entrySet()) {
                String name = variable.getKey();
                nameToValues.put(name, variable.getValue());
            }
        }

        for (Entry<String, Collection<String>> entry : nameToValues.asMap().entrySet()) {
            Variable variable = new Variable(entry.getKey(), entry.getValue());
            variablesBuilder.add(variable);
        }

        /*
         * Figure out how much influence each variable has on the measured value.
         * We sum the measurements taken with each value of each variable. For
         * variable that have influence on the measurement, the sums will differ
         * by value. If the variable has little influence, the sums will be similar
         * to one another and close to the overall average. We take the standard
         * deviation across each variable's collection of sums. Higher standard
         * deviation implies higher influence on the measured result.
         */
        double sumOfAllMeasurements = 0;
        for (ScenarioResult measurement : this.run.getMeasurements().values()) {
            sumOfAllMeasurements += measurement.getMeasurementSet(type).medianUnits();
        }
        for (Variable variable : variablesBuilder) {
            int numValues = variable.values.size();
            double[] sumForValue = new double[numValues];
            for (Entry<Scenario, ScenarioResult> entry : this.run.getMeasurements().entrySet()) {
                Scenario scenario = entry.getKey();
                sumForValue[variable.index(scenario)] += entry.getValue().getMeasurementSet(type).medianUnits();
            }
            double mean = sumOfAllMeasurements / sumForValue.length;
            double stdDeviationSquared = 0;
            for (double value : sumForValue) {
                double distance = value - mean;
                stdDeviationSquared += distance * distance;
            }
            variable.stdDeviation = Math.sqrt(stdDeviationSquared / numValues);
        }

        this.variables = new StandardDeviationOrdering().reverse().sortedCopy(variablesBuilder);
        this.scenarios = new ByVariablesOrdering().sortedCopy(this.run.getMeasurements().keySet());
        this.maxValue = max;
        this.logMinValue = Math.log(min);
        this.logMaxValue = Math.log(max);

        EnumMap<MeasurementType, Integer> digitsBeforeDecimalMap = new EnumMap<MeasurementType, Integer>(
                MeasurementType.class);
        EnumMap<MeasurementType, Integer> decimalPointMap = new EnumMap<MeasurementType, Integer>(
                MeasurementType.class);
        for (MeasurementType measurementType : orderedMeasurementTypes) {
            double maxForType = 0;
            double minForType = Double.POSITIVE_INFINITY;
            for (Entry<Scenario, ScenarioResult> entry : this.run.getMeasurements().entrySet()) {
                double d = entry.getValue().getMeasurementSet(measurementType).medianUnits();
                minForType = Math.min(minForType, d);
                maxForType = Math.max(maxForType, d);
            }

            unitMap.put(measurementType, getUnit(unitMap.get(measurementType), measurementType, minForType));

            divideByMap.put(measurementType, (double) getUnits(measurementType).get(unitMap.get(measurementType)));

            int numDigitsInMin = ceil(Math.log10(minForType));
            decimalDigitsMap.put(measurementType,
                    ceil(Math.max(0, ceil(Math.log10(divideByMap.get(measurementType))) + 3 - numDigitsInMin)));

            digitsBeforeDecimalMap.put(measurementType,
                    Math.max(1, ceil(Math.log10(maxForType / divideByMap.get(measurementType)))));

            decimalPointMap.put(measurementType, decimalDigitsMap.get(measurementType) > 0 ? 1 : 0);

            measurementColumnLengthMap.put(measurementType,
                    Math.max(maxForType > 0 ? digitsBeforeDecimalMap.get(measurementType)
                            + decimalPointMap.get(measurementType) + decimalDigitsMap.get(measurementType) : 1,
                            unitMap.get(measurementType).trim().length()));
        }

        this.printScore = arguments.printScore();
    }

    private String getUnit(String userSuppliedUnit, MeasurementType measurementType, double min) {
        Map<String, Integer> units = getUnits(measurementType);

        if (userSuppliedUnit == null) {
            List<Entry<String, Integer>> entries = UNIT_ORDERING.reverse().sortedCopy(units.entrySet());
            for (Entry<String, Integer> entry : entries) {
                if (min / entry.getValue() >= 1) {
                    return entry.getKey();
                }
            }
            // if no unit works, just use the smallest available unit.
            return entries.get(entries.size() - 1).getKey();
        }

        if (!units.keySet().contains(userSuppliedUnit)) {
            throw new RuntimeException("\"" + unitMap.get(measurementType) + "\" is not a valid unit.");
        }
        return userSuppliedUnit;
    }

    private Map<String, Integer> getUnits(MeasurementType measurementType) {
        Map<String, Integer> units = null;
        for (Entry<Scenario, ScenarioResult> entry : run.getMeasurements().entrySet()) {
            if (units == null) {
                units = entry.getValue().getMeasurementSet(measurementType).getUnitNames();
            } else {
                if (!units.equals(entry.getValue().getMeasurementSet(measurementType).getUnitNames())) {
                    throw new RuntimeException(
                            "measurement sets for run contain multiple, incompatible unit" + " sets.");
                }
            }
        }
        if (units == null) {
            throw new RuntimeException("run has no measurements.");
        }
        if (units.isEmpty()) {
            throw new RuntimeException("no units specified.");
        }
        return units;
    }

    /**
     * A variable and the set of values to which it has been assigned.
     */
    private static class Variable {
        final String name;
        final ImmutableList<String> values;
        final int maxLength;
        double stdDeviation;

        Variable(String name, Collection<String> values) {
            this.name = name;
            this.values = ImmutableList.copyOf(values);

            int maxLen = name.length();
            for (String value : values) {
                maxLen = Math.max(maxLen, value.length());
            }
            this.maxLength = maxLen;
        }

        String get(Scenario scenario) {
            return scenario.getVariables().get(name);
        }

        int index(Scenario scenario) {
            return values.indexOf(get(scenario));
        }

        boolean isInteresting() {
            return values.size() > 1;
        }
    }

    /**
     * Orders the different variables by their standard deviation. This results
     * in an appropriate grouping of output values.
     */
    private static class StandardDeviationOrdering extends Ordering<Variable> {
        public int compare(Variable a, Variable b) {
            return Double.compare(a.stdDeviation, b.stdDeviation);
        }
    }

    /**
     * Orders scenarios by the variables.
     */
    private class ByVariablesOrdering extends Ordering<Scenario> {
        public int compare(Scenario a, Scenario b) {
            for (Variable variable : variables) {
                int aValue = variable.values.indexOf(variable.get(a));
                int bValue = variable.values.indexOf(variable.get(b));
                int diff = aValue - bValue;
                if (diff != 0) {
                    return diff;
                }
            }
            return 0;
        }
    }

    void displayResults() {
        printValues();
        System.out.println();
        printUninterestingVariables();
        printCharCounts();
    }

    private void printCharCounts() {
        int systemOutCharCount = 0;
        int systemErrCharCount = 0;
        for (ScenarioResult scenarioResult : run.getMeasurements().values()) {
            for (MeasurementType measurementType : MeasurementType.values()) {
                MeasurementSet measurementSet = scenarioResult.getMeasurementSet(measurementType);
                if (measurementSet != null) {
                    systemOutCharCount += measurementSet.getSystemOutCharCount();
                    systemErrCharCount += measurementSet.getSystemErrCharCount();
                }
            }
        }
        if (systemOutCharCount > 0 || systemErrCharCount > 0) {
            System.out.println();
            System.out.println("Note: benchmarks printed " + systemOutCharCount + " characters to System.out and "
                    + systemErrCharCount + " characters to System.err." + " Use --debug to see this output.");
        }
    }

    /**
     * Prints a table of values.
     */
    private void printValues() {
        // header
        for (Variable variable : variables) {
            if (variable.isInteresting()) {
                System.out.printf("%" + variable.maxLength + "s ", variable.name);
            }
        }
        // doesn't make sense to show graphs at all for 1
        // scenario, since it leads to vacuous graphs.
        boolean showGraphs = scenarios.size() > 1;

        EnumMap<MeasurementType, String> numbersFormatMap = new EnumMap<MeasurementType, String>(
                MeasurementType.class);
        for (MeasurementType measurementType : orderedMeasurementTypes) {
            if (measurementType != type) {
                System.out.printf("%" + measurementColumnLengthMap.get(measurementType) + "s ",
                        unitMap.get(measurementType).trim());
            }

            numbersFormatMap.put(measurementType, "%" + measurementColumnLengthMap.get(measurementType) + "."
                    + decimalDigitsMap.get(measurementType) + "f" + (type == measurementType ? "" : " "));
        }

        System.out.printf("%" + measurementColumnLengthMap.get(type) + "s", unitMap.get(type).trim());
        if (showGraphs) {
            System.out.print(" linear runtime");
        }
        System.out.println();

        double sumOfLogs = 0.0;

        for (Scenario scenario : scenarios) {
            for (Variable variable : variables) {
                if (variable.isInteresting()) {
                    System.out.printf("%" + variable.maxLength + "s ", variable.get(scenario));
                }
            }
            ScenarioResult measurement = run.getMeasurements().get(scenario);
            sumOfLogs += Math.log(measurement.getMeasurementSet(type).medianUnits());

            for (MeasurementType measurementType : orderedMeasurementTypes) {
                if (measurementType != type) {
                    System.out.printf(numbersFormatMap.get(measurementType),
                            measurement.getMeasurementSet(measurementType).medianUnits()
                                    / divideByMap.get(measurementType));
                }
            }

            System.out.printf(numbersFormatMap.get(type),
                    measurement.getMeasurementSet(type).medianUnits() / divideByMap.get(type));
            if (showGraphs) {
                System.out.printf(" %s", barGraph(measurement.getMeasurementSet(type).medianUnits()));
            }
            System.out.println();
        }

        if (printScore) {
            // arithmetic mean of logs, aka log of geometric mean
            double meanLogUnits = sumOfLogs / scenarios.size();
            System.out.format("%nScore: %.3f%n", scoreTranslation.translate(meanLogUnits));
        }
    }

    /**
     * Prints variables with only one unique value.
     */
    private void printUninterestingVariables() {
        for (Variable variable : variables) {
            if (!variable.isInteresting()) {
                System.out.println(variable.name + ": " + Iterables.getOnlyElement(variable.values));
            }
        }
    }

    /**
     * Returns a string containing a bar of proportional width to the specified
     * value.
     */
    private String barGraph(double value) {
        int graphLength = floor(value / maxValue * barGraphWidth);
        graphLength = Math.max(1, graphLength);
        graphLength = Math.min(barGraphWidth, graphLength);
        return Strings.repeat("=", graphLength);
    }

    @SuppressWarnings("NumericCastThatLosesPrecision")
    private static int floor(double d) {
        return (int) d;
    }

    @SuppressWarnings("NumericCastThatLosesPrecision")
    private static int ceil(double d) {
        return (int) Math.ceil(d);
    }
}