blob: 8449bda0597e287907871089cc73972775f154fc [file] [log] [blame]
/**
* 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);
}
}