blob: 68f26f133d988b6d6a85b309023a6bf716889cbc [file] [log] [blame]
Jesse Wilson109c1282009-12-08 13:45:25 -08001/*
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.caliper;
18
19import com.google.common.collect.LinkedHashMultimap;
20import com.google.common.collect.Multimap;
21import com.google.common.collect.ImmutableMap;
22import com.google.common.collect.ImmutableSet;
23
24import java.io.BufferedReader;
25import java.io.File;
26import java.io.IOException;
27import java.io.InputStreamReader;
28import java.lang.reflect.Constructor;
29import java.lang.reflect.InvocationTargetException;
30import java.util.*;
31
32/**
33 * Creates, executes and reports benchmark runs.
34 */
35public final class Runner {
36
37 private String suiteClassName;
38 private BenchmarkSuite suite;
39
40 /** Effective parameters to run in the benchmark. */
41 private Multimap<String, String> parameters = LinkedHashMultimap.create();
42
43 /** JVMs to run in the benchmark */
44 private Set<String> userVms = new LinkedHashSet<String>();
45
46 /**
47 * Parameter values specified by the user on the command line. Parameters with
48 * no value in this multimap will get their values from the benchmark suite.
49 */
50 private Multimap<String, String> userParameters = LinkedHashMultimap.create();
51
52 /**
53 * Benchmark class specified by the user on the command line; or null to run
54 * the complete set of benchmark classes.
55 */
56 private Class<? extends Benchmark> userBenchmarkClass;
57
58 /**
59 * True if each benchmark should run in process.
60 */
61 private boolean inProcess;
62
63 private long warmupMillis = 5000;
64 private long runMillis = 5000;
65
66 /**
67 * Sets the named parameter to the specified value. This value will replace
68 * the benchmark suite's default values for the parameter. Multiple calls to
69 * this method will cause benchmarks for each value to be run.
70 */
71 void setParameter(String name, String value) {
72 userParameters.put(name, value);
73 }
74
75 private void prepareSuite() {
76 try {
77 @SuppressWarnings("unchecked") // guarded by the if statement that follows
78 Class<? extends BenchmarkSuite> suiteClass
79 = (Class<? extends BenchmarkSuite>) Class.forName(suiteClassName);
80 if (!BenchmarkSuite.class.isAssignableFrom(suiteClass)) {
81 throw new ConfigurationException(suiteClass + " is not a benchmark suite.");
82 }
83
84 Constructor<? extends BenchmarkSuite> constructor = suiteClass.getDeclaredConstructor();
85 suite = constructor.newInstance();
86 } catch (InvocationTargetException e) {
87 throw new ExecutionException(e.getCause());
88 } catch (Exception e) {
89 throw new ConfigurationException(e);
90 }
91 }
92
93 private void prepareParameters() {
94 for (String key : suite.parameterNames()) {
95 // first check if the user has specified values
96 Collection<String> userValues = userParameters.get(key);
97 if (!userValues.isEmpty()) {
98 parameters.putAll(key, userValues);
99 // TODO: type convert 'em to validate?
100
101 } else { // otherwise use the default values from the suite
102 Set<String> values = suite.parameterValues(key);
103 if (values.isEmpty()) {
104 throw new ConfigurationException(key + " has no values");
105 }
106 parameters.putAll(key, values);
107 }
108 }
109 }
110
Jesse Wilsone95457f2009-12-10 15:45:23 -0800111 private ImmutableSet<String> defaultVms() {
112 return "Dalvik".equals(System.getProperty("java.vm.name"))
113 ? ImmutableSet.of("dalvikvm")
114 : ImmutableSet.of("java");
115 }
116
Jesse Wilson109c1282009-12-08 13:45:25 -0800117 /**
118 * Returns a complete set of runs with every combination of values and
119 * benchmark classes.
120 */
121 private List<Run> createRuns() throws Exception {
122 List<RunBuilder> builders = new ArrayList<RunBuilder>();
123
124 // create runs for each benchmark class
125 Set<Class<? extends Benchmark>> benchmarkClasses = (userBenchmarkClass != null)
126 ? ImmutableSet.<Class<? extends Benchmark>>of(userBenchmarkClass)
127 : suite.benchmarkClasses();
128 for (Class<? extends Benchmark> benchmarkClass : benchmarkClasses) {
129 RunBuilder builder = new RunBuilder();
130 builder.benchmarkClass = benchmarkClass;
131 builders.add(builder);
132 }
133
134 // multiply the runs by the number of VMs
135 Set<String> vms = userVms.isEmpty()
Jesse Wilsone95457f2009-12-10 15:45:23 -0800136 ? defaultVms()
Jesse Wilson109c1282009-12-08 13:45:25 -0800137 : userVms;
138 Iterator<String> vmIterator = vms.iterator();
139 String firstVm = vmIterator.next();
140 for (RunBuilder builder : builders) {
141 builder.vm = firstVm;
142 }
143 int length = builders.size();
144 while (vmIterator.hasNext()) {
145 String alternateVm = vmIterator.next();
146 for (int s = 0; s < length; s++) {
147 RunBuilder copy = builders.get(s).copy();
148 copy.vm = alternateVm;
149 builders.add(copy);
150 }
151 }
152
153 for (Map.Entry<String, Collection<String>> parameter : parameters.asMap().entrySet()) {
154 Iterator<String> values = parameter.getValue().iterator();
155 if (!values.hasNext()) {
156 throw new ConfigurationException("Not enough values for " + parameter);
157 }
158
159 String key = parameter.getKey();
160
161 String firstValue = values.next();
162 for (RunBuilder builder : builders) {
163 builder.parameters.put(key, firstValue);
164 }
165
166 // multiply the size of the specs by the number of alternate values
167 length = builders.size();
168 while (values.hasNext()) {
169 String alternate = values.next();
170 for (int s = 0; s < length; s++) {
171 RunBuilder copy = builders.get(s).copy();
172 copy.parameters.put(key, alternate);
173 builders.add(copy);
174 }
175 }
176 }
177
178 List<Run> result = new ArrayList<Run>();
179 for (RunBuilder builder : builders) {
180 result.add(builder.build());
181 }
182
183 return result;
184 }
185
186 static class RunBuilder {
187 Map<String, String> parameters = new LinkedHashMap<String, String>();
188 Class<? extends Benchmark> benchmarkClass;
189 String vm;
190
191 RunBuilder copy() {
192 RunBuilder result = new RunBuilder();
193 result.parameters.putAll(parameters);
194 result.benchmarkClass = benchmarkClass;
195 result.vm = vm;
196 return result;
197 }
198
199 public Run build() {
200 return new Run(parameters, benchmarkClass, vm);
201 }
202 }
203
204 private double executeForked(Run run) {
205 ProcessBuilder builder = new ProcessBuilder();
206 List<String> command = builder.command();
207 command.addAll(Arrays.asList(run.getVm().split("\\s+")));
208 command.add("-cp");
209 command.add(System.getProperty("java.class.path"));
210 command.add(Runner.class.getName());
211 command.add("--warmupMillis");
212 command.add(String.valueOf(warmupMillis));
213 command.add("--runMillis");
214 command.add(String.valueOf(runMillis));
215 command.add("--inProcess");
216 command.add("--benchmark");
217 command.add(run.getBenchmarkClass().getName());
218 for (Map.Entry<String, String> entry : run.getParameters().entrySet()) {
219 command.add("-D" + entry.getKey() + "=" + entry.getValue());
220 }
221 command.add(suiteClassName);
222
223 BufferedReader reader = null;
224 try {
225 builder.redirectErrorStream(true);
226 builder.directory(new File(System.getProperty("user.dir")));
227 Process process = builder.start();
228
229 reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
230 String firstLine = reader.readLine();
231 Double nanosPerTrial = null;
232 try {
233 nanosPerTrial = Double.valueOf(firstLine);
234 } catch (NumberFormatException e) {
235 }
236
237 String anotherLine = reader.readLine();
238 if (nanosPerTrial != null && anotherLine == null) {
239 return nanosPerTrial;
240 }
241
242 String message = "Failed to execute " + command;
243 System.err.println(message);
244 System.err.println(" " + firstLine);
245 do {
246 System.err.println(" " + anotherLine);
247 } while ((anotherLine = reader.readLine()) != null);
248 throw new ConfigurationException(message);
249 } catch (IOException e) {
250 throw new ConfigurationException(e);
251 } finally {
252 if (reader != null) {
253 try {
254 reader.close();
255 } catch (IOException ignored) {
256 }
257 }
258 }
259 }
260
261 private Result runOutOfProcess() {
262 ImmutableMap.Builder<Run, Double> resultsBuilder = ImmutableMap.builder();
263
264 try {
265 List<Run> runs = createRuns();
266 int i = 0;
267 for (Run run : runs) {
268 beforeRun(i++, runs.size(), run);
269 double nanosPerTrial = executeForked(run);
270 afterRun(nanosPerTrial);
271 resultsBuilder.put(run, nanosPerTrial);
272 }
273 return new Result(resultsBuilder.build());
274 } catch (Exception e) {
275 throw new ExecutionException(e);
276 }
277 }
278
279 private void beforeRun(int index, int total, Run run) {
280 double percentDone = (double) index / total;
281 int runStringLength = 63; // so the total line length is 80
282 String runString = String.valueOf(run);
283 if (runString.length() > runStringLength) {
284 runString = runString.substring(0, runStringLength);
285 }
286 System.out.printf("%2.0f%% %-" + runStringLength + "s",
287 percentDone * 100, runString);
288 }
289
290 private void afterRun(double nanosPerTrial) {
291 System.out.printf(" %10.0fns%n", nanosPerTrial);
292 }
293
294 private void runInProcess() {
295 try {
296 Caliper caliper = new Caliper(warmupMillis, runMillis);
297
298 for (Run run : createRuns()) {
299 double result;
300 Benchmark benchmark = suite.createBenchmark(
301 run.getBenchmarkClass(), run.getParameters());
302 double warmupNanosPerTrial = caliper.warmUp(benchmark);
303 result = caliper.run(benchmark, warmupNanosPerTrial);
304 double nanosPerTrial = result;
305 System.out.println(nanosPerTrial);
306 }
307 } catch (Exception e) {
308 throw new ExecutionException(e);
309 }
310 }
311
312 private boolean parseArgs(String[] args) {
313 for (int i = 0; i < args.length; i++) {
314 if ("--help".equals(args[i])) {
315 return false;
316
317 } else if ("--benchmark".equals(args[i])) {
318 try {
319 @SuppressWarnings("unchecked") // guarded immediately afterwards!
320 Class<? extends Benchmark> c = (Class<? extends Benchmark>) Class.forName(args[++i]);
321 if (!Benchmark.class.isAssignableFrom(c)) {
322 System.out.println("Not a benchmark class: " + c);
323 return false;
324 }
325 userBenchmarkClass = c;
326 } catch (ClassNotFoundException e) {
327 throw new RuntimeException(e);
328 }
329
330 } else if ("--inProcess".equals(args[i])) {
331 inProcess = true;
332
333 } else if (args[i].startsWith("-D")) {
334 int equalsSign = args[i].indexOf('=');
335 if (equalsSign == -1) {
336 System.out.println("Malformed parameter " + args[i]);
337 return false;
338 }
339 String name = args[i].substring(2, equalsSign);
340 String value = args[i].substring(equalsSign + 1);
341 setParameter(name, value);
342
343 } else if ("--warmupMillis".equals(args[i])) {
344 warmupMillis = Long.parseLong(args[++i]);
345
346 } else if ("--runMillis".equals(args[i])) {
347 runMillis = Long.parseLong(args[++i]);
348
349 } else if ("--vm".equals(args[i])) {
350 userVms.add(args[++i]);
351
352 } else if (args[i].startsWith("-")) {
353 System.out.println("Unrecognized option: " + args[i]);
354 return false;
355
356 } else {
357 if (suiteClassName != null) {
358 System.out.println("Too many benchmark classes!");
359 return false;
360 }
361
362 suiteClassName = args[i];
363
364 }
365 }
366
367 if (inProcess && !userVms.isEmpty()) {
368 System.out.println("Cannot customize VM when running in process");
369 return false;
370 }
371
372 if (suiteClassName == null) {
373 System.out.println("No benchmark class provided.");
374 return false;
375 }
376
377 return true;
378 }
379
380 private void printUsage() {
381 System.out.println("Usage: Runner [OPTIONS...] <benchmark>");
382 System.out.println();
383 System.out.println(" <benchmark>: a benchmark class or suite");
384 System.out.println();
385 System.out.println("OPTIONS");
386 System.out.println();
387 System.out.println(" --D<param>=<value>: fix a benchmark parameter to a given value.");
388 System.out.println(" When multiple values for the same parameter are given (via");
389 System.out.println(" multiple --Dx=y args), all supplied values are used.");
390 System.out.println();
391 System.out.println(" --benchmark <class>: fix a benchmark executable to the named class");
392 System.out.println();
393 System.out.println(" --inProcess: run the benchmark in the same JVM rather than spawning");
394 System.out.println(" another with the same classpath. By default each benchmark is");
395 System.out.println(" run in a separate VM");
396 System.out.println();
397 System.out.println(" --warmupMillis <millis>: duration to warmup each benchmark");
398 System.out.println();
399 System.out.println(" --runMillis <millis>: duration to execute each benchmark");
400 System.out.println();
401 System.out.println(" --vm <vm>: executable to test benchmark on");
402
403 // adding new options? don't forget to update executeForked()
404 }
405
406 public static void main(String... args) {
407 Runner runner = new Runner();
408 if (!runner.parseArgs(args)) {
409 runner.printUsage();
410 return;
411 }
412
413 runner.prepareSuite();
414 runner.prepareParameters();
415 if (runner.inProcess) {
416 runner.runInProcess();
417 return;
418 }
419
420 Result result = runner.runOutOfProcess();
421 System.out.println();
422 new ConsoleReport(result).displayResults();
423 }
424
425 public static void main(Class<? extends BenchmarkSuite> suite, String... args) {
426 String[] argsWithSuiteName = new String[args.length + 1];
427 System.arraycopy(args, 0, argsWithSuiteName, 0, args.length);
428 argsWithSuiteName[args.length] = suite.getName();
429 main(argsWithSuiteName);
430 }
431}