/*
* Copyright (C) 2012 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.config;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.caliper.api.ResultProcessor;
import com.google.caliper.config.VmConfig.Builder;
import com.google.caliper.util.Util;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.collect.BiMap;
import com.google.common.collect.Collections2;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/**
* Represents caliper configuration. By default, {@code ~/.caliper/config.properties} and
* {@code global-config.properties}.
*
* @author gak@google.com (Gregory Kick)
*/
public final class CaliperConfig {
@VisibleForTesting final ImmutableMap<String, String> properties;
private final ImmutableMap<Class<? extends ResultProcessor>, ResultProcessorConfig>
resultProcessorConfigs;
@VisibleForTesting
public CaliperConfig(ImmutableMap<String, String> properties)
throws InvalidConfigurationException {
this.properties = checkNotNull(properties);
this.resultProcessorConfigs = findResultProcessorConfigs(subgroupMap(properties, "results"));
}
private static final Pattern CLASS_PROPERTY_PATTERN = Pattern.compile("(\\w+)\\.class");
private static <T> ImmutableBiMap<String, Class<? extends T>> mapGroupNamesToClasses(
ImmutableMap<String, String> groupProperties, Class<T> type)
throws InvalidConfigurationException {
BiMap<String, Class<? extends T>> namesToClasses = HashBiMap.create();
for (Entry<String, String> entry : groupProperties.entrySet()) {
Matcher matcher = CLASS_PROPERTY_PATTERN.matcher(entry.getKey());
if (matcher.matches() && !entry.getValue().isEmpty()) {
try {
Class<?> someClass = Class.forName(entry.getValue());
checkState(type.isAssignableFrom(someClass));
@SuppressWarnings("unchecked")
Class<? extends T> verifiedClass = (Class<? extends T>) someClass;
namesToClasses.put(matcher.group(1), verifiedClass);
} catch (ClassNotFoundException e) {
throw new InvalidConfigurationException("Cannot find result processor class: "
+ entry.getValue());
}
}
}
return ImmutableBiMap.copyOf(namesToClasses);
}
private static ImmutableMap<Class<? extends ResultProcessor>, ResultProcessorConfig>
findResultProcessorConfigs(ImmutableMap<String, String> resultsProperties)
throws InvalidConfigurationException {
ImmutableBiMap<String, Class<? extends ResultProcessor>> processorToClass =
mapGroupNamesToClasses(resultsProperties, ResultProcessor.class);
ImmutableMap.Builder<Class<? extends ResultProcessor>, ResultProcessorConfig> builder =
ImmutableMap.builder();
for (Entry<String, Class<? extends ResultProcessor>> entry : processorToClass.entrySet()) {
builder.put(entry.getValue(), getResultProcessorConfig(resultsProperties, entry.getKey()));
}
return builder.build();
}
public ImmutableMap<String, String> properties() {
return properties;
}
/**
* Returns the configuration of the current host JVM (including the flags used to create it). Any
* args specified using {@code vm.args} will also be applied
*/
public VmConfig getDefaultVmConfig() {
return new Builder(new File(System.getProperty("java.home")))
.addAllOptions(Collections2.filter(ManagementFactory.getRuntimeMXBean().getInputArguments(),
new Predicate<String>() {
@Override public boolean apply(@Nullable String input) {
// Exclude the -agentlib:jdwp param which configures the socket debugging protocol.
// If this is set in the parent VM we do not want it to be inherited by the child
// VM. If it is, the child will die immediately on startup because it will fail to
// bind to the debug port (because the parent VM is already bound to it).
return !input.startsWith("-agentlib:jdwp");
}
}))
// still incorporate vm.args
.addAllOptions(getArgs(subgroupMap(properties, "vm")))
.build();
}
public VmConfig getVmConfig(String name) throws InvalidConfigurationException {
checkNotNull(name);
ImmutableMap<String, String> vmGroupMap = subgroupMap(properties, "vm");
ImmutableMap<String, String> vmMap = subgroupMap(vmGroupMap, name);
File homeDir = getJdkHomeDir(vmGroupMap.get("baseDirectory"), vmMap.get("home"), name);
return new VmConfig.Builder(homeDir)
.addAllOptions(getArgs(vmGroupMap))
.addAllOptions(getArgs(vmMap))
.build();
}
private static final Pattern INSTRUMENT_CLASS_PATTERN = Pattern.compile("([^\\.]+)\\.class");
public ImmutableSet<String> getConfiguredInstruments() {
ImmutableSet.Builder<String> resultBuilder = ImmutableSet.builder();
for (String key : subgroupMap(properties, "instrument").keySet()) {
Matcher matcher = INSTRUMENT_CLASS_PATTERN.matcher(key);
if (matcher.matches()) {
resultBuilder.add(matcher.group(1));
}
}
return resultBuilder.build();
}
public InstrumentConfig getInstrumentConfig(String name) {
checkNotNull(name);
ImmutableMap<String, String> instrumentGroupMap = subgroupMap(properties, "instrument");
ImmutableMap<String, String> insrumentMap = subgroupMap(instrumentGroupMap, name);
@Nullable String className = insrumentMap.get("class");
checkArgument(className != null, "no instrument configured named %s", name);
return new InstrumentConfig.Builder()
.className(className)
.addAllOptions(subgroupMap(insrumentMap, "options"))
.build();
}
public ImmutableSet<Class<? extends ResultProcessor>> getConfiguredResultProcessors() {
return resultProcessorConfigs.keySet();
}
public ResultProcessorConfig getResultProcessorConfig(
Class<? extends ResultProcessor> resultProcessorClass) {
return resultProcessorConfigs.get(resultProcessorClass);
}
private static ResultProcessorConfig getResultProcessorConfig(
ImmutableMap<String, String> resultsProperties, String name) {
ImmutableMap<String, String> resultsMap = subgroupMap(resultsProperties, name);
return new ResultProcessorConfig.Builder()
.className(resultsMap.get("class"))
.addAllOptions(subgroupMap(resultsMap, "options"))
.build();
}
@Override public String toString() {
return Objects.toStringHelper(this)
.add("properties", properties)
.toString();
}
private static final ImmutableMap<String, String> subgroupMap(ImmutableMap<String, String> map,
String groupName) {
return Util.prefixedSubmap(map, groupName + ".");
}
private static List<String> getArgs(Map<String, String> properties) {
String argsString = Strings.nullToEmpty(properties.get("args"));
ImmutableList.Builder<String> args = ImmutableList.builder();
StringBuilder arg = new StringBuilder();
for (int i = 0; i < argsString.length(); i++) {
char c = argsString.charAt(i);
switch (c) {
case '\\':
arg.append(argsString.charAt(++i));
break;
case ' ':
if (arg.length() > 0) {
args.add(arg.toString());
}
arg = new StringBuilder();
break;
default:
arg.append(c);
break;
}
}
if (arg.length() > 0) {
args.add(arg.toString());
}
return args.build();
}
// TODO(gak): check that the directory seems to be a jdk home (with a java binary and all of that)
// TODO(gak): make this work with different directory layouts. I'm looking at you OS X...
private static File getJdkHomeDir(@Nullable String baseDirectoryPath,
@Nullable String homeDirPath, String vmConfigName)
throws InvalidConfigurationException {
if (homeDirPath == null) {
File baseDirectory = getBaseDirectory(baseDirectoryPath);
File homeDir = new File(baseDirectory, vmConfigName);
checkConfiguration(homeDir.isDirectory(), "%s is not a directory", homeDir);
return homeDir;
} else {
File potentialHomeDir = new File(homeDirPath);
if (potentialHomeDir.isAbsolute()) {
checkConfiguration(potentialHomeDir.isDirectory(), "%s is not a directory",
potentialHomeDir);
return potentialHomeDir;
} else {
File baseDirectory = getBaseDirectory(baseDirectoryPath);
File homeDir = new File(baseDirectory, homeDirPath);
checkConfiguration(homeDir.isDirectory(), "%s is not a directory", potentialHomeDir);
return homeDir;
}
}
}
private static File getBaseDirectory(@Nullable String baseDirectoryPath)
throws InvalidConfigurationException {
if (baseDirectoryPath == null) {
throw new InvalidConfigurationException(
"must set either a home directory or a base directory");
} else {
File baseDirectory = new File(baseDirectoryPath);
checkConfiguration(baseDirectory.isAbsolute(), "base directory cannot be a relative path");
checkConfiguration(baseDirectory.isDirectory(), "base directory must be a directory");
return baseDirectory;
}
}
private static void checkConfiguration(boolean check, String message)
throws InvalidConfigurationException {
if (!check) {
throw new InvalidConfigurationException(message);
}
}
private static void checkConfiguration(boolean check, String messageFormat, Object... args)
throws InvalidConfigurationException {
if (!check) {
throw new InvalidConfigurationException(String.format(messageFormat, args));
}
}
}