Paul Duffin | 7fc0b45 | 2015-11-10 17:45:15 +0000 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package com.google.caliper; |
| 18 | |
| 19 | import com.google.caliper.UserException.DisplayUsageException; |
| 20 | import com.google.caliper.UserException.ExceptionFromUserCodeException; |
| 21 | import com.google.caliper.util.InterleavedReader; |
| 22 | import com.google.common.base.Joiner; |
| 23 | import com.google.common.base.Splitter; |
| 24 | import com.google.common.collect.ImmutableList; |
| 25 | import com.google.common.collect.ImmutableMap; |
| 26 | import com.google.common.collect.ObjectArrays; |
| 27 | import com.google.common.io.Closeables; |
| 28 | import com.google.gson.JsonObject; |
| 29 | |
| 30 | import java.io.BufferedReader; |
| 31 | import java.io.File; |
| 32 | import java.io.FileFilter; |
| 33 | import java.io.FileInputStream; |
| 34 | import java.io.FileNotFoundException; |
| 35 | import java.io.FileOutputStream; |
| 36 | import java.io.IOException; |
| 37 | import java.io.InputStream; |
| 38 | import java.io.InputStreamReader; |
| 39 | import java.io.PrintStream; |
| 40 | import java.net.HttpURLConnection; |
| 41 | import java.net.InetSocketAddress; |
| 42 | import java.net.Proxy; |
| 43 | import java.net.URL; |
| 44 | import java.text.SimpleDateFormat; |
| 45 | import java.util.Date; |
| 46 | import java.util.List; |
| 47 | import java.util.Locale; |
| 48 | import java.util.Map; |
| 49 | import java.util.Map.Entry; |
| 50 | import java.util.TimeZone; |
| 51 | import java.util.regex.Pattern; |
| 52 | |
| 53 | /** |
| 54 | * Creates, executes and reports benchmark runs. |
| 55 | */ |
| 56 | public final class Runner { |
| 57 | |
| 58 | private static final FileFilter UPLOAD_FILE_FILTER = new FileFilter() { |
| 59 | @Override public boolean accept(File file) { |
| 60 | return file.getName().endsWith(".xml") || file.getName().endsWith(".json"); |
| 61 | } |
| 62 | }; |
| 63 | |
| 64 | private static final String FILE_NAME_DATE_FORMAT = "yyyy-MM-dd'T'HH-mm-ssZ"; |
| 65 | |
| 66 | private static final Splitter ARGUMENT_SPLITTER |
| 67 | = Splitter.on(Pattern.compile("\\s+")).omitEmptyStrings(); |
| 68 | |
| 69 | /** Command line arguments to the process */ |
| 70 | private Arguments arguments; |
| 71 | private ScenarioSelection scenarioSelection; |
| 72 | |
| 73 | private String createFileName(Result result) { |
| 74 | String timestamp = createTimestamp(); |
| 75 | return String.format("%s.%s.json", result.getRun().getBenchmarkName(), timestamp); |
| 76 | } |
| 77 | |
| 78 | private String createTimestamp() { |
| 79 | SimpleDateFormat dateFormat = new SimpleDateFormat(FILE_NAME_DATE_FORMAT, Locale.US); |
| 80 | dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); |
| 81 | dateFormat.setLenient(true); |
| 82 | return dateFormat.format(new Date()); |
| 83 | } |
| 84 | |
| 85 | public void run(String... args) { |
| 86 | this.arguments = Arguments.parse(args); |
| 87 | File resultsUploadFile = arguments.getUploadResultsFile(); |
| 88 | if (resultsUploadFile != null) { |
| 89 | uploadResultsFileOrDir(resultsUploadFile); |
| 90 | return; |
| 91 | } |
| 92 | this.scenarioSelection = new ScenarioSelection(arguments); |
| 93 | if (arguments.getDebug()) { |
| 94 | debug(); |
| 95 | return; |
| 96 | } |
| 97 | Result result = runOutOfProcess(); |
| 98 | new ConsoleReport(result.getRun(), arguments).displayResults(); |
| 99 | boolean saveResultsLocally = arguments.getSaveResultsFile() != null; |
| 100 | try { |
| 101 | postResults(result); |
| 102 | } catch (Exception e) { |
| 103 | System.out.println(); |
| 104 | System.out.println(e); |
| 105 | saveResultsLocally = true; |
| 106 | } |
| 107 | |
| 108 | if (saveResultsLocally) { |
| 109 | saveResults(result); |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | void uploadResultsFileOrDir(File resultsFileOrDir) { |
| 114 | try { |
| 115 | if (resultsFileOrDir.isDirectory()) { |
| 116 | for (File resultsFile : resultsFileOrDir.listFiles(UPLOAD_FILE_FILTER)) { |
| 117 | uploadResults(resultsFile); |
| 118 | } |
| 119 | } else { |
| 120 | uploadResults(resultsFileOrDir); |
| 121 | } |
| 122 | } catch (Exception e) { |
| 123 | throw new RuntimeException("uploading XML file failed", e); |
| 124 | } |
| 125 | } |
| 126 | |
| 127 | private void uploadResults(File resultsUploadFile) throws IOException { |
| 128 | System.out.println(); |
| 129 | System.out.println("Uploading " + resultsUploadFile.getCanonicalPath()); |
| 130 | InputStream inputStream = new FileInputStream(resultsUploadFile); |
| 131 | try { |
| 132 | Result result = new ResultsReader().getResult(inputStream); |
| 133 | postResults(result); |
| 134 | } finally { |
| 135 | inputStream.close(); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | private void saveResults(Result result) { |
| 140 | File resultsFile = arguments.getSaveResultsFile(); |
| 141 | File destinationFile; |
| 142 | if (resultsFile == null) { |
| 143 | File dir = new File("./caliper-results"); |
| 144 | dir.mkdirs(); |
| 145 | destinationFile = new File(dir, createFileName(result)); |
| 146 | } else if (resultsFile.exists() && resultsFile.isDirectory()) { |
| 147 | destinationFile = new File(resultsFile, createFileName(result)); |
| 148 | } else { |
| 149 | // assume this is a file |
| 150 | File parent = resultsFile.getParentFile(); |
| 151 | if (parent != null) { |
| 152 | parent.mkdirs(); |
| 153 | } |
| 154 | destinationFile = resultsFile; |
| 155 | } |
| 156 | |
| 157 | PrintStream filePrintStream; |
| 158 | try { |
| 159 | filePrintStream = new PrintStream(new FileOutputStream(destinationFile)); |
| 160 | } catch (FileNotFoundException e) { |
| 161 | throw new RuntimeException("can't open " + destinationFile, e); |
| 162 | } |
| 163 | String resultJson = Json.getGsonInstance().toJson(result); |
| 164 | try { |
| 165 | System.out.println(); |
| 166 | System.out.println("Writing results to " + destinationFile.getCanonicalPath()); |
| 167 | filePrintStream.print(resultJson); |
| 168 | } catch (Exception e) { |
| 169 | System.out.println(e); |
| 170 | System.out.println("Failed to write results to file, writing to standard out instead:"); |
| 171 | System.out.println(resultJson); |
| 172 | System.out.flush(); |
| 173 | } finally { |
| 174 | filePrintStream.close(); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | private void postResults(Result result) { |
| 179 | CaliperRc caliperrc = CaliperRc.INSTANCE; |
| 180 | String postUrl = caliperrc.getPostUrl(); |
| 181 | String apiKey = caliperrc.getApiKey(); |
| 182 | if (postUrl == null || apiKey == null) { |
| 183 | // TODO: probably nicer to show a message if only one is null |
| 184 | return; |
| 185 | } |
| 186 | |
| 187 | try { |
| 188 | URL url = new URL(postUrl + apiKey + "/" + result.getRun().getBenchmarkName()); |
| 189 | HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(getProxy()); |
| 190 | urlConnection.setDoOutput(true); |
| 191 | String resultJson = Json.getGsonInstance().toJson(result); |
| 192 | urlConnection.getOutputStream().write(resultJson.getBytes()); |
| 193 | if (urlConnection.getResponseCode() == 200) { |
| 194 | System.out.println(""); |
| 195 | System.out.println("View current and previous benchmark results online:"); |
| 196 | BufferedReader in = new BufferedReader( |
| 197 | new InputStreamReader(urlConnection.getInputStream())); |
| 198 | System.out.println(" " + in.readLine()); |
| 199 | in.close(); |
| 200 | return; |
| 201 | } |
| 202 | |
| 203 | System.out.println("Posting to " + postUrl + " failed: " |
| 204 | + urlConnection.getResponseMessage()); |
| 205 | BufferedReader reader = new BufferedReader( |
| 206 | new InputStreamReader(urlConnection.getInputStream())); |
| 207 | String line; |
| 208 | while ((line = reader.readLine()) != null) { |
| 209 | System.out.println(line); |
| 210 | } |
| 211 | reader.close(); |
| 212 | } catch (IOException e) { |
| 213 | throw new RuntimeException("Posting to " + postUrl + " failed.", e); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | private Proxy getProxy() { |
| 218 | String proxyAddress = CaliperRc.INSTANCE.getProxy(); |
| 219 | if (proxyAddress == null) { |
| 220 | return Proxy.NO_PROXY; |
| 221 | } |
| 222 | |
| 223 | String[] proxyHostAndPort = proxyAddress.trim().split(":"); |
| 224 | return new Proxy(Proxy.Type.HTTP, new InetSocketAddress( |
| 225 | proxyHostAndPort[0], Integer.parseInt(proxyHostAndPort[1]))); |
| 226 | } |
| 227 | |
| 228 | private ScenarioResult runScenario(Scenario scenario) { |
| 229 | MeasurementResult timeMeasurementResult = measure(scenario, MeasurementType.TIME); |
| 230 | MeasurementSet allocationMeasurements = null; |
| 231 | String allocationEventLog = null; |
| 232 | MeasurementSet memoryMeasurements = null; |
| 233 | String memoryEventLog = null; |
| 234 | if (arguments.getMeasureMemory()) { |
| 235 | MeasurementResult allocationsMeasurementResult = |
| 236 | measure(scenario, MeasurementType.INSTANCE); |
| 237 | allocationMeasurements = allocationsMeasurementResult.getMeasurements(); |
| 238 | allocationEventLog = allocationsMeasurementResult.getEventLog(); |
| 239 | MeasurementResult memoryMeasurementResult = |
| 240 | measure(scenario, MeasurementType.MEMORY); |
| 241 | memoryMeasurements = memoryMeasurementResult.getMeasurements(); |
| 242 | memoryEventLog = memoryMeasurementResult.getEventLog(); |
| 243 | } |
| 244 | |
| 245 | return new ScenarioResult(timeMeasurementResult.getMeasurements(), |
| 246 | timeMeasurementResult.getEventLog(), |
| 247 | allocationMeasurements, allocationEventLog, |
| 248 | memoryMeasurements, memoryEventLog); |
| 249 | } |
| 250 | |
| 251 | private static class MeasurementResult { |
| 252 | private final MeasurementSet measurements; |
| 253 | private final String eventLog; |
| 254 | |
| 255 | MeasurementResult(MeasurementSet measurements, String eventLog) { |
| 256 | this.measurements = measurements; |
| 257 | this.eventLog = eventLog; |
| 258 | } |
| 259 | |
| 260 | public MeasurementSet getMeasurements() { |
| 261 | return measurements; |
| 262 | } |
| 263 | |
| 264 | public String getEventLog() { |
| 265 | return eventLog; |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | private MeasurementResult measure(Scenario scenario, MeasurementType type) { |
| 270 | Vm vm = new VmFactory().createVm(scenario); |
| 271 | // this must be done before starting the forked process on certain VMs |
| 272 | ProcessBuilder processBuilder = createCommand(scenario, vm, type) |
| 273 | .redirectErrorStream(true); |
| 274 | Process timeProcess; |
| 275 | try { |
| 276 | timeProcess = processBuilder.start(); |
| 277 | } catch (IOException e) { |
| 278 | throw new RuntimeException("failed to start subprocess", e); |
| 279 | } |
| 280 | |
| 281 | MeasurementSet measurementSet = null; |
| 282 | StringBuilder eventLog = new StringBuilder(); |
| 283 | InterleavedReader reader = null; |
| 284 | try { |
| 285 | reader = new InterleavedReader(arguments.getMarker(), |
| 286 | new InputStreamReader(timeProcess.getInputStream())); |
| 287 | Object o; |
| 288 | while ((o = reader.read()) != null) { |
| 289 | if (o instanceof String) { |
| 290 | eventLog.append(o); |
| 291 | } else if (measurementSet == null) { |
| 292 | JsonObject jsonObject = (JsonObject) o; |
| 293 | measurementSet = Json.measurementSetFromJson(jsonObject); |
| 294 | } else { |
| 295 | throw new RuntimeException("Unexpected value: " + o); |
| 296 | } |
| 297 | } |
| 298 | } catch (IOException e) { |
| 299 | throw new RuntimeException(e); |
| 300 | } finally { |
| 301 | Closeables.closeQuietly(reader); |
| 302 | timeProcess.destroy(); |
| 303 | } |
| 304 | |
| 305 | if (measurementSet == null) { |
| 306 | String message = "Failed to execute " + Joiner.on(" ").join(processBuilder.command()); |
| 307 | System.err.println(" " + message); |
| 308 | System.err.println(eventLog.toString()); |
| 309 | throw new ConfigurationException(message); |
| 310 | } |
| 311 | |
| 312 | return new MeasurementResult(measurementSet, eventLog.toString()); |
| 313 | } |
| 314 | |
| 315 | private ProcessBuilder createCommand(Scenario scenario, Vm vm, MeasurementType type) { |
| 316 | File workingDirectory = new File(System.getProperty("user.dir")); |
| 317 | |
| 318 | String classPath = System.getProperty("java.class.path"); |
| 319 | if (classPath == null || classPath.length() == 0) { |
| 320 | throw new IllegalStateException("java.class.path is undefined in " + System.getProperties()); |
| 321 | } |
| 322 | |
| 323 | ImmutableList.Builder<String> vmArgs = ImmutableList.builder(); |
| 324 | vmArgs.addAll(ARGUMENT_SPLITTER.split(scenario.getVariables().get(Scenario.VM_KEY))); |
| 325 | if (type == MeasurementType.INSTANCE || type == MeasurementType.MEMORY) { |
| 326 | String allocationJarFile = System.getenv("ALLOCATION_JAR"); |
| 327 | vmArgs.add("-javaagent:" + allocationJarFile); |
| 328 | } |
| 329 | vmArgs.addAll(vm.getVmSpecificOptions(type, arguments)); |
| 330 | |
| 331 | Map<String, String> vmParameters = scenario.getVariables( |
| 332 | scenarioSelection.getVmParameterNames()); |
| 333 | for (String vmParameter : vmParameters.values()) { |
| 334 | vmArgs.addAll(ARGUMENT_SPLITTER.split(vmParameter)); |
| 335 | } |
| 336 | |
| 337 | ImmutableList.Builder<String> caliperArgs = ImmutableList.builder(); |
| 338 | caliperArgs.add("--warmupMillis").add(Long.toString(arguments.getWarmupMillis())); |
| 339 | caliperArgs.add("--runMillis").add(Long.toString(arguments.getRunMillis())); |
| 340 | caliperArgs.add("--measurementType").add(type.toString()); |
| 341 | caliperArgs.add("--marker").add(arguments.getMarker()); |
| 342 | |
| 343 | Map<String,String> userParameters = scenario.getVariables( |
| 344 | scenarioSelection.getUserParameterNames()); |
| 345 | for (Entry<String, String> entry : userParameters.entrySet()) { |
| 346 | caliperArgs.add("-D" + entry.getKey() + "=" + entry.getValue()); |
| 347 | } |
| 348 | caliperArgs.add(arguments.getSuiteClassName()); |
| 349 | |
| 350 | return vm.newProcessBuilder(workingDirectory, classPath, |
| 351 | vmArgs.build(), InProcessRunner.class.getName(), caliperArgs.build()); |
| 352 | } |
| 353 | |
| 354 | private void debug() { |
| 355 | try { |
| 356 | int debugReps = arguments.getDebugReps(); |
| 357 | InProcessRunner runner = new InProcessRunner(); |
| 358 | DebugMeasurer measurer = new DebugMeasurer(debugReps); |
| 359 | for (Scenario scenario : scenarioSelection.select()) { |
| 360 | System.out.println("running " + debugReps + " debug reps of " + scenario); |
| 361 | runner.run(scenarioSelection, scenario, measurer); |
| 362 | } |
| 363 | } catch (Exception e) { |
| 364 | throw new ExceptionFromUserCodeException(e); |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | private Result runOutOfProcess() { |
| 369 | Date executedDate = new Date(); |
| 370 | ImmutableMap.Builder<Scenario, ScenarioResult> resultsBuilder = ImmutableMap.builder(); |
| 371 | |
| 372 | try { |
| 373 | List<Scenario> scenarios = scenarioSelection.select(); |
| 374 | |
| 375 | int i = 0; |
| 376 | for (Scenario scenario : scenarios) { |
| 377 | beforeMeasurement(i++, scenarios.size(), scenario); |
| 378 | ScenarioResult scenarioResult = runScenario(scenario); |
| 379 | afterMeasurement(arguments.getMeasureMemory(), scenarioResult); |
| 380 | resultsBuilder.put(scenario, scenarioResult); |
| 381 | } |
| 382 | System.out.println(); |
| 383 | |
| 384 | Environment environment = new EnvironmentGetter().getEnvironmentSnapshot(); |
| 385 | return new Result( |
| 386 | new Run(resultsBuilder.build(), arguments.getSuiteClassName(), executedDate), |
| 387 | environment); |
| 388 | } catch (Exception e) { |
| 389 | throw new ExceptionFromUserCodeException(e); |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | private void beforeMeasurement(int index, int total, Scenario scenario) { |
| 394 | double percentDone = (double) index / total; |
| 395 | System.out.printf("%2.0f%% %s", percentDone * 100, scenario); |
| 396 | } |
| 397 | |
| 398 | private void afterMeasurement(boolean memoryMeasured, ScenarioResult scenarioResult) { |
| 399 | String memoryMeasurements = ""; |
| 400 | if (memoryMeasured) { |
| 401 | MeasurementSet instanceMeasurementSet = |
| 402 | scenarioResult.getMeasurementSet(MeasurementType.INSTANCE); |
| 403 | String instanceUnit = |
| 404 | ConsoleReport.UNIT_ORDERING.min(instanceMeasurementSet.getUnitNames().entrySet()).getKey(); |
| 405 | MeasurementSet memoryMeasurementSet = scenarioResult.getMeasurementSet(MeasurementType.MEMORY); |
| 406 | String memoryUnit = |
| 407 | ConsoleReport.UNIT_ORDERING.min(memoryMeasurementSet.getUnitNames().entrySet()).getKey(); |
| 408 | memoryMeasurements = String.format(", allocated %s%s for a total of %s%s", |
| 409 | Math.round(instanceMeasurementSet.medianUnits()), instanceUnit, |
| 410 | Math.round(memoryMeasurementSet.medianUnits()), memoryUnit); |
| 411 | } |
| 412 | |
| 413 | MeasurementSet timeMeasurementSet = scenarioResult.getMeasurementSet(MeasurementType.TIME); |
| 414 | String unit = |
| 415 | ConsoleReport.UNIT_ORDERING.min(timeMeasurementSet.getUnitNames().entrySet()).getKey(); |
| 416 | System.out.printf(" %.2f %s; \u03C3=%.2f %s @ %d trials%s%n", timeMeasurementSet.medianUnits(), |
| 417 | unit, timeMeasurementSet.standardDeviationUnits(), unit, |
| 418 | timeMeasurementSet.getMeasurements().size(), memoryMeasurements); |
| 419 | } |
| 420 | |
| 421 | public static void main(String[] args) { |
| 422 | try { |
| 423 | new Runner().run(args); |
| 424 | System.exit(0); // user code may have leave non-daemon threads behind! |
| 425 | } catch (DisplayUsageException e) { |
| 426 | e.display(); |
| 427 | System.exit(0); |
| 428 | } catch (UserException e) { |
| 429 | e.display(); |
| 430 | System.exit(1); |
| 431 | } |
| 432 | } |
| 433 | |
| 434 | @SuppressWarnings("unchecked") // temporary fakery |
| 435 | public static void main(Class<? extends Benchmark> suite, String[] args) { |
| 436 | main(ObjectArrays.concat(args, suite.getName())); |
| 437 | } |
| 438 | } |