1 /* 2 * Copyright (C) 2011 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.google.caliper.options; 16 17 import static com.google.common.base.Preconditions.checkNotNull; 18 import static java.util.concurrent.TimeUnit.MINUTES; 19 20 import com.google.caliper.options.CommandLineParser.Leftovers; 21 import com.google.caliper.options.CommandLineParser.Option; 22 import com.google.caliper.util.InvalidCommandException; 23 import com.google.caliper.util.ShortDuration; 24 import com.google.common.base.Joiner; 25 import com.google.common.base.MoreObjects; 26 import com.google.common.base.Optional; 27 import com.google.common.base.Splitter; 28 import com.google.common.collect.ArrayListMultimap; 29 import com.google.common.collect.ImmutableList; 30 import com.google.common.collect.ImmutableMap; 31 import com.google.common.collect.ImmutableSet; 32 import com.google.common.collect.ImmutableSetMultimap; 33 import com.google.common.collect.Maps; 34 import com.google.common.collect.Multimap; 35 import com.google.common.collect.Ordering; 36 37 import java.io.File; 38 import java.util.List; 39 import java.util.Map; 40 41 final class ParsedOptions implements CaliperOptions { 42 43 public static ParsedOptions from(String[] args, boolean requireBenchmarkClassName) 44 throws InvalidCommandException { 45 ParsedOptions options = new ParsedOptions(requireBenchmarkClassName); 46 47 CommandLineParser<ParsedOptions> parser = CommandLineParser.forClass(ParsedOptions.class); 48 try { 49 parser.parseAndInject(args, options); 50 } catch (InvalidCommandException e) { 51 e.setUsage(USAGE); 52 throw e; 53 } 54 return options; 55 } 56 57 /** 58 * True if the benchmark class name is expected as the last argument, false if it is not allowed. 59 */ 60 private final boolean requireBenchmarkClassName; 61 62 private ParsedOptions(boolean requireBenchmarkClassName) { 63 this.requireBenchmarkClassName = requireBenchmarkClassName; 64 } 65 66 // -------------------------------------------------------------------------- 67 // Dry run -- simple boolean, needs to be checked in some methods 68 // -------------------------------------------------------------------------- 69 70 @Option({"-n", "--dry-run"}) 71 private boolean dryRun; 72 73 @Override public boolean dryRun() { 74 return dryRun; 75 } 76 77 private void dryRunIncompatible(String optionName) throws InvalidCommandException { 78 // This only works because CLP does field injection before method injection 79 if (dryRun) { 80 throw new InvalidCommandException("Option not available in dry-run mode: " + optionName); 81 } 82 } 83 84 // -------------------------------------------------------------------------- 85 // Delimiter -- injected early so methods can use it 86 // -------------------------------------------------------------------------- 87 88 @Option({"-d", "--delimiter"}) 89 private String delimiter = ","; 90 91 private ImmutableSet<String> split(String string) { 92 return ImmutableSet.copyOf(Splitter.on(delimiter).split(string)); 93 } 94 95 // -------------------------------------------------------------------------- 96 // Benchmark method names to run 97 // -------------------------------------------------------------------------- 98 99 private ImmutableSet<String> benchmarkNames = ImmutableSet.of(); 100 101 @Option({"-b", "--benchmark"}) 102 private void setBenchmarkNames(String benchmarksString) { 103 benchmarkNames = split(benchmarksString); 104 } 105 106 @Override public ImmutableSet<String> benchmarkMethodNames() { 107 return benchmarkNames; 108 } 109 110 // -------------------------------------------------------------------------- 111 // Print configuration? 112 // -------------------------------------------------------------------------- 113 114 @Option({"-p", "--print-config"}) 115 private boolean printConfiguration = false; 116 117 @Override public boolean printConfiguration() { 118 return printConfiguration; 119 } 120 121 // -------------------------------------------------------------------------- 122 // Trials 123 // -------------------------------------------------------------------------- 124 125 private int trials = 1; 126 127 @Option({"-t", "--trials"}) 128 private void setTrials(int trials) throws InvalidCommandException { 129 dryRunIncompatible("trials"); 130 if (trials < 1) { 131 throw new InvalidCommandException("trials must be at least 1: " + trials); 132 } 133 this.trials = trials; 134 } 135 136 @Override public int trialsPerScenario() { 137 return trials; 138 } 139 140 // -------------------------------------------------------------------------- 141 // Time limit 142 // -------------------------------------------------------------------------- 143 144 private ShortDuration runTime = ShortDuration.of(5, MINUTES); 145 146 @Option({"-l", "--time-limit"}) 147 private void setTimeLimit(String timeLimitString) throws InvalidCommandException { 148 try { 149 this.runTime = ShortDuration.valueOf(timeLimitString); 150 } catch (IllegalArgumentException e) { 151 throw new InvalidCommandException("Invalid time limit: " + timeLimitString); 152 } 153 } 154 155 @Override public ShortDuration timeLimit() { 156 return runTime; 157 } 158 159 // -------------------------------------------------------------------------- 160 // Run name 161 // -------------------------------------------------------------------------- 162 163 private String runName = ""; 164 165 @Option({"-r", "--run-name"}) 166 private void setRunName(String runName) { 167 this.runName = checkNotNull(runName); 168 } 169 170 @Override public String runName() { 171 return runName; 172 } 173 174 // -------------------------------------------------------------------------- 175 // VM specifications 176 // -------------------------------------------------------------------------- 177 178 private ImmutableSet<String> vmNames = ImmutableSet.of(); 179 180 @Option({"-m", "--vm"}) 181 private void setVms(String vmsString) throws InvalidCommandException { 182 dryRunIncompatible("vm"); 183 vmNames = split(vmsString); 184 } 185 186 @Override public ImmutableSet<String> vmNames() { 187 return vmNames; 188 } 189 190 // -------------------------------------------------------------------------- 191 // Measuring instruments to use 192 // -------------------------------------------------------------------------- 193 194 private static final ImmutableSet<String> DEFAULT_INSTRUMENT_NAMES = 195 new ImmutableSet.Builder<String>() 196 .add("allocation") 197 .add("runtime") 198 .build(); 199 200 private ImmutableSet<String> instrumentNames = DEFAULT_INSTRUMENT_NAMES; 201 202 @Option({"-i", "--instrument"}) 203 private void setInstruments(String instrumentsString) { 204 instrumentNames = split(instrumentsString); 205 } 206 207 @Override public ImmutableSet<String> instrumentNames() { 208 return instrumentNames; 209 } 210 211 // -------------------------------------------------------------------------- 212 // Benchmark parameters 213 // -------------------------------------------------------------------------- 214 215 private Multimap<String, String> mutableUserParameters = ArrayListMultimap.create(); 216 217 @Option("-D") 218 private void addParameterSpec(String nameAndValues) throws InvalidCommandException { 219 addToMultimap(nameAndValues, mutableUserParameters); 220 } 221 222 @Override public ImmutableSetMultimap<String, String> userParameters() { 223 // de-dup values, but keep in order 224 return new ImmutableSetMultimap.Builder<String, String>() 225 .orderKeysBy(Ordering.natural()) 226 .putAll(mutableUserParameters) 227 .build(); 228 } 229 230 // -------------------------------------------------------------------------- 231 // VM arguments 232 // -------------------------------------------------------------------------- 233 234 private Multimap<String, String> mutableVmArguments = ArrayListMultimap.create(); 235 236 @Option("-J") 237 private void addVmArgumentsSpec(String nameAndValues) throws InvalidCommandException { 238 dryRunIncompatible("-J"); 239 addToMultimap(nameAndValues, mutableVmArguments); 240 } 241 242 @Override public ImmutableSetMultimap<String, String> vmArguments() { 243 // de-dup values, but keep in order 244 return new ImmutableSetMultimap.Builder<String, String>() 245 .orderKeysBy(Ordering.natural()) 246 .putAll(mutableVmArguments) 247 .build(); 248 } 249 250 // -------------------------------------------------------------------------- 251 // VM arguments 252 // -------------------------------------------------------------------------- 253 254 private final Map<String, String> mutableConfigPropertes = Maps.newHashMap(); 255 256 @Option("-C") 257 private void addConfigProperty(String nameAndValue) throws InvalidCommandException { 258 List<String> tokens = splitProperty(nameAndValue); 259 mutableConfigPropertes.put(tokens.get(0), tokens.get(1)); 260 } 261 262 @Override public ImmutableMap<String, String> configProperties() { 263 return ImmutableMap.copyOf(mutableConfigPropertes); 264 } 265 266 // -------------------------------------------------------------------------- 267 // Location of .caliper 268 // -------------------------------------------------------------------------- 269 270 private File caliperDirectory = new File(System.getProperty("user.home"), ".caliper"); 271 272 @Option({"--directory"}) 273 private void setCaliperDirectory(String path) { 274 caliperDirectory = new File(path); 275 } 276 277 @Override public File caliperDirectory() { 278 return caliperDirectory; 279 } 280 281 // -------------------------------------------------------------------------- 282 // Location of config.properties 283 // -------------------------------------------------------------------------- 284 285 private Optional<File> caliperConfigFile = Optional.absent(); 286 287 @Option({"-c", "--config"}) 288 private void setCaliperConfigFile(String filename) { 289 caliperConfigFile = Optional.of(new File(filename)); 290 } 291 292 @Override public File caliperConfigFile() { 293 return caliperConfigFile.or(new File(caliperDirectory, "config.properties")); 294 } 295 296 297 // -------------------------------------------------------------------------- 298 // Leftover - benchmark class name 299 // -------------------------------------------------------------------------- 300 301 private String benchmarkClassName; 302 303 @Leftovers 304 private void setLeftovers(ImmutableList<String> leftovers) throws InvalidCommandException { 305 if (requireBenchmarkClassName) { 306 if (leftovers.isEmpty()) { 307 throw new InvalidCommandException("No benchmark class specified"); 308 } 309 if (leftovers.size() > 1) { 310 throw new InvalidCommandException("Extra stuff, expected only class name: " + leftovers); 311 } 312 this.benchmarkClassName = leftovers.get(0); 313 } else { 314 if (!leftovers.isEmpty()) { 315 throw new InvalidCommandException( 316 "Extra stuff, did not expect non-option arguments: " + leftovers); 317 } 318 } 319 } 320 321 @Override public String benchmarkClassName() { 322 return benchmarkClassName; 323 } 324 325 // -------------------------------------------------------------------------- 326 // Helper methods 327 // -------------------------------------------------------------------------- 328 329 private static List<String> splitProperty(String propertyString) throws InvalidCommandException { 330 List<String> tokens = ImmutableList.copyOf(Splitter.on('=').limit(2).split(propertyString)); 331 if (tokens.size() != 2) { 332 throw new InvalidCommandException("no '=' found in: " + propertyString); 333 } 334 return tokens; 335 } 336 337 private void addToMultimap(String nameAndValues, Multimap<String, String> multimap) 338 throws InvalidCommandException { 339 List<String> tokens = splitProperty(nameAndValues); 340 String name = tokens.get(0); 341 String values = tokens.get(1); 342 343 if (multimap.containsKey(name)) { 344 throw new InvalidCommandException("multiple parameter sets for: " + name); 345 } 346 multimap.putAll(name, split(values)); 347 } 348 349 @Override public String toString() { 350 return MoreObjects.toStringHelper(this) 351 .add("benchmarkClassName", this.benchmarkClassName()) 352 .add("benchmarkMethodNames", this.benchmarkMethodNames()) 353 .add("benchmarkParameters", this.userParameters()) 354 .add("dryRun", this.dryRun()) 355 .add("instrumentNames", this.instrumentNames()) 356 .add("vms", this.vmNames()) 357 .add("vmArguments", this.vmArguments()) 358 .add("trials", this.trialsPerScenario()) 359 .add("printConfig", this.printConfiguration()) 360 .add("delimiter", this.delimiter) 361 .add("caliperConfigFile", this.caliperConfigFile) 362 .toString(); 363 } 364 365 // -------------------------------------------------------------------------- 366 // Usage 367 // -------------------------------------------------------------------------- 368 369 // TODO(kevinb): kinda nice if CommandLineParser could autogenerate most of this... 370 // TODO(kevinb): a test could actually check that we don't exceed 79 columns. 371 private static final ImmutableList<String> USAGE = ImmutableList.of( 372 "Usage:", 373 " java com.google.caliper.runner.CaliperMain <benchmark_class_name> [options...]", 374 "", 375 "Options:", 376 " -h, --help print this message", 377 " -n, --dry-run instead of measuring, execute a single rep for each scenario", 378 " in-process", 379 " -b, --benchmark comma-separated list of benchmark methods to run; 'foo' is", 380 " an alias for 'timeFoo' (default: all found in class)", 381 " -m, --vm comma-separated list of VMs to test on; possible values are", 382 " configured in Caliper's configuration file (default:", 383 " whichever VM caliper itself is running in, only)", 384 " -i, --instrument comma-separated list of measuring instruments to use; possible ", 385 " values are configured in Caliper's configuration file ", 386 " (default: \"" + Joiner.on(",").join(DEFAULT_INSTRUMENT_NAMES) + "\")", 387 " -t, --trials number of independent trials to peform per benchmark scenario; ", 388 " a positive integer (default: 1)", 389 " -l, --time-limit maximum length of time allowed for a single trial; use 0 to allow ", 390 " trials to run indefinitely. (default: 30s) ", 391 " -r, --run-name a user-friendly string used to identify the run", 392 " -p, --print-config print the effective configuration that will be used by Caliper", 393 " -d, --delimiter separator used in options that take multiple values (default: ',')", 394 " -c, --config location of Caliper's configuration file (default:", 395 " $HOME/.caliper/config.properties)", 396 " --directory location of Caliper's configuration and data directory ", 397 " (default: $HOME/.caliper)", 398 "", 399 " -Dparam=val1,val2,...", 400 " Specifies the values to inject into the 'param' field of the benchmark", 401 " class; if multiple values or parameters are specified in this way, caliper", 402 " will try all possible combinations.", 403 "", 404 // commented out until this flag is fixed 405 // " -JdisplayName='vm arg list choice 1,vm arg list choice 2,...'", 406 // " Specifies alternate sets of VM arguments to pass. As with any variable,", 407 // " caliper will test all possible combinations. Example:", 408 // " -Jmemory='-Xms32m -Xmx32m,-Xms512m -Xmx512m'", 409 // "", 410 " -CconfigProperty=value", 411 " Specifies a value for any property that could otherwise be specified in ", 412 " $HOME/.caliper/config.properties. Properties specified on the command line", 413 " will override those specified in the file.", 414 "", 415 "See http://code.google.com/p/caliper/wiki/CommandLineOptions for more details.", 416 ""); 417 } 418