blob: 72442db77c5b98f314314d063d5b0398e8db2ba6 [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.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
/**
* 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;
/**
* 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.
*/
void setParameter(String name, String value) {
userParameters.put(name, value);
}
private void prepareSuite() {
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.");
}
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);
}
}
}
private ImmutableSet<String> defaultVms() {
return "Dalvik".equals(System.getProperty("java.vm.name"))
? ImmutableSet.of("dalvikvm")
: ImmutableSet.of("java");
}
/**
* 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);
}
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);
}
// 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);
}
}
}
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);
}
}
private double executeForked(Run run) {
ProcessBuilder builder = new ProcessBuilder();
List<String> command = builder.command();
command.addAll(Arrays.asList(run.getVm().split("\\s+")));
command.add("-cp");
command.add(System.getProperty("java.class.path"));
command.add(Runner.class.getName());
command.add("--warmupMillis");
command.add(String.valueOf(warmupMillis));
command.add("--runMillis");
command.add(String.valueOf(runMillis));
command.add("--inProcess");
for (Map.Entry<String, String> entry : run.getParameters().entrySet()) {
command.add("-D" + entry.getKey() + "=" + entry.getValue());
}
command.add(suiteClassName);
BufferedReader reader = null;
try {
builder.redirectErrorStream(true);
builder.directory(new File(System.getProperty("user.dir")));
Process process = builder.start();
reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String firstLine = reader.readLine();
Double nanosPerTrial = null;
try {
nanosPerTrial = Double.valueOf(firstLine);
} catch (NumberFormatException e) {
}
String anotherLine = reader.readLine();
if (nanosPerTrial != null && anotherLine == null) {
return nanosPerTrial;
}
String message = "Failed to execute " + command;
System.err.println(message);
System.err.println(" " + firstLine);
do {
System.err.println(" " + anotherLine);
} while ((anotherLine = reader.readLine()) != null);
throw new ConfigurationException(message);
} catch (IOException e) {
throw new ConfigurationException(e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {
}
}
}
}
private Result runOutOfProcess() {
ImmutableMap.Builder<Run, Double> resultsBuilder = ImmutableMap.builder();
try {
List<Run> runs = createRuns();
int i = 0;
for (Run run : runs) {
beforeRun(i++, runs.size(), run);
double nanosPerTrial = executeForked(run);
afterRun(nanosPerTrial);
resultsBuilder.put(run, nanosPerTrial);
}
// blat out our progress bar
System.out.print("\r");
for (int j = 0; j < 80; j++) {
System.out.print(" ");
}
System.out.print("\r");
return new Result(resultsBuilder.build());
} catch (Exception e) {
throw new ExecutionException(e);
}
}
private void beforeRun(int index, int total, Run run) {
double percentDone = (double) index / total;
int runStringLength = 63; // so the total line length is 80
String runString = String.valueOf(run);
if (runString.length() > runStringLength) {
runString = runString.substring(0, runStringLength);
}
System.out.printf("\r%2.0f%% %-" + runStringLength + "s",
percentDone * 100, runString);
}
private void afterRun(double nanosPerTrial) {
System.out.printf(" %10.0fns", nanosPerTrial);
}
private void runInProcess() {
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;
}
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);
}
}