content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements. See the NOTICE file distributed with this
   * work for additional information regarding copyright ownership. The ASF
   * licenses this file to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations under
   * the License.
   */
  package org.apache.bookkeeper.stats.codahale;
  
  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  import java.util.concurrent.ThreadLocalRandom;
  import java.util.concurrent.TimeUnit;
  import org.apache.bookkeeper.stats.OpStatsLogger;
  import org.apache.bookkeeper.stats.StatsLogger;
  import org.openjdk.jmh.annotations.Benchmark;
  import org.openjdk.jmh.annotations.BenchmarkMode;
  import org.openjdk.jmh.annotations.Fork;
  import org.openjdk.jmh.annotations.Level;
  import org.openjdk.jmh.annotations.Measurement;
  import org.openjdk.jmh.annotations.Mode;
  import org.openjdk.jmh.annotations.OutputTimeUnit;
  import org.openjdk.jmh.annotations.Param;
  import org.openjdk.jmh.annotations.Scope;
  import org.openjdk.jmh.annotations.Setup;
  import org.openjdk.jmh.annotations.State;
  import org.openjdk.jmh.annotations.Threads;
  import org.openjdk.jmh.annotations.Warmup;
  
  /**
   * Microbenchmarks for default and improved (fast) Codahale timers.
   * To run:
   * build project from command line (mvn clean install).
   * execute ./run.sh
   * Specify argument "TimerBenchmark" to only run this benchmark.
   */
  public class TimerBenchmark {
  
      /**
       * Type of Timer.
       */
      public enum TimerType {
          CodahaleTimer,
          FastTimer
      }
  
      /**
       * Thread-local state.
       */
      @State(Scope.Thread)
      public static class MyState {
  
          @Param
          public TimerType timerType;
  
          @Param({"1", "10", "100"})
          public int timersPerThread;
  
          private static OpStatsLogger[] timers;
          private int timerIdx = 0;
  
          private static long[] times;
          private int timeIdx = 0;
  
          @Setup(Level.Trial)
          @SuppressFBWarnings("SSD_DO_NOT_USE_INSTANCE_LOCK_ON_SHARED_STATIC_DATA")
          public void doSetup() throws Exception {
              StatsLogger logger = null;
              switch (timerType) {
                  case CodahaleTimer:
                      logger = new CodahaleMetricsProvider().getStatsLogger("test");
                      break;
                  case FastTimer:
                      logger = new FastCodahaleMetricsProvider().getStatsLogger("test");
                      break;
              }
  
              synchronized (MyState.class) {
                  // timers (and response times) are shared across threads to test
                  // concurrency of timer updates.
                  if (timers == null) {
                      timers = new OpStatsLogger[timersPerThread];
                      for (int i = 0; i < timersPerThread; i++) {
                          timers[i] = logger.getOpStatsLogger("test-timer-" + i);
                      }
  
                      // just a bunch of random response times to not always hit the same bucket
                      times = new long[1000];
                      for (int i = 0; i < times.length; i++) {
                          times[i] = Math.abs(ThreadLocalRandom.current().nextLong() % 1000);
                      }
                  }
              }
          }
  
          public OpStatsLogger getTimer() {
              return timers[(timerIdx++) % timers.length];
          }
  
          public long getTime() {
              return times[(timeIdx++) % times.length];
          }
  
          public boolean isGetSnapshot() {
              // create a snapshot every 109 operations (typically snapshot creations will be much more infrequent)
              // 109 is prime, guaranteeing that we will create snapshots across all timers
              if (timeIdx % 109 == 0) {
                  timeIdx++;
                  return true;
              } else {
                  return false;
              }
          }
  
      }
  
      /**
       * Tests the performance of (concurrent) timer updates.
       * Note that test duration must exceed TIME_WINDOW (default: 60) to trigger
       * FastTimer's housekeeping. Manual tests show little performance difference
       * for longer running tests (since housekeeping is infrequent and cheap), so
       * we keep the test duration low to not have tests run for too long.
       * @param state
       */
      @Benchmark
      @BenchmarkMode(Mode.Throughput)
      @OutputTimeUnit(TimeUnit.MILLISECONDS)
      @Warmup(iterations = 2, time = 3, timeUnit = TimeUnit.SECONDS)
      @Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
      @Threads(4)
      @Fork(value = 1, warmups = 1)
      public void timerTest(MyState state) {
          state.getTimer().registerSuccessfulEvent(state.getTime(), TimeUnit.MILLISECONDS);
      }
  
      /**
       * Tests the performance of (concurrent) timer updates with
       * the creation of snapshots. We expect snapshot creation to
       * be infrequent (e.g. once every N seconds), while timer updates
       * are frequent (for many timers hundreds or thousands of times
       * per second). Here we're testing the creation of snapshots at
       * a rate much higher than we would expect in real life.
       * @param state
       */
      @Benchmark
      @BenchmarkMode(Mode.Throughput)
      @OutputTimeUnit(TimeUnit.MILLISECONDS)
      @Warmup(iterations = 2, time = 3, timeUnit = TimeUnit.SECONDS)
      @Measurement(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
      @Threads(4)
      @Fork(value = 1, warmups = 1)
      public void timerTestWithSnapshots(MyState state) {
          OpStatsLogger timer = state.getTimer();
          if (state.isGetSnapshot()) {
              timer.toOpStatsData();
          } else {
              timer.registerSuccessfulEvent(state.getTime(), TimeUnit.MILLISECONDS);
          }
      }
  
      /**
       * Test routing for manual testing of memory footprint of default Codahale Timer vs. improved FastTimer.
       * JMH can't do that, so we have a small stand-alone test routine here.
       * Run with:
       * <code>
       *  java -Xmx1g -cp target/benchmarks.jar \
       *     org.apache.bookkeeper.stats.codahale.TimerBenchmark &lt;codahale|fast&gt;
       * </code>
       * @param args
       */
      public static void main(String[] args) {
          if (args.length != 1
                  || (!args[0].equalsIgnoreCase("codahale") && !args[0].equalsIgnoreCase("fast"))) {
              System.out.println("usage: " + TimerBenchmark.class.getCanonicalName() + " <codahale|fast>");
              System.exit(1);
          }
          StatsLogger logger = null;
          if (args[0].equalsIgnoreCase("codahale")) {
              logger = new CodahaleMetricsProvider().getStatsLogger("test");
          } else {
              logger = new FastCodahaleMetricsProvider().getStatsLogger("test");
          }
          System.out.println("Using " + logger.getClass().getCanonicalName());
          System.out.println("Creating 1000 OpStatsLoggers (2000 Timers) and updating each of them 1000 times ...");
          OpStatsLogger[] timers = new OpStatsLogger[1000];
          for (int i = 0; i < timers.length; i++) {
              timers[i] = logger.getOpStatsLogger("test-timer-" + i);
          }
          long[] times = new long[199]; // 199 is prime, so each timer will get each time
          for (int i = 0; i < times.length; i++) {
              times[i] = Math.abs(ThreadLocalRandom.current().nextLong() % 1000);
          }
          for (int i = 0; i < 1000 * timers.length; i++) {
              timers[i % timers.length].registerSuccessfulEvent(times[i % times.length], TimeUnit.MILLISECONDS);
              timers[i % timers.length].registerFailedEvent(times[i % times.length], TimeUnit.MILLISECONDS);
          }
          times = null; // let it become garbage
          System.out.println("Done.");
          System.out.println("Now run 'jmap -histo:live <pid>' on this JVM to get a heap histogram, then kill this JVM.");
          while (true) {
              try {
                  TimeUnit.MILLISECONDS.sleep(1000);
              } catch (Exception e) {
                  // ignore
              }
          }
      }
  
  }
  