Jesse Wilson | 109c128 | 2009-12-08 13:45:25 -0800 | [diff] [blame] | 1 | /** |
| 2 | * Copyright (C) 2009 Google Inc. |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.google.caliper; |
| 18 | |
| 19 | import com.google.common.collect.*; |
| 20 | |
| 21 | import java.util.*; |
| 22 | |
| 23 | /** |
| 24 | * Prints a report containing the tested values and the corresponding |
| 25 | * measurements. Measurements are grouped by variable using indentation. |
| 26 | * Alongside numeric values, quick-glance ascii art bar charts are printed. |
| 27 | * Sample output: |
| 28 | * <pre> |
| 29 | * benchmark d ns logarithmic runtime |
| 30 | * ConcatenationBenchmark 3.141592653589793 4397 |||||||||||||||||||||||| |
| 31 | * ConcatenationBenchmark -0.0 223 ||||||||||||||| |
| 32 | * FormatterBenchmark 3.141592653589793 33999 |||||||||||||||||||||||||||||| |
| 33 | * FormatterBenchmark -0.0 26399 ||||||||||||||||||||||||||||| |
| 34 | * </pre> |
| 35 | */ |
| 36 | final class ConsoleReport { |
| 37 | |
| 38 | private static final int bargraphWidth = 30; |
| 39 | private static final String benchmarkKey = "benchmark"; |
| 40 | private static final String vmKey = "vm"; |
| 41 | |
| 42 | private final List<Parameter> parameters; |
| 43 | private final Result result; |
| 44 | private final List<Run> runs; |
| 45 | |
| 46 | private final double logMaxValue; |
| 47 | private final int decimalDigits; |
| 48 | private final double divideBy; |
| 49 | private final String units; |
| 50 | private final int measurementColumnLength; |
| 51 | |
| 52 | public ConsoleReport(Result result) { |
| 53 | this.result = result; |
| 54 | |
| 55 | double minValue = Double.POSITIVE_INFINITY; |
| 56 | double maxValue = 0; |
| 57 | |
| 58 | Multimap<String, String> nameToValues = LinkedHashMultimap.create(); |
| 59 | List<Parameter> parametersBuilder = new ArrayList<Parameter>(); |
| 60 | for (Map.Entry<Run, Double> entry : result.getMeasurements().entrySet()) { |
| 61 | Run run = entry.getKey(); |
| 62 | double d = entry.getValue(); |
| 63 | |
| 64 | minValue = minValue < d ? minValue : d; |
| 65 | maxValue = maxValue > d ? maxValue : d; |
| 66 | |
| 67 | for (Map.Entry<String, String> parameter : run.getParameters().entrySet()) { |
| 68 | String name = parameter.getKey(); |
| 69 | nameToValues.put(name, parameter.getValue()); |
| 70 | } |
| 71 | |
| 72 | nameToValues.put(benchmarkKey, run.getBenchmarkClass().getSimpleName()); |
| 73 | nameToValues.put(vmKey, run.getVm()); |
| 74 | } |
| 75 | |
| 76 | for (Map.Entry<String, Collection<String>> entry : nameToValues.asMap().entrySet()) { |
| 77 | Parameter parameter = new Parameter(entry.getKey(), entry.getValue()); |
| 78 | parametersBuilder.add(parameter); |
| 79 | } |
| 80 | |
| 81 | /* |
| 82 | * Figure out how much influence each parameter has on the measured value. |
| 83 | * We sum the measurements taken with each value of each parameter. For |
| 84 | * parameters that have influence on the measurement, the sums will differ |
| 85 | * by value. If the parameter has little influence, the sums will be similar |
| 86 | * to one another and close to the overall average. We take the standard |
| 87 | * deviation across each parameters collection of sums. Higher standard |
| 88 | * deviation implies higher influence on the measured result. |
| 89 | */ |
| 90 | double sumOfAllMeasurements = 0; |
| 91 | for (double measurement : result.getMeasurements().values()) { |
| 92 | sumOfAllMeasurements += measurement; |
| 93 | } |
| 94 | for (Parameter parameter : parametersBuilder) { |
| 95 | int numValues = parameter.values.size(); |
| 96 | double[] sumForValue = new double[numValues]; |
| 97 | for (Map.Entry<Run, Double> entry : result.getMeasurements().entrySet()) { |
| 98 | Run run = entry.getKey(); |
| 99 | sumForValue[parameter.index(run)] += entry.getValue(); |
| 100 | } |
| 101 | double mean = sumOfAllMeasurements / sumForValue.length; |
| 102 | double stdDeviationSquared = 0; |
| 103 | for (double value : sumForValue) { |
| 104 | double distance = value - mean; |
| 105 | stdDeviationSquared += distance * distance; |
| 106 | } |
| 107 | parameter.stdDeviation = Math.sqrt(stdDeviationSquared / numValues); |
| 108 | } |
| 109 | |
| 110 | this.parameters = new StandardDeviationOrdering().reverse().sortedCopy(parametersBuilder); |
| 111 | this.runs = new ByParametersOrdering().sortedCopy(result.getMeasurements().keySet()); |
| 112 | this.logMaxValue = Math.log(maxValue); |
| 113 | |
| 114 | int numDigitsInMin = (int) Math.ceil(Math.log10(minValue)); |
| 115 | if (numDigitsInMin > 9) { |
| 116 | divideBy = 1000000000; |
| 117 | decimalDigits = Math.max(0, 9 + 3 - numDigitsInMin); |
| 118 | units = "s"; |
| 119 | } else if (numDigitsInMin > 6) { |
| 120 | divideBy = 1000000; |
| 121 | decimalDigits = Math.max(0, 6 + 3 - numDigitsInMin); |
| 122 | units = "ms"; |
| 123 | } else if (numDigitsInMin > 3) { |
| 124 | divideBy = 1000; |
| 125 | decimalDigits = Math.max(0, 3 + 3 - numDigitsInMin); |
| 126 | units = "us"; |
| 127 | } else { |
| 128 | divideBy = 1; |
| 129 | decimalDigits = 0; |
| 130 | units = "ns"; |
| 131 | } |
Jesse Wilson | e95457f | 2009-12-10 15:45:23 -0800 | [diff] [blame^] | 132 | measurementColumnLength = maxValue > 0 |
| 133 | ? (int) Math.ceil(Math.log10(maxValue / divideBy)) + decimalDigits + 1 |
| 134 | : 1; |
Jesse Wilson | 109c128 | 2009-12-08 13:45:25 -0800 | [diff] [blame] | 135 | } |
| 136 | |
| 137 | /** |
| 138 | * A parameter plus all of its values. |
| 139 | */ |
| 140 | static class Parameter { |
| 141 | final String name; |
| 142 | final ImmutableList<String> values; |
| 143 | final int maxLength; |
| 144 | double stdDeviation; |
| 145 | |
| 146 | public Parameter(String name, Collection<String> values) { |
| 147 | this.name = name; |
| 148 | this.values = ImmutableList.copyOf(values); |
| 149 | |
| 150 | int maxLength = name.length(); |
| 151 | for (String value : values) { |
| 152 | maxLength = Math.max(maxLength, value.length()); |
| 153 | } |
| 154 | this.maxLength = maxLength; |
| 155 | } |
| 156 | |
| 157 | String get(Run run) { |
| 158 | if (benchmarkKey.equals(name)) { |
| 159 | return run.getBenchmarkClass().getSimpleName(); |
| 160 | } else if (vmKey.equals(name)) { |
| 161 | return run.getVm(); |
| 162 | } else { |
| 163 | return run.getParameters().get(name); |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | int index(Run run) { |
| 168 | return values.indexOf(get(run)); |
| 169 | } |
| 170 | |
| 171 | boolean isInteresting() { |
| 172 | return values.size() > 1; |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | /** |
| 177 | * Orders the different parameters by their standard deviation. This results |
| 178 | * in an appropriate grouping of output values. |
| 179 | */ |
| 180 | static class StandardDeviationOrdering extends Ordering<Parameter> { |
| 181 | public int compare(Parameter a, Parameter b) { |
| 182 | return Double.compare(a.stdDeviation, b.stdDeviation); |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Orders runs by the parameters. |
| 188 | */ |
| 189 | class ByParametersOrdering extends Ordering<Run> { |
| 190 | public int compare(Run a, Run b) { |
| 191 | for (Parameter parameter : parameters) { |
| 192 | int aValue = parameter.values.indexOf(parameter.get(a)); |
| 193 | int bValue = parameter.values.indexOf(parameter.get(b)); |
| 194 | int diff = aValue - bValue; |
| 195 | if (diff != 0) { |
| 196 | return diff; |
| 197 | } |
| 198 | } |
| 199 | return 0; |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | void displayResults() { |
| 204 | printValues(); |
| 205 | System.out.println(); |
| 206 | printUninterestingParameters(); |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Prints a table of values. |
| 211 | */ |
| 212 | private void printValues() { |
| 213 | for (Parameter parameter : parameters) { |
| 214 | if (parameter.isInteresting()) { |
| 215 | System.out.printf("%" + parameter.maxLength + "s ", parameter.name); |
| 216 | } |
| 217 | } |
| 218 | System.out.printf("%" + measurementColumnLength + "s logarithmic runtime%n", units); |
| 219 | |
| 220 | String numbersFormat = "%" + measurementColumnLength + "." + decimalDigits + "f %s%n"; |
| 221 | for (Run run : runs) { |
| 222 | for (Parameter parameter : parameters) { |
| 223 | if (parameter.isInteresting()) { |
| 224 | System.out.printf("%" + parameter.maxLength + "s ", parameter.get(run)); |
| 225 | } |
| 226 | } |
| 227 | double measurement = result.getMeasurements().get(run); |
| 228 | System.out.printf(numbersFormat, measurement / divideBy, bargraph(measurement)); |
| 229 | } |
| 230 | } |
| 231 | |
| 232 | /** |
| 233 | * Prints parameters with only one unique value. |
| 234 | */ |
| 235 | private void printUninterestingParameters() { |
| 236 | for (Parameter parameter : parameters) { |
| 237 | if (!parameter.isInteresting()) { |
| 238 | System.out.println(parameter.name + ": " + Iterables.getOnlyElement(parameter.values)); |
| 239 | } |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * Returns a string containing a bar of proportional width to the specified |
| 245 | * value. |
| 246 | */ |
| 247 | private String bargraph(double value) { |
| 248 | double logValue = Math.log(value); |
| 249 | int numChars = (int) ((logValue / logMaxValue) * bargraphWidth); |
| 250 | StringBuilder result = new StringBuilder(numChars); |
| 251 | for (int i = 0; i < numChars; i++) { |
| 252 | result.append("|"); |
| 253 | } |
| 254 | return result.toString(); |
| 255 | } |
| 256 | } |