com.google.caliper.report.ReportGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.google.caliper.report.ReportGenerator.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.report;

import com.google.caliper.util.CommandLineParser;
import com.google.caliper.LinearTranslation;
import com.google.caliper.MeasurementType;
import com.google.caliper.plugin.ReportRenderer;
import com.google.caliper.SuiteRun;
import com.google.caliper.Scenario;
import com.google.caliper.ScenarioResult;
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;

/**
 * 
 *
 * @author somebody@google
 * @author Renat.Gilmanov
 */
public final class ReportGenerator {

    private static final int BAR_GRAPH_WIDTH = 30;
    private static final int UNITS_FOR_SCORE_100 = 1;
    private static final int UNITS_FOR_SCORE_10 = 1000000000; // 1 s
    private RendererRegistry registry;
    private String rendererName;

    /**
     * 
     */
    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 SuiteRun 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 CommandLineParser arguments;

    // TODO: refactor
    /**
     * 
     * @param run
     * @param registry
     * @param arguments
     */
    public ReportGenerator(SuiteRun run, RendererRegistry registry, CommandLineParser arguments) {
        this.run = run;
        this.arguments = arguments;

        rendererName = arguments.getReportType();

        this.registry = registry;
        //        File pluginFolder = arguments.getPluginFolder();
        //        this.registry = new DefaultRendererRegistry(
        //                (null != pluginFolder) ? pluginFolder : new File(DEFAULT_PLUGIN_LOCATION),
        //                arguments);

        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.values());
        } 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()));
        }
    }

    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 {

        private final String name;
        private final ImmutableList<String> values;
        private final int maxLength;
        private 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;
        }
    }

    /**
     * 
     */
    public void displayResults() {
        buildReport();
        System.out.println();
        printUninterestingVariables();
    }

    /**
     * Prints a table of values.
     */
    private void buildReport() {
        DefaultReportHeader header = new DefaultReportHeader();

        // --- header (variable.name)
        StringBuilder variableBuilder = new StringBuilder();

        for (Variable variable : variables) {
            if (variable.isInteresting()) {
                variableBuilder.append(String.format("%" + variable.maxLength + "s ", variable.name));
            }
        }

        header.setScaledTitle(variableBuilder.toString());

        EnumMap<MeasurementType, String> numbersFormatMap = new EnumMap<MeasurementType, String>(
                MeasurementType.class);

        // --- header (measurement.type)
        StringBuilder measurementBuilder = new StringBuilder();
        for (MeasurementType measurementType : orderedMeasurementTypes) {
            if (measurementType != type) {
                measurementBuilder
                        .append(String.format("%" + measurementColumnLengthMap.get(measurementType) + "s ",
                                unitMap.get(measurementType).trim()));
            }
            numbersFormatMap.put(measurementType, "%" + measurementColumnLengthMap.get(measurementType) + "."
                    + decimalDigitsMap.get(measurementType) + "f" + (type == measurementType ? "" : " "));
        }

        header.setMeasurementHeader(measurementBuilder.toString());

        // --- unit.type
        header.setValueTitle(
                String.format("%" + measurementColumnLengthMap.get(type) + "s", unitMap.get(type).trim()));

        DefaultBenchmarkReport report = new DefaultBenchmarkReport();
        report.setHeader(header);

        // ----------------------------------------------
        double sumOfLogs = 0.0;

        for (Scenario scenario : scenarios) {
            // --- scenario.name
            StringBuilder nameBuilder = new StringBuilder();
            DefaultReportRow reportRow = new DefaultReportRow();
            report.addRow(reportRow);

            int count = 0;
            for (Variable variable : variables) {
                if (variable.isInteresting()) {
                    if (count > 0) {
                        nameBuilder.append('_');
                        nameBuilder.append(variable.get(scenario));
                    } else {
                        nameBuilder.append(normalizeString(variable.get(scenario)));
                    }
                }
                count++;
            }

            reportRow.setVariableName(nameBuilder.toString());

            ScenarioResult measurement = run.getMeasurements().get(scenario);
            sumOfLogs += Math.log(measurement.getMeasurementSet(type).medianUnits());

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

            // -- grapth
            reportRow.setValue(measurement.getMeasurementSet(type).medianUnits() / divideByMap.get(type));
            reportRow.setValueType(type.toString());
            reportRow.setScaledValue(getScaled(measurement.getMeasurementSet(type).medianUnits()));
        }

        double meanLogUnits = sumOfLogs / scenarios.size();
        // arithmetic mean of logs, aka log of geometric mean
        report.setScore(scoreTranslation.translate(meanLogUnits));

        ReportRenderer reportRenderer = registry.getRenderer(rendererName);
        if (null != reportRenderer) {
            reportRenderer.renderReport(report);
        } else {
            System.out.println("report renderer is null");
        }
    }

    private String normalizeString(String value) {
        final int maxLength = 24;
        return (value.length() > maxLength) ? value.substring(0, maxLength) : value;
    }

    /**
     * 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 int getScaled(double value) {
        int graphLength;
        LinearTranslation t = new LinearTranslation(logMinValue, 1, logMaxValue, BAR_GRAPH_WIDTH);
        graphLength = floor(t.translate(Math.log(value)));
        graphLength = Math.max(1, graphLength);
        graphLength = Math.min(BAR_GRAPH_WIDTH, graphLength);
        return 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);
    }
}