diff options
author | Jesse Wilson <jessewilson@google.com> | 2010-01-13 17:12:18 -0800 |
---|---|---|
committer | Jesse Wilson <jessewilson@google.com> | 2010-01-13 17:12:18 -0800 |
commit | f062bf49c71013ec19cb71218778299535aceaa8 (patch) | |
tree | 17acfe2a694f55828f64caaae4c25179e61525a8 /src/com/google/caliper | |
parent | 1440b36663f61ebde1952d91b4a1f4c1a27fcefa (diff) | |
download | caliper-f062bf49c71013ec19cb71218778299535aceaa8.tar.gz |
Update Caliper to r71.
Diffstat (limited to 'src/com/google/caliper')
-rw-r--r-- | src/com/google/caliper/Arguments.java | 156 | ||||
-rw-r--r-- | src/com/google/caliper/Benchmark.java | 1 | ||||
-rw-r--r-- | src/com/google/caliper/Caliper.java | 65 | ||||
-rw-r--r-- | src/com/google/caliper/ConfigurationException.java | 8 | ||||
-rw-r--r-- | src/com/google/caliper/ConsoleReport.java | 176 | ||||
-rw-r--r-- | src/com/google/caliper/ExecutionException.java | 28 | ||||
-rw-r--r-- | src/com/google/caliper/InProcessRunner.java | 71 | ||||
-rw-r--r-- | src/com/google/caliper/Param.java | 9 | ||||
-rw-r--r-- | src/com/google/caliper/Parameter.java | 95 | ||||
-rw-r--r-- | src/com/google/caliper/Result.java | 37 | ||||
-rw-r--r-- | src/com/google/caliper/Run.java | 76 | ||||
-rw-r--r-- | src/com/google/caliper/Runner.java | 363 | ||||
-rw-r--r-- | src/com/google/caliper/Scenario.java | 81 | ||||
-rw-r--r-- | src/com/google/caliper/ScenarioSelection.java | 208 | ||||
-rw-r--r-- | src/com/google/caliper/SimpleBenchmark.java | 28 | ||||
-rw-r--r-- | src/com/google/caliper/TypeConverter.java | 58 | ||||
-rw-r--r-- | src/com/google/caliper/UserException.java | 141 | ||||
-rw-r--r-- | src/com/google/caliper/Xml.java | 108 |
18 files changed, 1194 insertions, 515 deletions
diff --git a/src/com/google/caliper/Arguments.java b/src/com/google/caliper/Arguments.java new file mode 100644 index 0000000..e5ff0f7 --- /dev/null +++ b/src/com/google/caliper/Arguments.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2010 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.UserException.DisplayUsageException; +import com.google.caliper.UserException.MalformedParameterException; +import com.google.caliper.UserException.MultipleBenchmarkClassesException; +import com.google.caliper.UserException.NoBenchmarkClassException; +import com.google.caliper.UserException.UnrecognizedOptionException; +import com.google.common.collect.Iterators; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Parse command line arguments for the runner and in-process runner. + */ +public final class Arguments { + private String suiteClassName; + + /** JVMs to run in the benchmark */ + private final Set<String> userVms = new LinkedHashSet<String>(); + + /** + * Parameter values specified by the user on the command line. Parameters with + * no value in this multimap will get their values from the benchmark suite. + */ + private final Multimap<String, String> userParameters = LinkedHashMultimap.create(); + + private long warmupMillis = 5000; + private long runMillis = 5000; + + /** The URL to post benchmark results to. */ + private String postHost = "http://microbenchmarks.appspot.com/run/"; + + public String getSuiteClassName() { + return suiteClassName; + } + + public Set<String> getUserVms() { + return userVms; + } + + public Multimap<String, String> getUserParameters() { + return userParameters; + } + + public long getWarmupMillis() { + return warmupMillis; + } + + public long getRunMillis() { + return runMillis; + } + + public String getPostHost() { + return postHost; + } + + public static Arguments parse(String[] argsArray) { + Arguments result = new Arguments(); + + Iterator<String> args = Iterators.forArray(argsArray); + while (args.hasNext()) { + String arg = args.next(); + + if ("--help".equals(arg)) { + throw new DisplayUsageException(); + } + + if ("--postHost".equals(arg)) { + result.postHost = args.next(); + + } else if (arg.startsWith("-D")) { + int equalsSign = arg.indexOf('='); + if (equalsSign == -1) { + throw new MalformedParameterException(arg); + } + String name = arg.substring(2, equalsSign); + String value = arg.substring(equalsSign + 1); + result.userParameters.put(name, value); + + } else if ("--warmupMillis".equals(arg)) { + result.warmupMillis = Long.parseLong(args.next()); + + } else if ("--runMillis".equals(arg)) { + result.runMillis = Long.parseLong(args.next()); + + } else if ("--vm".equals(arg)) { + result.userVms.add(args.next()); + + } else if (arg.startsWith("-")) { + throw new UnrecognizedOptionException(arg); + + } else { + if (result.suiteClassName != null) { + throw new MultipleBenchmarkClassesException(result.suiteClassName, arg); + } + result.suiteClassName = arg; + } + } + + if (result.suiteClassName == null) { + throw new NoBenchmarkClassException(); + } + + return result; + } + + public static void printUsage() { + Arguments defaults = new Arguments(); + + System.out.println(); + System.out.println("Usage: Runner [OPTIONS...] <benchmark>"); + System.out.println(); + System.out.println(" <benchmark>: a benchmark class or suite"); + System.out.println(); + System.out.println("OPTIONS"); + System.out.println(); + System.out.println(" -D<param>=<value>: fix a benchmark parameter to a given value."); + System.out.println(" When multiple values for the same parameter are given (via"); + System.out.println(" multiple --Dx=y args), all supplied values are used."); + System.out.println(); + System.out.println(" --inProcess: run the benchmark in the same JVM rather than spawning"); + System.out.println(" another with the same classpath. By default each benchmark is"); + System.out.println(" run in a separate VM"); + System.out.println(); + System.out.println(" --postHost <host>: the URL to post benchmark results to, or \"none\""); + System.out.println(" to skip posting results to the web."); + System.out.println(" default value: " + defaults.postHost); + System.out.println(); + System.out.println(" --warmupMillis <millis>: duration to warmup each benchmark"); + System.out.println(); + System.out.println(" --runMillis <millis>: duration to execute each benchmark"); + System.out.println(); + System.out.println(" --vm <vm>: executable to test benchmark on"); + + // adding new options? don't forget to update executeForked() + } +} diff --git a/src/com/google/caliper/Benchmark.java b/src/com/google/caliper/Benchmark.java index 19426e6..b5d35f9 100644 --- a/src/com/google/caliper/Benchmark.java +++ b/src/com/google/caliper/Benchmark.java @@ -16,7 +16,6 @@ package com.google.caliper; -import java.lang.reflect.Method; import java.util.Map; import java.util.Set; diff --git a/src/com/google/caliper/Caliper.java b/src/com/google/caliper/Caliper.java index 315431f..6c9d625 100644 --- a/src/com/google/caliper/Caliper.java +++ b/src/com/google/caliper/Caliper.java @@ -26,7 +26,7 @@ class Caliper { private final long warmupNanos; private final long runNanos; - public Caliper(long warmupMillis, long runMillis) { + Caliper(long warmupMillis, long runMillis) { checkArgument(warmupMillis > 50); checkArgument(runMillis > 50); @@ -37,15 +37,24 @@ class Caliper { public double warmUp(TimedRunnable timedRunnable) throws Exception { long startNanos = System.nanoTime(); long endNanos = startNanos + warmupNanos; - int trials = 0; long currentNanos; + int netReps = 0; + int reps = 1; + + /* + * Run progressively more reps at a time until we cross our warmup + * threshold. This way any just-in-time compiler will be comfortable running + * multiple iterations of our measurement method. + */ while ((currentNanos = System.nanoTime()) < endNanos) { - timedRunnable.run(1); - trials++; + timedRunnable.run(reps); + netReps += reps; + reps *= 2; } - double nanosPerExecution = (currentNanos - startNanos) / trials; + + double nanosPerExecution = (currentNanos - startNanos) / (double) netReps; if (nanosPerExecution > 1000000000 || nanosPerExecution < 2) { - throw new ConfigurationException("Runtime out of range"); + throw new ConfigurationException("Runtime " + nanosPerExecution + " out of range"); } return nanosPerExecution; } @@ -54,15 +63,51 @@ class Caliper { * In the run proper, we predict how extrapolate based on warmup how many * runs we're going to need, and run them all in a single batch. */ - public double run(TimedRunnable test, double estimatedNanosPerTrial) throws Exception { + public double run(TimedRunnable test, double estimatedNanosPerTrial) + throws Exception { + @SuppressWarnings("NumericCastThatLosesPrecision") int trials = (int) (runNanos / estimatedNanosPerTrial); if (trials == 0) { trials = 1; } + + double nanosPerTrial = measure(test, trials); + + // if the runtime was in the expected range, return it. We're good. + if (isPlausible(estimatedNanosPerTrial, nanosPerTrial)) { + return nanosPerTrial; + } + + // The runtime was outside of the expected range. Perhaps the VM is inlining + // things too aggressively? We'll run more rounds to confirm that the + // runtime scales with the number of trials. + double nanosPerTrial2 = measure(test, trials * 4); + if (isPlausible(nanosPerTrial, nanosPerTrial2)) { + return nanosPerTrial; + } + + throw new ConfigurationException("Measurement error: " + + "runtime isn't proportional to the number of repetitions!"); + } + + /** + * Returns true if the given measurement is consistent with the expected + * measurement. + */ + private boolean isPlausible(double expected, double measurement) { + double ratio = measurement / expected; + return ratio > 0.5 && ratio < 2.0; + } + + private double measure(TimedRunnable test, int trials) throws Exception { + prepareForTest(); long startNanos = System.nanoTime(); test.run(trials); - long endNanos = System.nanoTime(); - estimatedNanosPerTrial = (endNanos - startNanos) / trials; - return estimatedNanosPerTrial; + return (System.nanoTime() - startNanos) / (double) trials; + } + + private void prepareForTest() { + System.gc(); + System.gc(); } }
\ No newline at end of file diff --git a/src/com/google/caliper/ConfigurationException.java b/src/com/google/caliper/ConfigurationException.java index 5ad7bde..c4a35ec 100644 --- a/src/com/google/caliper/ConfigurationException.java +++ b/src/com/google/caliper/ConfigurationException.java @@ -19,13 +19,15 @@ package com.google.caliper; /** * Thrown upon occurrence of a configuration error. */ -public final class ConfigurationException extends RuntimeException { +final class ConfigurationException extends RuntimeException { - public ConfigurationException(String s) { + ConfigurationException(String s) { super(s); } - public ConfigurationException(Throwable cause) { + ConfigurationException(Throwable cause) { super(cause); } + + private static final long serialVersionUID = 0; } diff --git a/src/com/google/caliper/ConsoleReport.java b/src/com/google/caliper/ConsoleReport.java index b367ec4..e59ceb6 100644 --- a/src/com/google/caliper/ConsoleReport.java +++ b/src/com/google/caliper/ConsoleReport.java @@ -16,8 +16,11 @@ package com.google.caliper; -import com.google.common.collect.*; - +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.Collection; import java.util.List; @@ -39,13 +42,11 @@ import java.util.Map; final class ConsoleReport { private static final int bargraphWidth = 30; - private static final String vmKey = "vm"; - private final List<Parameter> parameters; - private final Result result; - private final List<Run> runs; + private final List<Variable> variables; + private final Run run; + private final List<Scenario> scenarios; - private final double minValue; private final double maxValue; private final double logMaxValue; private final int decimalDigits; @@ -53,53 +54,51 @@ final class ConsoleReport { private final String units; private final int measurementColumnLength; - public ConsoleReport(Result result) { - this.result = result; + ConsoleReport(Run run) { + this.run = run; - double minValue = Double.POSITIVE_INFINITY; - double maxValue = 0; + double min = Double.POSITIVE_INFINITY; + double max = 0; Multimap<String, String> nameToValues = LinkedHashMultimap.create(); - List<Parameter> parametersBuilder = new ArrayList<Parameter>(); - for (Map.Entry<Run, Double> entry : result.getMeasurements().entrySet()) { - Run run = entry.getKey(); + List<Variable> variablesBuilder = new ArrayList<Variable>(); + for (Map.Entry<Scenario, Double> entry : run.getMeasurements().entrySet()) { + Scenario scenario = entry.getKey(); double d = entry.getValue(); - minValue = Math.min(minValue, d); - maxValue = Math.max(maxValue, d); + min = Math.min(min, d); + max = Math.max(max, d); - for (Map.Entry<String, String> parameter : run.getParameters().entrySet()) { - String name = parameter.getKey(); - nameToValues.put(name, parameter.getValue()); + for (Map.Entry<String, String> variable : scenario.getVariables().entrySet()) { + String name = variable.getKey(); + nameToValues.put(name, variable.getValue()); } - - nameToValues.put(vmKey, run.getVm()); } for (Map.Entry<String, Collection<String>> entry : nameToValues.asMap().entrySet()) { - Parameter parameter = new Parameter(entry.getKey(), entry.getValue()); - parametersBuilder.add(parameter); + Variable variable = new Variable(entry.getKey(), entry.getValue()); + variablesBuilder.add(variable); } /* - * Figure out how much influence each parameter has on the measured value. - * We sum the measurements taken with each value of each parameter. For - * parameters that have influence on the measurement, the sums will differ - * by value. If the parameter has little influence, the sums will be similar + * 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 parameters collection of sums. Higher standard + * deviation across each variable's collection of sums. Higher standard * deviation implies higher influence on the measured result. */ double sumOfAllMeasurements = 0; - for (double measurement : result.getMeasurements().values()) { + for (double measurement : run.getMeasurements().values()) { sumOfAllMeasurements += measurement; } - for (Parameter parameter : parametersBuilder) { - int numValues = parameter.values.size(); + for (Variable variable : variablesBuilder) { + int numValues = variable.values.size(); double[] sumForValue = new double[numValues]; - for (Map.Entry<Run, Double> entry : result.getMeasurements().entrySet()) { - Run run = entry.getKey(); - sumForValue[parameter.index(run)] += entry.getValue(); + for (Map.Entry<Scenario, Double> entry : run.getMeasurements().entrySet()) { + Scenario scenario = entry.getKey(); + sumForValue[variable.index(scenario)] += entry.getValue(); } double mean = sumOfAllMeasurements / sumForValue.length; double stdDeviationSquared = 0; @@ -107,16 +106,15 @@ final class ConsoleReport { double distance = value - mean; stdDeviationSquared += distance * distance; } - parameter.stdDeviation = Math.sqrt(stdDeviationSquared / numValues); + variable.stdDeviation = Math.sqrt(stdDeviationSquared / numValues); } - this.parameters = new StandardDeviationOrdering().reverse().sortedCopy(parametersBuilder); - this.runs = new ByParametersOrdering().sortedCopy(result.getMeasurements().keySet()); - this.minValue = minValue; - this.maxValue = maxValue; - this.logMaxValue = Math.log(maxValue); + this.variables = new StandardDeviationOrdering().reverse().sortedCopy(variablesBuilder); + this.scenarios = new ByVariablesOrdering().sortedCopy(run.getMeasurements().keySet()); + this.maxValue = max; + this.logMaxValue = Math.log(max); - int numDigitsInMin = (int) Math.ceil(Math.log10(minValue)); + int numDigitsInMin = ceil(Math.log10(min)); if (numDigitsInMin > 9) { divideBy = 1000000000; decimalDigits = Math.max(0, 9 + 3 - numDigitsInMin); @@ -134,41 +132,37 @@ final class ConsoleReport { decimalDigits = 0; units = "ns"; } - measurementColumnLength = maxValue > 0 - ? (int) Math.ceil(Math.log10(maxValue / divideBy)) + decimalDigits + 1 + measurementColumnLength = max > 0 + ? ceil(Math.log10(max / divideBy)) + decimalDigits + 1 : 1; } /** - * A parameter plus all of its values. + * A variable and the set of values to which it has been assigned. */ - static class Parameter { + private static class Variable { final String name; final ImmutableList<String> values; final int maxLength; double stdDeviation; - public Parameter(String name, Collection<String> values) { + Variable(String name, Collection<String> values) { this.name = name; this.values = ImmutableList.copyOf(values); - int maxLength = name.length(); + int maxLen = name.length(); for (String value : values) { - maxLength = Math.max(maxLength, value.length()); + maxLen = Math.max(maxLen, value.length()); } - this.maxLength = maxLength; + this.maxLength = maxLen; } - String get(Run run) { - if (vmKey.equals(name)) { - return run.getVm(); - } else { - return run.getParameters().get(name); - } + String get(Scenario scenario) { + return scenario.getVariables().get(name); } - int index(Run run) { - return values.indexOf(get(run)); + int index(Scenario scenario) { + return values.indexOf(get(scenario)); } boolean isInteresting() { @@ -177,23 +171,23 @@ final class ConsoleReport { } /** - * Orders the different parameters by their standard deviation. This results + * Orders the different variables by their standard deviation. This results * in an appropriate grouping of output values. */ - static class StandardDeviationOrdering extends Ordering<Parameter> { - public int compare(Parameter a, Parameter b) { + private static class StandardDeviationOrdering extends Ordering<Variable> { + public int compare(Variable a, Variable b) { return Double.compare(a.stdDeviation, b.stdDeviation); } } /** - * Orders runs by the parameters. + * Orders scenarios by the variables. */ - class ByParametersOrdering extends Ordering<Run> { - public int compare(Run a, Run b) { - for (Parameter parameter : parameters) { - int aValue = parameter.values.indexOf(parameter.get(a)); - int bValue = parameter.values.indexOf(parameter.get(b)); + 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; @@ -206,7 +200,7 @@ final class ConsoleReport { void displayResults() { printValues(); System.out.println(); - printUninterestingParameters(); + printUninterestingVariables(); } /** @@ -214,33 +208,33 @@ final class ConsoleReport { */ private void printValues() { // header - for (Parameter parameter : parameters) { - if (parameter.isInteresting()) { - System.out.printf("%" + parameter.maxLength + "s ", parameter.name); + for (Variable variable : variables) { + if (variable.isInteresting()) { + System.out.printf("%" + variable.maxLength + "s ", variable.name); } } System.out.printf("%" + measurementColumnLength + "s logarithmic runtime%n", units); // rows String numbersFormat = "%" + measurementColumnLength + "." + decimalDigits + "f %s%n"; - for (Run run : runs) { - for (Parameter parameter : parameters) { - if (parameter.isInteresting()) { - System.out.printf("%" + parameter.maxLength + "s ", parameter.get(run)); + for (Scenario scenario : scenarios) { + for (Variable variable : variables) { + if (variable.isInteresting()) { + System.out.printf("%" + variable.maxLength + "s ", variable.get(scenario)); } } - double measurement = result.getMeasurements().get(run); + double measurement = run.getMeasurements().get(scenario); System.out.printf(numbersFormat, measurement / divideBy, bargraph(measurement)); } } /** - * Prints parameters with only one unique value. + * Prints variables with only one unique value. */ - private void printUninterestingParameters() { - for (Parameter parameter : parameters) { - if (!parameter.isInteresting()) { - System.out.println(parameter.name + ": " + Iterables.getOnlyElement(parameter.values)); + private void printUninterestingVariables() { + for (Variable variable : variables) { + if (!variable.isInteresting()) { + System.out.println(variable.name + ": " + Iterables.getOnlyElement(variable.values)); } } } @@ -250,17 +244,27 @@ final class ConsoleReport { * value. */ private String bargraph(double value) { - int numLinearChars = (int) ((value / maxValue) * bargraphWidth); + int numLinearChars = floor(value / maxValue * bargraphWidth); double logValue = Math.log(value); - int numChars = (int) ((logValue / logMaxValue) * bargraphWidth); - StringBuilder result = new StringBuilder(numChars); + int numChars = floor(logValue / logMaxValue * bargraphWidth); + StringBuilder sb = new StringBuilder(numChars); for (int i = 0; i < numLinearChars; i++) { - result.append("X"); + sb.append("X"); } for (int i = numLinearChars; i < numChars; i++) { - result.append("|"); + sb.append("|"); } - return result.toString(); + return sb.toString(); + } + + @SuppressWarnings("NumericCastThatLosesPrecision") + private static int floor(double d) { + return (int) d; + } + + @SuppressWarnings("NumericCastThatLosesPrecision") + private static int ceil(double d) { + return (int) Math.ceil(d); } } diff --git a/src/com/google/caliper/ExecutionException.java b/src/com/google/caliper/ExecutionException.java deleted file mode 100644 index 7d8a592..0000000 --- a/src/com/google/caliper/ExecutionException.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; - -/** - * Thrown upon occurrence of a runtime failure during test construction or - * execution. - */ -public final class ExecutionException extends RuntimeException { - - public ExecutionException(Throwable throwable) { - super(throwable); - } -} diff --git a/src/com/google/caliper/InProcessRunner.java b/src/com/google/caliper/InProcessRunner.java new file mode 100644 index 0000000..33e9e00 --- /dev/null +++ b/src/com/google/caliper/InProcessRunner.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 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.UserException.CantCustomizeInProcessVmException; +import com.google.caliper.UserException.ExceptionFromUserCodeException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +/** + * Executes a benchmark in the current VM. + */ +final class InProcessRunner { + + public void run(String... args) { + Arguments arguments = Arguments.parse(args); + + if (!arguments.getUserVms().isEmpty()) { + throw new CantCustomizeInProcessVmException(); + } + + ScenarioSelection scenarioSelection = new ScenarioSelection(arguments); + + PrintStream resultStream = System.out; + System.setOut(nullPrintStream()); + System.setErr(nullPrintStream()); + + try { + Caliper caliper = new Caliper(arguments.getWarmupMillis(), arguments.getRunMillis()); + + for (Scenario scenario : scenarioSelection.select()) { + TimedRunnable timedRunnable = scenarioSelection.createBenchmark(scenario); + double warmupNanosPerTrial = caliper.warmUp(timedRunnable); + double nanosPerTrial = caliper.run(timedRunnable, warmupNanosPerTrial); + resultStream.println(nanosPerTrial); + } + } catch (Exception e) { + throw new ExceptionFromUserCodeException(e); + } + } + + public static void main(String... args) { + try { + new InProcessRunner().run(args); + } catch (UserException e) { + e.display(); // TODO: send this to the host process + System.exit(1); + } + } + + public PrintStream nullPrintStream() { + return new PrintStream(new OutputStream() { + public void write(int b) throws IOException {} + }); + } +} diff --git a/src/com/google/caliper/Param.java b/src/com/google/caliper/Param.java index 28d3588..bea0269 100644 --- a/src/com/google/caliper/Param.java +++ b/src/com/google/caliper/Param.java @@ -26,4 +26,11 @@ import java.lang.annotation.Target; */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface Param {} +public @interface Param { + /** + * One or more default values, as strings, that this parameter should be given if none are + * specified on the command line. If values are specified on the command line, the defaults given + * here are all ignored. + */ + String[] value() default {}; +} diff --git a/src/com/google/caliper/Parameter.java b/src/com/google/caliper/Parameter.java index 1ba77b5..caca252 100644 --- a/src/com/google/caliper/Parameter.java +++ b/src/com/google/caliper/Parameter.java @@ -16,8 +16,19 @@ package com.google.caliper; -import java.lang.reflect.*; -import java.util.*; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; /** * A parameter in a {@link SimpleBenchmark}. @@ -35,22 +46,35 @@ abstract class Parameter<T> { */ public static Map<String, Parameter<?>> forClass(Class<? extends Benchmark> suiteClass) { Map<String, Parameter<?>> parameters = new TreeMap<String, Parameter<?>>(); - for (final Field field : suiteClass.getDeclaredFields()) { + for (Field field : suiteClass.getDeclaredFields()) { if (field.isAnnotationPresent(Param.class)) { field.setAccessible(true); - Parameter parameter = Parameter.forField(suiteClass, field); + Parameter<?> parameter = forField(suiteClass, field); parameters.put(parameter.getName(), parameter); } } return parameters; } - public static Parameter forField( + private static Parameter<?> forField( Class<? extends Benchmark> suiteClass, final Field field) { - Parameter result = null; + // First check for String values on the annotation itself + final Object[] defaults = field.getAnnotation(Param.class).value(); + if (defaults.length > 0) { + return new Parameter<Object>(field) { + @Override public Collection<Object> values() throws Exception { + return Arrays.asList(defaults); + } + }; + // TODO: or should we continue so we can give an error/warning if params are also give in a + // method or field? + } + + Parameter<?> result = null; Type returnType = null; Member member = null; + // Now check for a fooValues() method try { final Method valuesMethod = suiteClass.getDeclaredMethod(field.getName() + "Values"); valuesMethod.setAccessible(true); @@ -58,13 +82,14 @@ abstract class Parameter<T> { returnType = valuesMethod.getGenericReturnType(); result = new Parameter<Object>(field) { @SuppressWarnings("unchecked") // guarded below - public Collection<Object> values() throws Exception { + @Override public Collection<Object> values() throws Exception { return (Collection<Object>) valuesMethod.invoke(null); } }; } catch (NoSuchMethodException ignored) { } + // Now check for a fooValues field try { final Field valuesField = suiteClass.getDeclaredField(field.getName() + "Values"); valuesField.setAccessible(true); @@ -75,36 +100,58 @@ abstract class Parameter<T> { returnType = valuesField.getGenericType(); result = new Parameter<Object>(field) { @SuppressWarnings("unchecked") // guarded below - public Collection<Object> values() throws Exception { + @Override public Collection<Object> values() throws Exception { return (Collection<Object>) valuesField.get(null); } }; } catch (NoSuchFieldException ignored) { } - if (result == null) { - throw new ConfigurationException("No values member defined for " + field); + if (member != null && !Modifier.isStatic(member.getModifiers())) { + throw new ConfigurationException("Values member must be static " + member); + } + + // If there isn't a values member but the parameter is an enum, we default + // to EnumSet.allOf. + if (member == null && field.getType().isEnum()) { + returnType = Collection.class; + result = new Parameter<Object>(field) { + // TODO: figure out the simplest way to make this compile and be green in IDEA too + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType", "RedundantCast"}) + // guarded above + @Override public Collection<Object> values() throws Exception { + Set<Enum> set = EnumSet.allOf((Class<Enum>) field.getType()); + return (Collection) set; + } + }; } - if (!Modifier.isStatic(member.getModifiers())) { - throw new ConfigurationException("Values member must be static " + member); + if (result == null) { + return new Parameter<Object>(field) { + @Override public Collection<Object> values() { + // TODO: need tests to make sure this fails properly when no cmdline params given and + // works properly when they are given + return Collections.emptySet(); + } + }; + } else if (!isValidReturnType(returnType)) { + throw new ConfigurationException("Invalid return type " + returnType + + " for values member " + member + "; must be Collection"); } + return result; + } - // validate return type - boolean valid = false; + private static boolean isValidReturnType(Type returnType) { + if (returnType == Collection.class) { + return true; + } if (returnType instanceof ParameterizedType) { ParameterizedType type = (ParameterizedType) returnType; if (type.getRawType() == Collection.class) { - valid = true; + return true; } } - - if (!valid) { - throw new ConfigurationException("Invalid return type " + returnType - + " for values member " + member + "; must be Collection"); - } - - return result; + return false; } /** @@ -129,7 +176,7 @@ abstract class Parameter<T> { /** * Returns the field's name. */ - public String getName() { + String getName() { return field.getName(); } -}
\ No newline at end of file +} diff --git a/src/com/google/caliper/Result.java b/src/com/google/caliper/Result.java deleted file mode 100644 index 888a9f4..0000000 --- a/src/com/google/caliper/Result.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * 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.common.collect.ImmutableMap; - -import java.util.Map; - -/** - * The complete result of a benchmark suite run. - */ -final class Result { - - private final ImmutableMap<Run, Double> measurements; - - public Result(Map<Run, Double> measurements) { - this.measurements = ImmutableMap.copyOf(measurements); - } - - public ImmutableMap<Run, Double> getMeasurements() { - return measurements; - } -} diff --git a/src/com/google/caliper/Run.java b/src/com/google/caliper/Run.java index a9109de..f2e71de 100644 --- a/src/com/google/caliper/Run.java +++ b/src/com/google/caliper/Run.java @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2009 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,33 +16,77 @@ package com.google.caliper; -import com.google.common.collect.ImmutableMap; - -import java.lang.reflect.Method; +import java.io.Serializable; +import java.util.Date; +import java.util.LinkedHashMap; import java.util.Map; /** - * A configured benchmark. + * The complete result of a benchmark suite run. + * + * <p>Gwt-safe. */ -final class Run { +public final class Run + implements Serializable /* for GWT Serialization */ { + + private /*final*/ Map<Scenario, Double> measurements; + private /*final*/ String benchmarkName; + private /*final*/ String executedByUuid; + private /*final*/ long executedTimestamp; - private final ImmutableMap<String, String> parameters; - private final String vm; + // TODO: add more run properites such as checksums of the executed code - public Run(Map<String, String> parameters, String vm) { - this.parameters = ImmutableMap.copyOf(parameters); - this.vm = vm; + public Run(Map<Scenario, Double> measurements, + String benchmarkName, String executedByUuid, Date executedTimestamp) { + if (benchmarkName == null || executedByUuid == null || executedTimestamp == null) { + throw new NullPointerException(); + } + + this.measurements = new LinkedHashMap<Scenario, Double>(measurements); + this.benchmarkName = benchmarkName; + this.executedByUuid = executedByUuid; + this.executedTimestamp = executedTimestamp.getTime(); + } + + public Map<Scenario, Double> getMeasurements() { + return measurements; } - public ImmutableMap<String, String> getParameters() { - return parameters; + public String getBenchmarkName() { + return benchmarkName; } - public String getVm() { - return vm; + public String getExecutedByUuid() { + return executedByUuid; + } + + public Date getExecutedTimestamp() { + return new Date(executedTimestamp); + } + + @Override public boolean equals(Object o) { + if (o instanceof Run) { + Run that = (Run) o; + return measurements.equals(that.measurements) + && benchmarkName.equals(that.benchmarkName) + && executedByUuid.equals(that.executedByUuid) + && executedTimestamp == that.executedTimestamp; + } + + return false; + } + + @Override public int hashCode() { + int result = measurements.hashCode(); + result = result * 37 + benchmarkName.hashCode(); + result = result * 37 + executedByUuid.hashCode(); + result = result * 37 + (int) ((executedTimestamp >> 32) ^ executedTimestamp); + return result; } @Override public String toString() { - return "Run" + parameters; + return measurements.toString(); } + + private Run() {} // for GWT Serialization } diff --git a/src/com/google/caliper/Runner.java b/src/com/google/caliper/Runner.java index 72442db..e359df8 100644 --- a/src/com/google/caliper/Runner.java +++ b/src/com/google/caliper/Runner.java @@ -16,180 +16,113 @@ package com.google.caliper; +import com.google.caliper.UserException.ExceptionFromUserCodeException; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Multimap; - +import com.google.common.collect.ImmutableMap.Builder; +import com.google.common.collect.ObjectArrays; import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.UUID; /** * Creates, executes and reports benchmark runs. */ public final class Runner { - private String suiteClassName; - private Benchmark suite; - - /** Effective parameters to run in the benchmark. */ - private Multimap<String, String> parameters = LinkedHashMultimap.create(); - - /** JVMs to run in the benchmark */ - private Set<String> userVms = new LinkedHashSet<String>(); - - /** - * Parameter values specified by the user on the command line. Parameters with - * no value in this multimap will get their values from the benchmark suite. - */ - private Multimap<String, String> userParameters = LinkedHashMultimap.create(); - - /** - * True if each benchmark should run in process. - */ - private boolean inProcess; - - private long warmupMillis = 5000; - private long runMillis = 5000; + /** Command line arguments to the process */ + private Arguments arguments; + private ScenarioSelection scenarioSelection; /** - * Sets the named parameter to the specified value. This value will replace - * the benchmark suite's default values for the parameter. Multiple calls to - * this method will cause benchmarks for each value to be run. + * Returns the UUID of the executing host. Multiple runs by the same user on + * the same machine should yield the same result. */ - void setParameter(String name, String value) { - userParameters.put(name, value); - } - - private void prepareSuite() { + private String getExecutedByUuid() { try { - @SuppressWarnings("unchecked") // guarded by the if statement that follows - Class<? extends Benchmark> suiteClass - = (Class<? extends Benchmark>) Class.forName(suiteClassName); - if (!Benchmark.class.isAssignableFrom(suiteClass)) { - throw new ConfigurationException(suiteClass + " is not a benchmark suite."); + File dotCaliperRc = new File(System.getProperty("user.home"), ".caliperrc"); + Properties properties = new Properties(); + if (dotCaliperRc.exists()) { + properties.load(new FileInputStream(dotCaliperRc)); } - Constructor<? extends Benchmark> constructor = suiteClass.getDeclaredConstructor(); - suite = constructor.newInstance(); - } catch (InvocationTargetException e) { - throw new ExecutionException(e.getCause()); - } catch (Exception e) { - throw new ConfigurationException(e); - } - } - - private void prepareParameters() { - for (String key : suite.parameterNames()) { - // first check if the user has specified values - Collection<String> userValues = userParameters.get(key); - if (!userValues.isEmpty()) { - parameters.putAll(key, userValues); - // TODO: type convert 'em to validate? - - } else { // otherwise use the default values from the suite - Set<String> values = suite.parameterValues(key); - if (values.isEmpty()) { - throw new ConfigurationException(key + " has no values"); - } - parameters.putAll(key, values); + String userUuid = properties.getProperty("userUuid"); + if (userUuid == null) { + userUuid = UUID.randomUUID().toString(); + properties.setProperty("userUuid", userUuid); + properties.store(new FileOutputStream(dotCaliperRc), ""); } + + return userUuid; + } catch (IOException e) { + throw new RuntimeException(e); } } - private ImmutableSet<String> defaultVms() { - return "Dalvik".equals(System.getProperty("java.vm.name")) - ? ImmutableSet.of("dalvikvm") - : ImmutableSet.of("java"); + public void run(String... args) { + this.arguments = Arguments.parse(args); + this.scenarioSelection = new ScenarioSelection(arguments); + Run run = runOutOfProcess(); + new ConsoleReport(run).displayResults(); + postResults(run); } - /** - * Returns a complete set of runs with every combination of values and - * benchmark classes. - */ - private List<Run> createRuns() throws Exception { - List<RunBuilder> builders = new ArrayList<RunBuilder>(); - - // create runs for each VMs - Set<String> vms = userVms.isEmpty() - ? defaultVms() - : userVms; - for (String vm : vms) { - RunBuilder runBuilder = new RunBuilder(); - runBuilder.vm = vm; - builders.add(runBuilder); + private void postResults(Run run) { + String postHost = arguments.getPostHost(); + if ("none".equals(postHost)) { + return; } - for (Map.Entry<String, Collection<String>> parameter : parameters.asMap().entrySet()) { - Iterator<String> values = parameter.getValue().iterator(); - if (!values.hasNext()) { - throw new ConfigurationException("Not enough values for " + parameter); - } - - String key = parameter.getKey(); - - String firstValue = values.next(); - for (RunBuilder builder : builders) { - builder.parameters.put(key, firstValue); + try { + URL url = new URL(postHost + run.getExecutedByUuid() + "/" + run.getBenchmarkName()); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setDoOutput(true); + Xml.runToXml(run, urlConnection.getOutputStream()); + if (urlConnection.getResponseCode() == 200) { + System.out.println(""); + System.out.println("View current and previous benchmark results online:"); + System.out.println(" " + url); + return; } - // multiply the size of the specs by the number of alternate values - int size = builders.size(); - while (values.hasNext()) { - String alternate = values.next(); - for (int s = 0; s < size; s++) { - RunBuilder copy = builders.get(s).copy(); - copy.parameters.put(key, alternate); - builders.add(copy); - } + System.out.println("Posting to " + postHost + " failed: " + + urlConnection.getResponseMessage()); + BufferedReader reader = new BufferedReader( + new InputStreamReader(urlConnection.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); } - } - - List<Run> result = new ArrayList<Run>(); - for (RunBuilder builder : builders) { - result.add(builder.build()); - } - - return result; - } - - static class RunBuilder { - Map<String, String> parameters = new LinkedHashMap<String, String>(); - String vm; - - RunBuilder copy() { - RunBuilder result = new RunBuilder(); - result.parameters.putAll(parameters); - result.vm = vm; - return result; - } - - public Run build() { - return new Run(parameters, vm); + } catch (IOException e) { + throw new RuntimeException(e); } } - private double executeForked(Run run) { + private double executeForked(Scenario scenario) { ProcessBuilder builder = new ProcessBuilder(); List<String> command = builder.command(); - command.addAll(Arrays.asList(run.getVm().split("\\s+"))); + command.addAll(Arrays.asList(scenario.getVariables().get(Scenario.VM_KEY).split("\\s+"))); command.add("-cp"); command.add(System.getProperty("java.class.path")); - command.add(Runner.class.getName()); + command.add(InProcessRunner.class.getName()); command.add("--warmupMillis"); - command.add(String.valueOf(warmupMillis)); + command.add(String.valueOf(arguments.getWarmupMillis())); command.add("--runMillis"); - command.add(String.valueOf(runMillis)); - command.add("--inProcess"); - for (Map.Entry<String, String> entry : run.getParameters().entrySet()) { + command.add(String.valueOf(arguments.getRunMillis())); + for (Entry<String, String> entry : scenario.getParameters().entrySet()) { command.add("-D" + entry.getKey() + "=" + entry.getValue()); } - command.add(suiteClassName); + command.add(arguments.getSuiteClassName()); BufferedReader reader = null; try { @@ -202,7 +135,7 @@ public final class Runner { Double nanosPerTrial = null; try { nanosPerTrial = Double.valueOf(firstLine); - } catch (NumberFormatException e) { + } catch (NumberFormatException ignore) { } String anotherLine = reader.readLine(); @@ -229,163 +162,63 @@ public final class Runner { } } - private Result runOutOfProcess() { - ImmutableMap.Builder<Run, Double> resultsBuilder = ImmutableMap.builder(); + // TODO: check if this is platform-independent + @SuppressWarnings("HardcodedLineSeparator") + private static final String RETURN = "\r"; + + private Run runOutOfProcess() { + String executedByUuid = getExecutedByUuid(); + Date executedDate = new Date(); + Builder<Scenario, Double> resultsBuilder = ImmutableMap.builder(); try { - List<Run> runs = createRuns(); + List<Scenario> scenarios = scenarioSelection.select(); int i = 0; - for (Run run : runs) { - beforeRun(i++, runs.size(), run); - double nanosPerTrial = executeForked(run); - afterRun(nanosPerTrial); - resultsBuilder.put(run, nanosPerTrial); + for (Scenario scenario : scenarios) { + beforeMeasurement(i++, scenarios.size(), scenario); + double nanosPerTrial = executeForked(scenario); + afterMeasurement(nanosPerTrial); + resultsBuilder.put(scenario, nanosPerTrial); } // blat out our progress bar - System.out.print("\r"); + System.out.print(RETURN); for (int j = 0; j < 80; j++) { System.out.print(" "); } - System.out.print("\r"); + System.out.print(RETURN); - return new Result(resultsBuilder.build()); + return new Run(resultsBuilder.build(), arguments.getSuiteClassName(), executedByUuid, executedDate); } catch (Exception e) { - throw new ExecutionException(e); + throw new ExceptionFromUserCodeException(e); } } - private void beforeRun(int index, int total, Run run) { + private void beforeMeasurement(int index, int total, Scenario scenario) { double percentDone = (double) index / total; int runStringLength = 63; // so the total line length is 80 - String runString = String.valueOf(run); + String runString = String.valueOf(scenario); if (runString.length() > runStringLength) { runString = runString.substring(0, runStringLength); } - System.out.printf("\r%2.0f%% %-" + runStringLength + "s", + System.out.printf(RETURN + "%2.0f%% %-" + runStringLength + "s", percentDone * 100, runString); } - private void afterRun(double nanosPerTrial) { + private void afterMeasurement(double nanosPerTrial) { System.out.printf(" %10.0fns", nanosPerTrial); } - private void runInProcess() { + public static void main(String... args) { try { - Caliper caliper = new Caliper(warmupMillis, runMillis); - - for (Run run : createRuns()) { - double result; - TimedRunnable timedRunnable = suite.createBenchmark(run.getParameters()); - double warmupNanosPerTrial = caliper.warmUp(timedRunnable); - result = caliper.run(timedRunnable, warmupNanosPerTrial); - double nanosPerTrial = result; - System.out.println(nanosPerTrial); - } - } catch (Exception e) { - throw new ExecutionException(e); - } - } - - private boolean parseArgs(String[] args) throws Exception { - for (int i = 0; i < args.length; i++) { - if ("--help".equals(args[i])) { - return false; - - } else if ("--inProcess".equals(args[i])) { - inProcess = true; - - } else if (args[i].startsWith("-D")) { - int equalsSign = args[i].indexOf('='); - if (equalsSign == -1) { - System.out.println("Malformed parameter " + args[i]); - return false; - } - String name = args[i].substring(2, equalsSign); - String value = args[i].substring(equalsSign + 1); - setParameter(name, value); - - } else if ("--warmupMillis".equals(args[i])) { - warmupMillis = Long.parseLong(args[++i]); - - } else if ("--runMillis".equals(args[i])) { - runMillis = Long.parseLong(args[++i]); - - } else if ("--vm".equals(args[i])) { - userVms.add(args[++i]); - - } else if (args[i].startsWith("-")) { - System.out.println("Unrecognized option: " + args[i]); - return false; - - } else { - if (suiteClassName != null) { - System.out.println("Too many benchmark classes!"); - return false; - } - suiteClassName = args[i]; - } - } - - if (inProcess && !userVms.isEmpty()) { - System.out.println("Cannot customize VM when running in process"); - return false; - } - - if (suiteClassName == null) { - System.out.println("No benchmark class provided."); - return false; - } - - return true; - } - - private void printUsage() { - System.out.println("Usage: Runner [OPTIONS...] <benchmark>"); - System.out.println(); - System.out.println(" <benchmark>: a benchmark class or suite"); - System.out.println(); - System.out.println("OPTIONS"); - System.out.println(); - System.out.println(" --D<param>=<value>: fix a benchmark parameter to a given value."); - System.out.println(" When multiple values for the same parameter are given (via"); - System.out.println(" multiple --Dx=y args), all supplied values are used."); - System.out.println(); - System.out.println(" --inProcess: run the benchmark in the same JVM rather than spawning"); - System.out.println(" another with the same classpath. By default each benchmark is"); - System.out.println(" run in a separate VM"); - System.out.println(); - System.out.println(" --warmupMillis <millis>: duration to warmup each benchmark"); - System.out.println(); - System.out.println(" --runMillis <millis>: duration to execute each benchmark"); - System.out.println(); - System.out.println(" --vm <vm>: executable to test benchmark on"); - - // adding new options? don't forget to update executeForked() - } - - public static void main(String... args) throws Exception { // TODO: cleaner error reporting - Runner runner = new Runner(); - if (!runner.parseArgs(args)) { - runner.printUsage(); - return; + new Runner().run(args); + } catch (UserException e) { + e.display(); + System.exit(1); } - - runner.prepareSuite(); - runner.prepareParameters(); - if (runner.inProcess) { - runner.runInProcess(); - return; - } - - Result result = runner.runOutOfProcess(); - new ConsoleReport(result).displayResults(); } - public static void main(Class<? extends Benchmark> suite, String... args) throws Exception { - String[] argsWithSuiteName = new String[args.length + 1]; - System.arraycopy(args, 0, argsWithSuiteName, 0, args.length); - argsWithSuiteName[args.length] = suite.getName(); - main(argsWithSuiteName); + public static void main(Class<? extends Benchmark> suite, String... args) { + main(ObjectArrays.concat(args, suite.getName())); } } diff --git a/src/com/google/caliper/Scenario.java b/src/com/google/caliper/Scenario.java new file mode 100644 index 0000000..3fd06e4 --- /dev/null +++ b/src/com/google/caliper/Scenario.java @@ -0,0 +1,81 @@ +/* + * 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 java.io.Serializable; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * A configured benchmark. + * + * <p>Gwt-safe. + */ +public final class Scenario + implements Serializable /* for GWT */ { + + static final String VM_KEY = "vm"; + + /** + * The subset of variable names that are managed by the system. It is an error + * to create a parameter with the same name as one of these variables. + */ + static final Set<String> SYSTEM_VARIABLES = new HashSet<String>(Arrays.asList(VM_KEY)); + + private /*final*/ Map<String, String> variables; + + public Scenario(Map<String, String> variables) { + this.variables = new LinkedHashMap<String, String>(variables); + } + + public Map<String, String> getVariables() { + return variables; + } + + /** + * Returns the user-specified parameters. This is the (possibly-empty) set of + * variables that may be varied from scenario to scenario in the same + * environment. + */ + public Map<String, String> getParameters() { + Map<String, String> result = new LinkedHashMap<String, String>(); + for (Map.Entry<String, String> entry : variables.entrySet()) { + if (!SYSTEM_VARIABLES.contains(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); + } + } + return result; + } + + @Override public boolean equals(Object o) { + return o instanceof Scenario + && ((Scenario) o).getVariables().equals(variables); + } + + @Override public int hashCode() { + return variables.hashCode(); + } + + @Override public String toString() { + return "Scenario" + variables; + } + + private Scenario() {} // for GWT +} diff --git a/src/com/google/caliper/ScenarioSelection.java b/src/com/google/caliper/ScenarioSelection.java new file mode 100644 index 0000000..7814818 --- /dev/null +++ b/src/com/google/caliper/ScenarioSelection.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2010 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.UserException.AbstractBenchmarkException; +import com.google.caliper.UserException.DoesntImplementBenchmarkException; +import com.google.caliper.UserException.ExceptionFromUserCodeException; +import com.google.caliper.UserException.NoParameterlessConstructorException; +import com.google.caliper.UserException.NoSuchClassException; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Multimap; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Figures out which scenarios to benchmark given a benchmark suite, set of user + * parameters, and set of user VMs. + */ +public final class ScenarioSelection { + + private final String suiteClassName; + private final Multimap<String, String> userParameters; + private final Set<String> userVms; + + private Benchmark suite; + + /** Effective parameters to run in the benchmark. */ + private final Multimap<String, String> parameters = LinkedHashMultimap.create(); + + public ScenarioSelection(Arguments arguments) { + this(arguments.getSuiteClassName(), arguments.getUserParameters(), arguments.getUserVms()); + } + + public ScenarioSelection(String suiteClassName, + Multimap<String, String> userParameters, Set<String> userVms) { + this.suiteClassName = suiteClassName; + this.userParameters = userParameters; + this.userVms = userVms; + } + + /** + * Returns the selected scenarios for this benchmark. + */ + public List<Scenario> select() { + prepareSuite(); + prepareParameters(); + return createScenarios(); + } + + public TimedRunnable createBenchmark(Scenario scenario) { + return suite.createBenchmark(scenario.getParameters()); + } + + private void prepareSuite() { + Class<?> benchmarkClass; + try { + benchmarkClass = getClassByName(suiteClassName); + } catch (ExceptionInInitializerError e) { + throw new ExceptionFromUserCodeException(e.getCause()); + } catch (ClassNotFoundException ignored) { + throw new NoSuchClassException(suiteClassName); + } + + Object s; + try { + Constructor<?> constructor = benchmarkClass.getDeclaredConstructor(); + constructor.setAccessible(true); + s = constructor.newInstance(); + } catch (InstantiationException ignore) { + throw new AbstractBenchmarkException(benchmarkClass); + } catch (NoSuchMethodException ignore) { + throw new NoParameterlessConstructorException(benchmarkClass); + } catch (IllegalAccessException impossible) { + throw new AssertionError(impossible); // shouldn't happen since we setAccessible(true) + } catch (InvocationTargetException e) { + throw new ExceptionFromUserCodeException(e.getCause()); + } + + if (s instanceof Benchmark) { + this.suite = (Benchmark) s; + } else { + throw new DoesntImplementBenchmarkException(benchmarkClass); + } + } + + private static Class<?> getClassByName(String className) throws ClassNotFoundException { + try { + return Class.forName(className); + } catch (ClassNotFoundException ignored) { + // try replacing the last dot with a $, in case that helps + // example: tutorial.Tutorial.Benchmark1 becomes tutorial.Tutorial$Benchmark1 + // amusingly, the $ character means three different things in this one line alone + String newName = className.replaceFirst("\\.([^.]+)$", "\\$$1"); + return Class.forName(newName); + } + } + + private void prepareParameters() { + for (String key : suite.parameterNames()) { + // first check if the user has specified values + Collection<String> userValues = userParameters.get(key); + if (!userValues.isEmpty()) { + parameters.putAll(key, userValues); + // TODO: type convert 'em to validate? + + } else { // otherwise use the default values from the suite + Set<String> values = suite.parameterValues(key); + if (values.isEmpty()) { + throw new ConfigurationException(key + " has no values"); + } + parameters.putAll(key, values); + } + } + } + + private ImmutableSet<String> defaultVms() { + return "Dalvik".equals(System.getProperty("java.vm.name")) + ? ImmutableSet.of("dalvikvm") + : ImmutableSet.of("java"); + } + + /** + * Returns a complete set of scenarios with every combination of values and + * benchmark classes. + */ + private List<Scenario> createScenarios() { + List<ScenarioBuilder> builders = new ArrayList<ScenarioBuilder>(); + + // create scenarios for each VM + Set<String> vms = userVms.isEmpty() + ? defaultVms() + : userVms; + for (String vm : vms) { + ScenarioBuilder scenarioBuilder = new ScenarioBuilder(); + scenarioBuilder.parameters.put(Scenario.VM_KEY, vm); + builders.add(scenarioBuilder); + } + + for (Entry<String, Collection<String>> parameter : parameters.asMap().entrySet()) { + Iterator<String> values = parameter.getValue().iterator(); + if (!values.hasNext()) { + throw new ConfigurationException("Not enough values for " + parameter); + } + + String key = parameter.getKey(); + + String firstValue = values.next(); + for (ScenarioBuilder builder : builders) { + builder.parameters.put(key, firstValue); + } + + // multiply the size of the specs by the number of alternate values + int size = builders.size(); + while (values.hasNext()) { + String alternate = values.next(); + for (int s = 0; s < size; s++) { + ScenarioBuilder copy = builders.get(s).copy(); + copy.parameters.put(key, alternate); + builders.add(copy); + } + } + } + + List<Scenario> result = new ArrayList<Scenario>(); + for (ScenarioBuilder builder : builders) { + result.add(builder.build()); + } + + return result; + } + + private static class ScenarioBuilder { + final Map<String, String> parameters = new LinkedHashMap<String, String>(); + + ScenarioBuilder copy() { + ScenarioBuilder result = new ScenarioBuilder(); + result.parameters.putAll(parameters); + return result; + } + + public Scenario build() { + return new Scenario(parameters); + } + } +} diff --git a/src/com/google/caliper/SimpleBenchmark.java b/src/com/google/caliper/SimpleBenchmark.java index 8d2d4b1..31ff6c9 100644 --- a/src/com/google/caliper/SimpleBenchmark.java +++ b/src/com/google/caliper/SimpleBenchmark.java @@ -16,12 +16,11 @@ package com.google.caliper; +import com.google.caliper.UserException.ExceptionFromUserCodeException; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; - import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -88,34 +87,31 @@ public abstract class SimpleBenchmark implements Benchmark { return methods.keySet(); } + Parameter<?> parameter = parameters.get(parameterName); + if (parameter == null) { + throw new IllegalArgumentException(); + } try { - TypeConverter typeConverter = new TypeConverter(); - Parameter<?> parameter = parameters.get(parameterName); - if (parameter == null) { - throw new IllegalArgumentException(); - } Collection<?> values = parameter.values(); - Type type = parameter.getType(); ImmutableSet.Builder<String> result = ImmutableSet.builder(); for (Object value : values) { - result.add(typeConverter.toString(value, type)); + result.add(String.valueOf(value)); } return result.build(); } catch (Exception e) { - throw new ExecutionException(e); + throw new ExceptionFromUserCodeException(e); } } public TimedRunnable createBenchmark(Map<String, String> parameterValues) { - TypeConverter typeConverter = new TypeConverter(); - if (!parameterNames().equals(parameterValues.keySet())) { throw new IllegalArgumentException("Invalid parameters specified. Expected " + parameterNames() + " but was " + parameterValues.keySet()); } try { + @SuppressWarnings({"ClassNewInstance"}) // can throw any Exception, so we catch all Exceptions final SimpleBenchmark copyOfSelf = getClass().newInstance(); final Method method = methods.get(parameterValues.get("benchmark")); @@ -125,8 +121,8 @@ public abstract class SimpleBenchmark implements Benchmark { continue; } - Parameter parameter = parameters.get(parameterName); - Object value = typeConverter.fromString(entry.getValue(), parameter.getType()); + Parameter<?> parameter = parameters.get(parameterName); + Object value = TypeConverter.fromString(entry.getValue(), parameter.getType()); parameter.set(copyOfSelf, value); } copyOfSelf.setUp(); @@ -138,7 +134,7 @@ public abstract class SimpleBenchmark implements Benchmark { }; } catch (Exception e) { - throw new ExecutionException(e); + throw new ExceptionFromUserCodeException(e); } } @@ -148,7 +144,7 @@ public abstract class SimpleBenchmark implements Benchmark { */ private Map<String, Method> createTimedMethods() { ImmutableMap.Builder<String, Method> result = ImmutableMap.builder(); - for (final Method method : getClass().getDeclaredMethods()) { + for (Method method : getClass().getDeclaredMethods()) { int modifiers = method.getModifiers(); if (!method.getName().startsWith("time")) { continue; diff --git a/src/com/google/caliper/TypeConverter.java b/src/com/google/caliper/TypeConverter.java index 73300ec..29d00ea 100644 --- a/src/com/google/caliper/TypeConverter.java +++ b/src/com/google/caliper/TypeConverter.java @@ -16,42 +16,44 @@ package com.google.caliper; +import com.google.common.collect.ImmutableMap; +import java.lang.reflect.Method; import java.lang.reflect.Type; +import java.util.Map; /** * Convert objects to and from Strings. */ -class TypeConverter { +final class TypeConverter { + private TypeConverter() {} - // the enum to strings conversion is manually checked - @SuppressWarnings("unchecked") - public Object fromString(String value, Type type) { - if (type instanceof Class) { - Class<?> c = (Class<?>) type; - if (c.isEnum()) { - return Enum.valueOf((Class) c, value); - } else if (type == Double.class || type == double.class) { - return Double.valueOf(value); - } else if (type == Integer.class || type == int.class) { - return Integer.valueOf(value); - } + public static Object fromString(String value, Type type) { + Class<?> c = wrap((Class<?>) type); + try { + Method m = c.getMethod("valueOf", String.class); + return m.invoke(null, value); + } catch (Exception e) { + throw new UnsupportedOperationException( + "Cannot convert " + value + " of type " + type, e); } - throw new UnsupportedOperationException( - "Cannot convert " + value + " of type " + type); } - public String toString(Object value, Type type) { - if (type instanceof Class) { - Class<?> c = (Class<?>) type; - if (c.isEnum()) { - return value.toString(); - } else if (type == Double.class || type == double.class) { - return value.toString(); - } else if (type == Integer.class || type == int.class) { - return value.toString(); - } - } - throw new UnsupportedOperationException( - "Cannot convert " + value + " of type " + type); + // safe because both Long.class and long.class are of type Class<Long> + @SuppressWarnings("unchecked") + private static <T> Class<T> wrap(Class<T> c) { + return c.isPrimitive() ? (Class<T>) PRIMITIVES_TO_WRAPPERS.get(c) : c; } + + private static final Map<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS + = new ImmutableMap.Builder<Class<?>, Class<?>>() + .put(boolean.class, Boolean.class) + .put(byte.class, Byte.class) + .put(char.class, Character.class) + .put(double.class, Double.class) + .put(float.class, Float.class) + .put(int.class, Integer.class) + .put(long.class, Long.class) + .put(short.class, Short.class) + .put(void.class, Void.class) + .build(); } diff --git a/src/com/google/caliper/UserException.java b/src/com/google/caliper/UserException.java new file mode 100644 index 0000000..66fb8e3 --- /dev/null +++ b/src/com/google/caliper/UserException.java @@ -0,0 +1,141 @@ +/* + * 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 java.util.Arrays; + +/** + * Signifies a problem that should be explained in user-friendly terms on the command line, without + * a confusing stack trace, and optionally followed by a usage summary. + */ +@SuppressWarnings("serial") // never going to serialize these... right? +public abstract class UserException extends RuntimeException { + protected final String error; + + protected UserException(String error) { + this.error = error; + } + + public abstract void display(); + + // - - - - + + public abstract static class ErrorInUsageException extends UserException { + protected ErrorInUsageException(String error) { + super(error); + } + + @Override public void display() { + if (error != null) { + System.err.println("Error: " + error); + } + Arguments.printUsage(); + } + } + + public abstract static class ErrorInUserCodeException extends UserException { + private final String remedy; + + protected ErrorInUserCodeException(String error, String remedy) { + super(error); + this.remedy = remedy; + } + + @Override public void display() { + System.err.println("Error: " + error); + System.err.println("Typical Remedy: " + remedy); + } + } + + // - - - - + + // Not technically an error, but works nicely this way anyway + public static class DisplayUsageException extends ErrorInUsageException { + public DisplayUsageException() { + super(null); + } + } + + public static class UnrecognizedOptionException extends ErrorInUsageException { + public UnrecognizedOptionException(String arg) { + super("Argument not recognized: " + arg); + } + } + + public static class NoBenchmarkClassException extends ErrorInUsageException { + public NoBenchmarkClassException() { + super("No benchmark class specified."); + } + } + + public static class MultipleBenchmarkClassesException extends ErrorInUsageException { + public MultipleBenchmarkClassesException(String a, String b) { + super("Multiple benchmark classes specified: " + Arrays.asList(a, b)); + } + } + + public static class MalformedParameterException extends ErrorInUsageException { + public MalformedParameterException(String arg) { + super("Malformed parameter: " + arg); + } + } + + public static class CantCustomizeInProcessVmException extends ErrorInUsageException { + public CantCustomizeInProcessVmException() { + super("Can't customize VM when running in process."); + } + } + + public static class NoSuchClassException extends ErrorInUsageException { + public NoSuchClassException(String name) { + super("No class named [" + name + "] was found (check CLASSPATH)."); + } + } + + + public static class AbstractBenchmarkException extends ErrorInUserCodeException { + public AbstractBenchmarkException(Class<?> specifiedClass) { + super("Class [" + specifiedClass.getName() + "] is abstract.", "Specify a concrete class."); + } + } + + public static class NoParameterlessConstructorException extends ErrorInUserCodeException { + public NoParameterlessConstructorException(Class<?> specifiedClass) { + super("Class [" + specifiedClass.getName() + "] has no parameterless constructor.", + "Remove all constructors or add a parameterless constructor."); + } + } + + public static class DoesntImplementBenchmarkException extends ErrorInUserCodeException { + public DoesntImplementBenchmarkException(Class<?> specifiedClass) { + super("Class [" + specifiedClass + "] does not implement the " + Benchmark.class.getName() + + " interface.", "Add 'extends " + SimpleBenchmark.class + "' to the class declaration."); + } + } + + // TODO: should remove the caliper stack frames.... + public static class ExceptionFromUserCodeException extends UserException { + public ExceptionFromUserCodeException(Throwable t) { + super("An exception was thrown from the benchmark code."); + initCause(t); + } + @Override public void display() { + System.err.println(error); + getCause().printStackTrace(System.err); + } + } +} diff --git a/src/com/google/caliper/Xml.java b/src/com/google/caliper/Xml.java new file mode 100644 index 0000000..f7cfafc --- /dev/null +++ b/src/com/google/caliper/Xml.java @@ -0,0 +1,108 @@ +/** + * 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.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +public final class Xml { + private static final String DATE_FORMAT_STRING = "yyyy-MM-dd'T'HH:mm:ssz"; + + /** + * Encodes this result as XML to the specified stream. This XML can be parsed + * with {@link #runFromXml(InputStream)}. Sample output: + * <pre>{@code + * <result benchmark="examples.FooBenchmark" + * executedBy="A0:1F:CAFE:BABE" + * executedTimestamp="2010-01-05T11:08:15PST"> + * <scenario bar="15" foo="A" vm="dalvikvm">1200.1</scenario> + * <scenario bar="15" foo="B" vm="dalvikvm">1100.2</scenario> + * </result> + * }</pre> + */ + public static void runToXml(Run run, OutputStream out) { + // BEGIN android-removed + // we don't have DOM level 3 on Android yet + // try { + // Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + // Element result = doc.createElement("result"); + // doc.appendChild(result); + // + // result.setAttribute("benchmark", run.getBenchmarkName()); + // result.setAttribute("executedBy", run.getExecutedByUuid()); + // String executedTimestampString = new SimpleDateFormat(DATE_FORMAT_STRING) + // .format(run.getExecutedTimestamp()); + // result.setAttribute("executedTimestamp", executedTimestampString); + // + // for (Map.Entry<Scenario, Double> entry : run.getMeasurements().entrySet()) { + // Element runElement = doc.createElement("scenario"); + // result.appendChild(runElement); + // + // Scenario scenario = entry.getKey(); + // for (Map.Entry<String, String> parameter : scenario.getVariables().entrySet()) { + // runElement.setAttribute(parameter.getKey(), parameter.getValue()); + // } + // runElement.setTextContent(String.valueOf(entry.getValue())); + // } + // + // TransformerFactory.newInstance().newTransformer() + // .transform(new DOMSource(doc), new StreamResult(out)); + // } catch (Exception e) { + // throw new IllegalStateException("Malformed XML document", e); + // } + // END android-removed + } + + /** + * Creates a result by decoding XML from the specified stream. The XML should + * be consistent with the format emitted by {@link #runToXml(Run, OutputStream)}. + */ + public static Run runFromXml(InputStream in) { + // BEGIN android-removed + // we don't have DOM level 3 on Android yet + throw new UnsupportedOperationException(); + // try { + // Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); + // Element result = document.getDocumentElement(); + // + // String benchmarkName = result.getAttribute("benchmark"); + // String executedByUuid = result.getAttribute("executedBy"); + // String executedDateString = result.getAttribute("executedTimestamp"); + // Date executedDate = new SimpleDateFormat(DATE_FORMAT_STRING).parse(executedDateString); + // + // ImmutableMap.Builder<Scenario, Double> measurementsBuilder = ImmutableMap.builder(); + // for (Node node : childrenOf(result)) { + // Element scenarioElement = (Element) node; + // Scenario scenario = new Scenario(attributesOf(scenarioElement)); + // double measurement = Double.parseDouble(scenarioElement.getTextContent()); + // measurementsBuilder.put(scenario, measurement); + // } + // + // return new Run(measurementsBuilder.build(), benchmarkName, executedByUuid, executedDate); + // } catch (Exception e) { + // throw new IllegalStateException("Malformed XML document", e); + // } + // END android-removed + } + + private Xml() {} +} |