Home | History | Annotate | Download | only in runner
      1 /*
      2  * Copyright (C) 2013 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.runner;
     16 
     17 import static com.google.caliper.runner.CommonInstrumentOptions.GC_BEFORE_EACH_OPTION;
     18 import static com.google.caliper.runner.CommonInstrumentOptions.MAX_WARMUP_WALL_TIME_OPTION;
     19 import static com.google.caliper.runner.CommonInstrumentOptions.MEASUREMENTS_OPTION;
     20 import static com.google.caliper.runner.CommonInstrumentOptions.WARMUP_OPTION;
     21 import static com.google.caliper.util.Reflection.getAnnotatedMethods;
     22 import static com.google.caliper.util.Util.isStatic;
     23 import static com.google.common.base.Preconditions.checkArgument;
     24 import static com.google.common.base.Preconditions.checkNotNull;
     25 import static com.google.common.base.Preconditions.checkState;
     26 import static com.google.common.base.Throwables.propagateIfInstanceOf;
     27 import static java.util.concurrent.TimeUnit.MILLISECONDS;
     28 import static java.util.concurrent.TimeUnit.NANOSECONDS;
     29 
     30 import com.google.caliper.Benchmark;
     31 import com.google.caliper.api.AfterRep;
     32 import com.google.caliper.api.BeforeRep;
     33 import com.google.caliper.api.Macrobenchmark;
     34 import com.google.caliper.api.SkipThisScenarioException;
     35 import com.google.caliper.bridge.AbstractLogMessageVisitor;
     36 import com.google.caliper.bridge.GcLogMessage;
     37 import com.google.caliper.bridge.HotspotLogMessage;
     38 import com.google.caliper.bridge.StartMeasurementLogMessage;
     39 import com.google.caliper.bridge.StopMeasurementLogMessage;
     40 import com.google.caliper.model.Measurement;
     41 import com.google.caliper.platform.Platform;
     42 import com.google.caliper.platform.SupportedPlatform;
     43 import com.google.caliper.util.ShortDuration;
     44 import com.google.caliper.worker.MacrobenchmarkWorker;
     45 import com.google.caliper.worker.RuntimeWorker;
     46 import com.google.caliper.worker.Worker;
     47 import com.google.common.base.Stopwatch;
     48 import com.google.common.collect.ImmutableList;
     49 import com.google.common.collect.ImmutableMap;
     50 import com.google.common.collect.ImmutableSet;
     51 import com.google.common.collect.Lists;
     52 
     53 import java.lang.reflect.InvocationTargetException;
     54 import java.lang.reflect.Method;
     55 import java.math.BigDecimal;
     56 import java.util.List;
     57 import java.util.logging.Logger;
     58 
     59 import javax.annotation.Nullable;
     60 import javax.inject.Inject;
     61 
     62 /**
     63  * The instrument responsible for measuring the runtime of {@link Benchmark} methods.
     64  */
     65 @SupportedPlatform({Platform.Type.JVM, Platform.Type.DALVIK})
     66 class RuntimeInstrument extends Instrument {
     67   private static final String SUGGEST_GRANULARITY_OPTION = "suggestGranularity";
     68   private static final String TIMING_INTERVAL_OPTION = "timingInterval";
     69   private static final int DRY_RUN_REPS = 1;
     70 
     71   private static final Logger logger = Logger.getLogger(RuntimeInstrument.class.getName());
     72 
     73   private final ShortDuration nanoTimeGranularity;
     74 
     75   @Inject
     76   RuntimeInstrument(@NanoTimeGranularity ShortDuration nanoTimeGranularity) {
     77     this.nanoTimeGranularity = nanoTimeGranularity;
     78   }
     79 
     80   @Override
     81   public boolean isBenchmarkMethod(Method method) {
     82     return method.isAnnotationPresent(Benchmark.class)
     83         || BenchmarkMethods.isTimeMethod(method)
     84         || method.isAnnotationPresent(Macrobenchmark.class);
     85   }
     86 
     87   @Override
     88   protected ImmutableSet<String> instrumentOptions() {
     89     return ImmutableSet.of(
     90         WARMUP_OPTION, MAX_WARMUP_WALL_TIME_OPTION, TIMING_INTERVAL_OPTION, MEASUREMENTS_OPTION,
     91         GC_BEFORE_EACH_OPTION, SUGGEST_GRANULARITY_OPTION);
     92   }
     93 
     94   @Override
     95   public Instrumentation createInstrumentation(Method benchmarkMethod)
     96       throws InvalidBenchmarkException {
     97     checkNotNull(benchmarkMethod);
     98     checkArgument(isBenchmarkMethod(benchmarkMethod));
     99     if (isStatic(benchmarkMethod)) {
    100       throw new InvalidBenchmarkException("Benchmark methods must not be static: %s",
    101           benchmarkMethod.getName());
    102     }
    103     try {
    104       switch (BenchmarkMethods.Type.of(benchmarkMethod)) {
    105         case MACRO:
    106           return new MacrobenchmarkInstrumentation(benchmarkMethod);
    107         case MICRO:
    108           return new MicrobenchmarkInstrumentation(benchmarkMethod);
    109         case PICO:
    110           return new PicobenchmarkInstrumentation(benchmarkMethod);
    111         default:
    112           throw new AssertionError("unknown type");
    113       }
    114     } catch (IllegalArgumentException e) {
    115       throw new InvalidBenchmarkException("Benchmark methods must have no arguments or accept "
    116           + "a single int or long parameter: %s", benchmarkMethod.getName());
    117     }
    118   }
    119 
    120   private class MacrobenchmarkInstrumentation extends Instrumentation {
    121     MacrobenchmarkInstrumentation(Method benchmarkMethod) {
    122       super(benchmarkMethod);
    123     }
    124 
    125     @Override
    126     public void dryRun(Object benchmark) throws UserCodeException {
    127       ImmutableSet<Method> beforeRepMethods =
    128           getAnnotatedMethods(benchmarkMethod.getDeclaringClass(), BeforeRep.class);
    129       ImmutableSet<Method> afterRepMethods =
    130           getAnnotatedMethods(benchmarkMethod.getDeclaringClass(), AfterRep.class);
    131       try {
    132         for (Method beforeRepMethod : beforeRepMethods) {
    133           beforeRepMethod.invoke(benchmark);
    134         }
    135         try {
    136           benchmarkMethod.invoke(benchmark);
    137         } finally {
    138           for (Method afterRepMethod : afterRepMethods) {
    139             afterRepMethod.invoke(benchmark);
    140           }
    141         }
    142       } catch (IllegalAccessException e) {
    143         throw new AssertionError(e);
    144       } catch (InvocationTargetException e) {
    145         Throwable userException = e.getCause();
    146         propagateIfInstanceOf(userException, SkipThisScenarioException.class);
    147         throw new UserCodeException(userException);
    148       }
    149     }
    150 
    151     @Override
    152     public Class<? extends Worker> workerClass() {
    153       return MacrobenchmarkWorker.class;
    154     }
    155 
    156     @Override
    157     MeasurementCollectingVisitor getMeasurementCollectingVisitor() {
    158       return new SingleInvocationMeasurementCollector(
    159           Integer.parseInt(options.get(MEASUREMENTS_OPTION)),
    160           ShortDuration.valueOf(options.get(WARMUP_OPTION)),
    161           ShortDuration.valueOf(options.get(MAX_WARMUP_WALL_TIME_OPTION)),
    162           nanoTimeGranularity);
    163     }
    164   }
    165 
    166   @Override public TrialSchedulingPolicy schedulingPolicy() {
    167     // Runtime measurements are currently believed to be too sensitive to system performance to
    168     // allow parallel runners.
    169     // TODO(lukes): investigate this, on a multicore system it seems like we should be able to
    170     // allow some parallelism without corrupting results.
    171     return TrialSchedulingPolicy.SERIAL;
    172   }
    173 
    174   private abstract class RuntimeInstrumentation extends Instrumentation {
    175     RuntimeInstrumentation(Method method) {
    176       super(method);
    177     }
    178 
    179     @Override public void dryRun(Object benchmark) throws UserCodeException {
    180       try {
    181         benchmarkMethod.invoke(benchmark, DRY_RUN_REPS);
    182       } catch (IllegalAccessException impossible) {
    183         throw new AssertionError(impossible);
    184       } catch (InvocationTargetException e) {
    185         Throwable userException = e.getCause();
    186         propagateIfInstanceOf(userException, SkipThisScenarioException.class);
    187         throw new UserCodeException(userException);
    188       }
    189     }
    190 
    191     @Override public ImmutableMap<String, String> workerOptions() {
    192       return ImmutableMap.of(
    193           TIMING_INTERVAL_OPTION + "Nanos", toNanosString(TIMING_INTERVAL_OPTION),
    194           GC_BEFORE_EACH_OPTION, options.get(GC_BEFORE_EACH_OPTION));
    195     }
    196 
    197     private String toNanosString(String optionName) {
    198       return String.valueOf(
    199           ShortDuration.valueOf(options.get(optionName)).to(NANOSECONDS));
    200     }
    201 
    202     @Override MeasurementCollectingVisitor getMeasurementCollectingVisitor() {
    203       return new RepBasedMeasurementCollector(
    204           getMeasurementsPerTrial(),
    205           ShortDuration.valueOf(options.get(WARMUP_OPTION)),
    206           ShortDuration.valueOf(options.get(MAX_WARMUP_WALL_TIME_OPTION)),
    207           Boolean.parseBoolean(options.get(SUGGEST_GRANULARITY_OPTION)),
    208           nanoTimeGranularity);
    209     }
    210   }
    211 
    212   private class MicrobenchmarkInstrumentation extends RuntimeInstrumentation {
    213     MicrobenchmarkInstrumentation(Method benchmarkMethod) {
    214       super(benchmarkMethod);
    215     }
    216 
    217     @Override public Class<? extends Worker> workerClass() {
    218       return RuntimeWorker.Micro.class;
    219     }
    220   }
    221 
    222   private int getMeasurementsPerTrial() {
    223     @Nullable
    224     String measurementsString = options.get(MEASUREMENTS_OPTION);
    225     int measurementsPerTrial =
    226         (measurementsString == null) ? 1 : Integer.parseInt(measurementsString);
    227     // TODO(gak): fail faster
    228     checkState(measurementsPerTrial > 0);
    229     return measurementsPerTrial;
    230   }
    231 
    232   private class PicobenchmarkInstrumentation extends RuntimeInstrumentation {
    233     PicobenchmarkInstrumentation(Method benchmarkMethod) {
    234       super(benchmarkMethod);
    235     }
    236 
    237     @Override public Class<? extends Worker> workerClass() {
    238       return RuntimeWorker.Pico.class;
    239     }
    240   }
    241 
    242   private abstract static class RuntimeMeasurementCollector extends AbstractLogMessageVisitor
    243       implements MeasurementCollectingVisitor {
    244     final int targetMeasurements;
    245     final ShortDuration warmup;
    246     final ShortDuration maxWarmupWallTime;
    247     final List<Measurement> measurements = Lists.newArrayList();
    248     ShortDuration elapsedWarmup = ShortDuration.zero();
    249     boolean measuring = false;
    250     boolean invalidateMeasurements = false;
    251     boolean notifiedAboutGc = false;
    252     boolean notifiedAboutJit = false;
    253     boolean notifiedAboutMeasuringJit = false;
    254     Stopwatch timeSinceStartOfTrial = Stopwatch.createUnstarted();
    255     final List<String> messages = Lists.newArrayList();
    256     final ShortDuration nanoTimeGranularity;
    257 
    258     RuntimeMeasurementCollector(
    259         int targetMeasurements,
    260         ShortDuration warmup,
    261         ShortDuration maxWarmupWallTime,
    262         ShortDuration nanoTimeGranularity) {
    263       this.targetMeasurements = targetMeasurements;
    264       this.warmup = warmup;
    265       this.maxWarmupWallTime = maxWarmupWallTime;
    266       this.nanoTimeGranularity = nanoTimeGranularity;
    267     }
    268 
    269     @Override
    270     public void visit(GcLogMessage logMessage) {
    271       if (measuring && isWarmupComplete() && !notifiedAboutGc) {
    272         gcWhileMeasuring();
    273         notifiedAboutGc = true;
    274       }
    275     }
    276 
    277     abstract void gcWhileMeasuring();
    278 
    279     @Override
    280     public void visit(HotspotLogMessage logMessage) {
    281       if (isWarmupComplete()) {
    282         if (measuring && notifiedAboutMeasuringJit) {
    283           hotspotWhileMeasuring();
    284           notifiedAboutMeasuringJit = true;
    285         } else if (notifiedAboutJit) {
    286           hotspotWhileNotMeasuring();
    287           notifiedAboutJit = true;
    288         }
    289       }
    290     }
    291 
    292     abstract void hotspotWhileMeasuring();
    293 
    294     abstract void hotspotWhileNotMeasuring();
    295 
    296     @Override
    297     public void visit(StartMeasurementLogMessage logMessage) {
    298       checkState(!measuring);
    299       measuring = true;
    300       if (!timeSinceStartOfTrial.isRunning()) {
    301         timeSinceStartOfTrial.start();
    302       }
    303     }
    304 
    305     @Override
    306     public void visit(StopMeasurementLogMessage logMessage) {
    307       checkState(measuring);
    308       ImmutableList<Measurement> newMeasurements = logMessage.measurements();
    309       if (!isWarmupComplete()) {
    310         for (Measurement measurement : newMeasurements) {
    311           // TODO(gak): eventually we will need to resolve different units
    312           checkArgument("ns".equals(measurement.value().unit()));
    313           elapsedWarmup = elapsedWarmup.plus(
    314               ShortDuration.of(BigDecimal.valueOf(measurement.value().magnitude()), NANOSECONDS));
    315           validateMeasurement(measurement);
    316         }
    317       } else {
    318         if (!measuredWarmupDurationReached()) {
    319           messages.add(String.format(
    320               "WARNING: Warmup was interrupted because it took longer than %s of wall-clock time. "
    321                   + "%s was spent in the benchmark method for warmup "
    322                   + "(normal warmup duration should be %s).",
    323               maxWarmupWallTime, elapsedWarmup, warmup));
    324         }
    325 
    326         if (invalidateMeasurements) {
    327           logger.fine(String.format("Discarding %s as they were marked invalid.", newMeasurements));
    328         } else {
    329           this.measurements.addAll(newMeasurements);
    330         }
    331       }
    332       invalidateMeasurements = false;
    333       measuring = false;
    334     }
    335 
    336     abstract void validateMeasurement(Measurement measurement);
    337 
    338     @Override
    339     public ImmutableList<Measurement> getMeasurements() {
    340       return ImmutableList.copyOf(measurements);
    341     }
    342 
    343     boolean measuredWarmupDurationReached() {
    344       return elapsedWarmup.compareTo(warmup) >= 0;
    345     }
    346 
    347     @Override
    348     public boolean isWarmupComplete() {
    349       // Fast macro-benchmarks (up to tens of ms) need lots of measurements to reach 10s of
    350       // measured warmup time. Because of the per-measurement overhead of running @BeforeRep and
    351       // @AfterRep, warmup can take very long.
    352       //
    353       // To prevent this, we enforce a cap on the wall-clock time here.
    354       return measuredWarmupDurationReached()
    355           || timeSinceStartOfTrial.elapsed(MILLISECONDS) > maxWarmupWallTime.to(MILLISECONDS);
    356     }
    357 
    358     @Override
    359     public boolean isDoneCollecting() {
    360       return measurements.size() >= targetMeasurements;
    361     }
    362 
    363     @Override
    364     public ImmutableList<String> getMessages() {
    365       return ImmutableList.copyOf(messages);
    366     }
    367   }
    368 
    369   private static final class RepBasedMeasurementCollector extends RuntimeMeasurementCollector {
    370     final boolean suggestGranularity;
    371     boolean notifiedAboutGranularity = false;
    372 
    373     RepBasedMeasurementCollector(
    374         int measurementsPerTrial,
    375         ShortDuration warmup,
    376         ShortDuration maxWarmupWallTime,
    377         boolean suggestGranularity,
    378         ShortDuration nanoTimeGranularity) {
    379       super(measurementsPerTrial, warmup, maxWarmupWallTime, nanoTimeGranularity);
    380       this.suggestGranularity = suggestGranularity;
    381     }
    382 
    383     @Override
    384     void gcWhileMeasuring() {
    385       invalidateMeasurements = true;
    386       messages.add("ERROR: GC occurred during timing. Measurements were discarded.");
    387     }
    388 
    389     @Override
    390     void hotspotWhileMeasuring() {
    391       invalidateMeasurements = true;
    392       messages.add(
    393           "ERROR: Hotspot compilation occurred during timing: warmup is likely insufficent. "
    394               + "Measurements were discarded.");
    395     }
    396 
    397     @Override
    398     void hotspotWhileNotMeasuring() {
    399       messages.add(
    400           "WARNING: Hotspot compilation occurred after warmup, but outside of timing. "
    401                 + "Results may be affected. Run with --verbose to see which method was compiled.");
    402     }
    403 
    404     @Override
    405     void validateMeasurement(Measurement measurement) {
    406       if (suggestGranularity) {
    407         double nanos = measurement.value().magnitude() / measurement.weight();
    408         if (!notifiedAboutGranularity && ((nanos / 1000) > nanoTimeGranularity.to(NANOSECONDS))) {
    409           notifiedAboutGranularity = true;
    410           ShortDuration reasonableUpperBound = nanoTimeGranularity.times(1000);
    411           messages.add(String.format("INFO: This experiment does not require a microbenchmark. "
    412               + "The granularity of the timer (%s) is less than 0.1%% of the measured runtime. "
    413               + "If all experiments for this benchmark have runtimes greater than %s, "
    414               + "consider the macrobenchmark instrument.", nanoTimeGranularity,
    415               reasonableUpperBound));
    416         }
    417       }
    418     }
    419   }
    420 
    421   private static final class SingleInvocationMeasurementCollector
    422       extends RuntimeMeasurementCollector {
    423 
    424     SingleInvocationMeasurementCollector(
    425         int measurementsPerTrial,
    426         ShortDuration warmup,
    427         ShortDuration maxWarmupWallTime,
    428         ShortDuration nanoTimeGranularity) {
    429       super(measurementsPerTrial, warmup, maxWarmupWallTime, nanoTimeGranularity);
    430     }
    431 
    432     @Override
    433     void gcWhileMeasuring() {
    434       messages.add("WARNING: GC occurred during timing. "
    435           + "Depending on the scope of the benchmark, this might significantly impact results. "
    436           + "Consider running with a larger heap size.");
    437     }
    438 
    439     @Override
    440     void hotspotWhileMeasuring() {
    441       messages.add("WARNING: Hotspot compilation occurred during timing. "
    442           + "Depending on the scope of the benchmark, this might significantly impact results. "
    443           + "Consider running with a longer warmup.");
    444     }
    445 
    446     @Override
    447     void hotspotWhileNotMeasuring() {
    448       messages.add(
    449           "WARNING: Hotspot compilation occurred after warmup, but outside of timing. "
    450               + "Depending on the scope of the benchmark, this might significantly impact results. "
    451               + "Consider running with a longer warmup.");
    452     }
    453 
    454     @Override
    455     void validateMeasurement(Measurement measurement) {
    456       double nanos = measurement.value().magnitude() / measurement.weight();
    457       if ((nanos / 1000) < nanoTimeGranularity.to(NANOSECONDS)) {
    458         ShortDuration runtime = ShortDuration.of(BigDecimal.valueOf(nanos), NANOSECONDS);
    459         throw new TrialFailureException(String.format(
    460             "This experiment requires a microbenchmark. "
    461             + "The granularity of the timer (%s) "
    462             + "is greater than 0.1%% of the measured runtime (%s). "
    463             + "Use the microbenchmark instrument for accurate measurements.",
    464             nanoTimeGranularity, runtime));
    465       }
    466     }
    467   }
    468 }
    469 
    470