blob: fcfead06a344cc5441e76d8b259ed5638f33990a [file] [log] [blame]
/*
* 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.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 Set<String> userVms;
private final Multimap<String, String> vmParameters;
private final String suiteClassName;
/**
* The user parameters specified on the command line. This may be a subset of
* the effective user parameters because parameters not specified here may get
* default values from the benchmark class.
*/
private final Multimap<String, String> userParameterArguments;
/**
* The actual user parameters we'll use to run in the benchmark. This contains
* the userParameterArguments plus the default user parameters.
*/
private Multimap<String, String> userParameters;
private final int trials;
private Benchmark suite;
public ScenarioSelection(Arguments arguments) {
this(arguments.getUserVms(), arguments.getVmParameters(), arguments.getSuiteClassName(),
arguments.getUserParameters(), arguments.getTrials());
}
public ScenarioSelection(Set<String> userVms, Multimap<String, String> vmParameters,
String suiteClassName, Multimap<String, String> userParameterArguments, int trials) {
this.userVms = userVms;
this.vmParameters = vmParameters;
this.suiteClassName = suiteClassName;
this.userParameterArguments = userParameterArguments;
this.trials = trials;
}
/**
* Returns the selected scenarios for this benchmark.
*/
public List<Scenario> select() {
prepareSuite();
userParameters = computeUserParameters();
return createScenarios();
}
/**
* Returns a normalized version of {@code scenario}, with information from {@code suite}
* assisting in correcting problems.
*/
public Scenario normalizeScenario(Scenario scenario) {
// This only applies to SimpleBenchmarks since they accept the special "benchmark"
// parameter. This is a special case because SimpleBenchmark is the most commonly
// used benchmark class. Have to do this horrible stuff since Benchmark API
// doesn't provide scenario-normalization (and it shouldn't), which SimpleBenchmark
// requires.
if (suite instanceof SimpleBenchmark) {
return ((SimpleBenchmark) suite).normalizeScenario(scenario);
}
return scenario;
}
public Set<String> getUserParameterNames() {
if (userParameters == null) {
throw new IllegalStateException();
}
return userParameters.keySet();
}
public Set<String> getVmParameterNames() {
return vmParameters.keySet();
}
public ConfiguredBenchmark createBenchmark(Scenario scenario) {
return suite.createBenchmark(scenario.getVariables(getUserParameterNames()));
}
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 Multimap<String, String> computeUserParameters() {
Multimap<String, String> result = LinkedHashMultimap.create();
for (String key : suite.parameterNames()) {
// first check if the user has specified values
Collection<String> userValues = userParameterArguments.get(key);
if (!userValues.isEmpty()) {
result.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. "
+ "Did you forget a -D" + key + "=<value> command line argument?");
}
result.putAll(key, values);
}
}
return result;
}
/**
* Returns a complete set of scenarios with every combination of variables.
*/
private List<Scenario> createScenarios() {
List<ScenarioBuilder> builders = new ArrayList<ScenarioBuilder>();
builders.add(new ScenarioBuilder());
Map<String, Collection<String>> variables = new LinkedHashMap<String, Collection<String>>();
variables.put(Scenario.VM_KEY, userVms.isEmpty() ? VmFactory.defaultVms() : userVms);
variables.put(Scenario.TRIAL_KEY, newListOfSize(trials));
variables.putAll(userParameters.asMap());
variables.putAll(vmParameters.asMap());
for (Entry<String, Collection<String>> entry : variables.entrySet()) {
Iterator<String> values = entry.getValue().iterator();
if (!values.hasNext()) {
throw new ConfigurationException("Not enough values for " + entry);
}
String firstValue = values.next();
for (ScenarioBuilder builder : builders) {
builder.variables.put(entry.getKey(), 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.variables.put(entry.getKey(), alternate);
builders.add(copy);
}
}
}
List<Scenario> result = new ArrayList<Scenario>();
for (ScenarioBuilder builder : builders) {
result.add(normalizeScenario(builder.build()));
}
return result;
}
/**
* Returns a list containing {@code count} distinct elements.
*/
private Collection<String> newListOfSize(int count) {
List<String> result = new ArrayList<String>();
for (int i = 0; i < count; i++) {
result.add(Integer.toString(i));
}
return result;
}
private static class ScenarioBuilder {
final Map<String, String> variables = new LinkedHashMap<String, String>();
ScenarioBuilder copy() {
ScenarioBuilder result = new ScenarioBuilder();
result.variables.putAll(variables);
return result;
}
public Scenario build() {
return new Scenario(variables);
}
}
}