1 /* 2 * Copyright (C) 2011 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.runner; 18 19 import static com.google.common.base.Preconditions.checkArgument; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 import static com.google.common.base.Throwables.propagateIfInstanceOf; 22 import static java.util.logging.Level.SEVERE; 23 24 import com.google.caliper.Benchmark; 25 import com.google.caliper.api.SkipThisScenarioException; 26 import com.google.caliper.config.VmConfig; 27 import com.google.caliper.platform.Platform; 28 import com.google.caliper.platform.SupportedPlatform; 29 import com.google.caliper.worker.MacrobenchmarkAllocationWorker; 30 import com.google.caliper.worker.MicrobenchmarkAllocationWorker; 31 import com.google.caliper.worker.Worker; 32 import com.google.common.base.Optional; 33 import com.google.common.base.Strings; 34 import com.google.common.collect.ImmutableMap; 35 import com.google.common.collect.ImmutableSet; 36 import com.google.monitoring.runtime.instrumentation.AllocationInstrumenter; 37 38 import java.io.File; 39 import java.io.IOException; 40 import java.lang.reflect.InvocationTargetException; 41 import java.lang.reflect.Method; 42 import java.util.jar.JarFile; 43 import java.util.jar.Manifest; 44 import java.util.logging.Logger; 45 46 /** 47 * {@link Instrument} that watches the memory allocations in an invocation of the 48 * benchmark method and reports some statistic. The benchmark method must accept a 49 * single int argument 'reps', which is the number of times to execute the guts of 50 * the benchmark method, and it must be public and non-static. 51 * 52 * <p>Note that the allocation instruments reports a "worst case" for allocation in that it reports 53 * the bytes and objects allocated in interpreted mode (no JIT). 54 */ 55 @SupportedPlatform(Platform.Type.JVM) 56 public final class AllocationInstrument extends Instrument { 57 private static final String ALLOCATION_AGENT_JAR_OPTION = "allocationAgentJar"; 58 /** 59 * If this option is set to {@code true} then every individual allocation will be tracked and 60 * logged. This will also increase the detail of certain error messages. 61 */ 62 private static final String TRACK_ALLOCATIONS_OPTION = "trackAllocations"; 63 private static final Logger logger = Logger.getLogger(AllocationInstrument.class.getName()); 64 65 @Override isBenchmarkMethod(Method method)66 public boolean isBenchmarkMethod(Method method) { 67 return method.isAnnotationPresent(Benchmark.class) || BenchmarkMethods.isTimeMethod(method); 68 } 69 70 @Override createInstrumentation(Method benchmarkMethod)71 public Instrumentation createInstrumentation(Method benchmarkMethod) 72 throws InvalidBenchmarkException { 73 checkNotNull(benchmarkMethod); 74 checkArgument(isBenchmarkMethod(benchmarkMethod)); 75 try { 76 switch (BenchmarkMethods.Type.of(benchmarkMethod)) { 77 case MACRO: 78 return new MacroAllocationInstrumentation(benchmarkMethod); 79 case MICRO: 80 case PICO: 81 return new MicroAllocationInstrumentation(benchmarkMethod); 82 default: 83 throw new AssertionError("unknown type"); 84 } 85 } catch (IllegalArgumentException e) { 86 throw new InvalidBenchmarkException("Benchmark methods must have no arguments or accept " 87 + "a single int or long parameter: %s", benchmarkMethod.getName()); 88 } 89 } 90 91 private final class MicroAllocationInstrumentation extends Instrumentation { MicroAllocationInstrumentation(Method benchmarkMethod)92 MicroAllocationInstrumentation(Method benchmarkMethod) { 93 super(benchmarkMethod); 94 } 95 96 @Override dryRun(Object benchmark)97 public void dryRun(Object benchmark) throws UserCodeException { 98 // execute the benchmark method, but don't try to take any measurements, because this JVM 99 // may not have the allocation instrumenter agent. 100 try { 101 benchmarkMethod.invoke(benchmark, 1); 102 } catch (IllegalAccessException impossible) { 103 throw new AssertionError(impossible); 104 } catch (InvocationTargetException e) { 105 Throwable userException = e.getCause(); 106 propagateIfInstanceOf(userException, SkipThisScenarioException.class); 107 throw new UserCodeException(userException); 108 } 109 } 110 workerOptions()111 @Override public ImmutableMap<String, String> workerOptions() { 112 return ImmutableMap.of(TRACK_ALLOCATIONS_OPTION, options.get(TRACK_ALLOCATIONS_OPTION)); 113 } 114 115 @Override workerClass()116 public Class<? extends Worker> workerClass() { 117 return MicrobenchmarkAllocationWorker.class; 118 } 119 120 @Override getMeasurementCollectingVisitor()121 MeasurementCollectingVisitor getMeasurementCollectingVisitor() { 122 return new Instrument.DefaultMeasurementCollectingVisitor( 123 ImmutableSet.of("bytes", "objects")); 124 } 125 } 126 schedulingPolicy()127 @Override public TrialSchedulingPolicy schedulingPolicy() { 128 // Assuming there is enough memory it should be fine to run these in parallel. 129 return TrialSchedulingPolicy.PARALLEL; 130 } 131 132 private final class MacroAllocationInstrumentation extends Instrumentation { MacroAllocationInstrumentation(Method benchmarkMethod)133 MacroAllocationInstrumentation(Method benchmarkMethod) { 134 super(benchmarkMethod); 135 } 136 137 @Override dryRun(Object benchmark)138 public void dryRun(Object benchmark) throws InvalidBenchmarkException { 139 // execute the benchmark method, but don't try to take any measurements, because this JVM 140 // may not have the allocation instrumenter agent. 141 try { 142 benchmarkMethod.invoke(benchmark); 143 } catch (IllegalAccessException impossible) { 144 throw new AssertionError(impossible); 145 } catch (InvocationTargetException e) { 146 Throwable userException = e.getCause(); 147 propagateIfInstanceOf(userException, SkipThisScenarioException.class); 148 throw new UserCodeException(userException); 149 } 150 } 151 workerOptions()152 @Override public ImmutableMap<String, String> workerOptions() { 153 return ImmutableMap.of(TRACK_ALLOCATIONS_OPTION, options.get(TRACK_ALLOCATIONS_OPTION)); 154 } 155 156 @Override workerClass()157 public Class<? extends Worker> workerClass() { 158 return MacrobenchmarkAllocationWorker.class; 159 } 160 161 @Override getMeasurementCollectingVisitor()162 MeasurementCollectingVisitor getMeasurementCollectingVisitor() { 163 return new Instrument.DefaultMeasurementCollectingVisitor( 164 ImmutableSet.of("bytes", "objects")); 165 } 166 } 167 168 @Override instrumentOptions()169 public ImmutableSet<String> instrumentOptions() { 170 return ImmutableSet.of(ALLOCATION_AGENT_JAR_OPTION, TRACK_ALLOCATIONS_OPTION); 171 } 172 findAllocationInstrumentJarOnClasspath()173 private static Optional<File> findAllocationInstrumentJarOnClasspath() throws IOException { 174 ImmutableSet<File> jarFiles = JarFinder.findJarFiles( 175 Thread.currentThread().getContextClassLoader(), 176 ClassLoader.getSystemClassLoader()); 177 for (File file : jarFiles) { 178 JarFile jarFile = null; 179 try { 180 jarFile = new JarFile(file); 181 Manifest manifest = jarFile.getManifest(); 182 if ((manifest != null) 183 && AllocationInstrumenter.class.getName().equals( 184 manifest.getMainAttributes().getValue("Premain-Class"))) { 185 return Optional.of(file); 186 } 187 } finally { 188 if (jarFile != null) { 189 jarFile.close(); 190 } 191 } 192 } 193 return Optional.absent(); 194 } 195 196 /** 197 * This instrument's worker requires the allocationinstrumenter agent jar, specified 198 * on the worker VM's command line with "-javaagent:[jarfile]". 199 */ getExtraCommandLineArgs(VmConfig vmConfig)200 @Override ImmutableSet<String> getExtraCommandLineArgs(VmConfig vmConfig) { 201 String agentJar = options.get(ALLOCATION_AGENT_JAR_OPTION); 202 if (Strings.isNullOrEmpty(agentJar)) { 203 try { 204 Optional<File> instrumentJar = findAllocationInstrumentJarOnClasspath(); 205 // TODO(gak): bundle up the allocation jar and unpack it if it's not on the classpath 206 if (instrumentJar.isPresent()) { 207 agentJar = instrumentJar.get().getAbsolutePath(); 208 } 209 } catch (IOException e) { 210 logger.log(SEVERE, 211 "An exception occurred trying to locate the allocation agent jar on the classpath", e); 212 } 213 } 214 if (Strings.isNullOrEmpty(agentJar) || !new File(agentJar).exists()) { 215 throw new IllegalStateException("Can't find required allocationinstrumenter agent jar"); 216 } 217 // Add microbenchmark args to minimize differences in the output 218 return new ImmutableSet.Builder<String>() 219 .addAll(super.getExtraCommandLineArgs(vmConfig)) 220 // we just run in interpreted mode to ensure that intrinsics don't break the instrumentation 221 .add("-Xint") 222 .add("-javaagent:" + agentJar) 223 // Some environments rename files and use symlinks to improve resource caching, 224 // if the agent jar path is actually a symlink it will prevent the agent from finding itself 225 // and adding itself to the bootclasspath, so we do it manually here. 226 .add("-Xbootclasspath/a:" + agentJar) 227 .build(); 228 } 229 } 230