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 static org.junit.Assert.assertEquals;
  import static org.junit.Assert.assertTrue;
  
  import com.codahale.metrics.Snapshot;
  import java.util.ArrayList;
  import java.util.Random;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicInteger;
  import org.junit.Test;
  
  /**
   * Unit tests for FastTimer.
   *
   */
  public class FastTimerTest {
  
      /*
       * To simplify testing, we're over-riding the time source used by FastTimer with some
       * fake time we're incrementing manually. This speeds-up testing (we don't have to wait
       * for real seconds to elapse) and also guarantees deterministic behavior for the unit
       * test.
       */
      private static AtomicInteger mockedTime = new AtomicInteger(0);
  
      private int incSec() {
          return mockedTime.incrementAndGet();
      }
  
      private FastTimer getMockedFastTimer(int timeWindowSeconds, FastTimer.Buckets buckets) {
          return new FastTimer(timeWindowSeconds, buckets) {
              @Override
              protected int getTime() {
                  return mockedTime.get();
              }
          };
      }
  
      @Test
      public void testMeanRate() {
          FastTimer t = getMockedFastTimer(1, FastTimer.Buckets.fine);
  
          t.update(10, TimeUnit.NANOSECONDS);
          assertTrue("should calculate mean before advancing time", t.getMeanRate() > 0);
  
          incSec();
          assertTrue("should calculate mean after advancing time", t.getMeanRate() > 0);
     }
  
      @Test
      public void testBuckets() {
          FastTimer t = new FastTimer(1, FastTimer.Buckets.fine);
          for (int b = 0; b < t.getNumberOfBuckets(); b++) {
              long lowerBound = b > 0 ? t.getBucketBound(b - 1) + 1 : 0;
              long bucketMean = t.getBucketValue(b);
              long upperBound = t.getBucketBound(b);
              System.out.println(String.format("Bucket %3d [%12d - %12d], avg=%12d",
                      b, lowerBound, upperBound, bucketMean));
              assertEquals(String.format("bucket for lowerBound value %d", lowerBound),
                      b, t.getBucket(lowerBound));
              assertEquals(String.format("bucket for bucketMean value %d", bucketMean),
                      b, t.getBucket(bucketMean));
              assertEquals(String.format("bucket for upperBound value %d", upperBound),
                      b, t.getBucket(upperBound));
              if (b > 0) {
                  assertEquals(String.format("bucket before bucket %d", b), b - 1, t.getBucket(lowerBound - 1));
              }
              if (b + 1 < t.getNumberOfBuckets()) {
                  assertEquals(String.format("bucket after bucket %d", b), b + 1, t.getBucket(upperBound + 1));
              }
          }
      }
  
      @Test
      public void testFunctional() {
          FastTimer t = getMockedFastTimer(1, FastTimer.Buckets.fine);
          for (int i = 0; i <= 10000; i++) {
              t.update(i, TimeUnit.MICROSECONDS);
          }
          incSec(); // advance mocked time to next second
          Snapshot s = t.getSnapshot();
          assertEquals("FastTimer.getCount()", 10001, t.getCount());
          assertEquals("FastSnapshot.getMin()", 1, s.getMin());
          assertEquals("FastSnapshot.getMax()", TimeUnit.MICROSECONDS.toNanos(10000), s.getMax());
          assertEquals("FastSnapshot.getMean()", TimeUnit.MICROSECONDS.toNanos(5000), (long) s.getMean());
          assertEquals("FastSnapshot.getMedian()", TimeUnit.MICROSECONDS.toNanos(5000), (long) s.getMedian());
          assertEquals("FastSnapshot.getValue(0.1)", TimeUnit.MICROSECONDS.toNanos(1000), (long) s.getValue(0.1));
          assertEquals("FastSnapshot.getValue(0.9)", TimeUnit.MICROSECONDS.toNanos(9000), (long) s.getValue(0.9));
          assertEquals("FastSnapshot.getValue(0.99)", TimeUnit.MICROSECONDS.toNanos(9900), (long) s.getValue(0.99));
      }
  
      @Test
      public void testTimer() {
          // load definitions for testing the timer
          // following 3 array lengths must match: each element defines values for one phase
          final int[] timeRange = new int[] {   90,  190,   50,   90, 100, 100 };
          final int[] timeBase = new int[]  {   10,   10,   50,   10,   0,   0 };
          final int[] rate = new int[]      { 1000, 1000, 1000, 1000,   0,   1 };
  
          final int window = 5; // use a 5 second window for testing
          FastTimer t = getMockedFastTimer(window, FastTimer.Buckets.fine);
          Random r = new Random(12345); // fixed random seed for deterministic value distribution
          int phase = 0;
          int sec = 0;
  
          long count = 0;
          // start generating test load for each of the configured phases
          while (phase < timeRange.length) {
              for (int i = 0; i < rate[phase]; i++) {
                  t.update(r.nextInt(timeRange[phase]) + timeBase[phase], TimeUnit.MILLISECONDS);
                  count++;
              }
              incSec(); // advance mocked time to next second
              if (++sec % window == 0) {
                  // every WINDOW seconds, check the timer values
                  Snapshot s = t.getSnapshot();
                  System.out.println(String.format(
                          "phase %3d: count=%10d, rate=%6.0f, min=%6.1f, avg=%6.1f, q99=%6.1f, max=%6.1f",
                          phase, t.getCount(), t.getMeanRate(), ((double) s.getMin()) / 1000000.0,
                          s.getMean() / 1000000.0, s.getValue(0.99) / 1000000.0, ((double) s.getMax()) / 1000000.0));
  
                  // check count (events the timer has ever seen)
                  assertEquals("FastTimer.getCount()", count, t.getCount());
                  // check rate (should be precisely the configured rate)
                  assertEquals("FastTimer.getMeanRate()", rate[phase],
                          (int) Math.round(t.getMeanRate()));
                  assertEquals("FastTimer.getOneMinuteRate()", rate[phase],
                          (int) Math.round(t.getOneMinuteRate()));
                  assertEquals("FastTimer.getFiveMinuteRate()", rate[phase],
                          (int) Math.round(t.getFiveMinuteRate()));
                  assertEquals("FastTimer.getFifteenMinuteRate()", rate[phase],
                          (int) Math.round(t.getFifteenMinuteRate()));
                  // at rates > 1000 (with fixed seed), we know that the following checks will be successful
                  if (t.getMeanRate() >= 1000) {
                      // check minimum value == lower bound
                      assertEquals("FastSnapshot.getMin()", timeBase[phase], s.getMin() / 1000000);
                      // check maximum value == upper bound
                      assertEquals("FastSnapshot.getMax()", timeBase[phase] + timeRange[phase] - 1,
                              (s.getMax() / 1000000));
                      // check 99th percentile == upper bound
                      assertEquals("FastSnapshot.getValue(0.99)",
                              t.getBucketBound(t.getBucket(
                                      TimeUnit.MILLISECONDS.toNanos(timeBase[phase] + timeRange[phase] - 1))),
                              (long) s.getValue(0.99));
                      // check mean is within 10% of configured mean
                      assertEquals("FastSnapshot.getMean()", (timeBase[phase] + (timeRange[phase] / 2)) / 10,
                              (int) (Math.round(s.getMean() / 1000000) / 10));
                  }
  
                  // start next phase
                  phase++;
              }
          }
      }
  
      @Test
      public void testTimerMultiThreaded() {
          final int window = 5; // use a 5 second window for testing
          FastTimer t = getMockedFastTimer(window, FastTimer.Buckets.fine);
  
          // start 10 threads, which each update the timer 1000 times
          ArrayList<Thread> threads = new ArrayList<Thread>();
          for (int i = 0; i < 10; i++) {
              Thread thread = new Thread(() -> {
                  for (int j = 0; j < 1000; j++) {
                      t.update(10, TimeUnit.MILLISECONDS);
                  }
              });
              threads.add(thread);
              thread.start();
          }
          // wait for 10 threads to finish
          for (Thread thread : threads) {
              try {
                  thread.join();
              } catch (InterruptedException e) {
                  // ignore
              }
          }
          incSec(); // advance mocked time to next second
  
          assertEquals("FastTimer.getCount()", 10000, t.getCount());
          assertEquals("FastTimer.getMeanRate()", 2000, (int) Math.round(t.getMeanRate()));
  
          Snapshot s = t.getSnapshot();
          assertEquals("FastSnapshot.getMin()", 10, s.getMin() / 1000000);
          assertEquals("FastSnapshot.getMax()", 10, (s.getMax() / 1000000));
          assertEquals("FastSnapshot.getValue(0.99)", 10, Math.round(s.getValue(0.99) / 1000000));
          assertEquals("FastSnapshot.getMean()", 10, (int) Math.round(s.getMean() / 1000000));
      }
  
      @Test
      public void testTimerNoBuckets() {
          final int window = 5; // use a 5 second window for testing
          FastTimer t = getMockedFastTimer(window, FastTimer.Buckets.none);
  
          for (int i = 0; i < 1000; i++) {
              t.update(10, TimeUnit.MILLISECONDS);
          }
          incSec(); // advance mocked time to next second
  
          assertEquals("FastTimer.getCount()", 1000, t.getCount());
          assertEquals("FastTimer.getMeanRate()", 200, (int) Math.round(t.getMeanRate()));
  
          Snapshot s = t.getSnapshot();
          assertEquals("FastSnapshot.getMin()", 10, s.getMin() / 1000000);
          assertEquals("FastSnapshot.getMax()", 10, (s.getMax() / 1000000));
          assertEquals("FastSnapshot.getValue(0.99)", 0, Math.round(s.getValue(0.99) / 1000000));
          assertEquals("FastSnapshot.getMean()", 10, (int) Math.round(s.getMean() / 1000000));
      }
  
      @Test
      public void testSnapshotOutOfSync() {
          FastTimer t = getMockedFastTimer(1, FastTimer.Buckets.fine);
          t.update(t.getBucketBound(0) - 1, TimeUnit.NANOSECONDS); // add value to 1st bucket
          t.update(t.getBucketBound(1) - 1, TimeUnit.NANOSECONDS); // add value to 2nd bucket
          t.update(t.getBucketBound(2) - 1, TimeUnit.NANOSECONDS); // add value to 3rd bucket
          incSec(); // advance mocked time to next second
          Snapshot s1 = t.getSnapshot();
          long[] buckets = new long[t.getNumberOfBuckets()];
          buckets[0] = 1;
          buckets[1] = 1;
          buckets[2] = 1;
          Snapshot s2 = new FastSnapshot(t,
                  t.getBucketBound(0) - 1,
                  t.getBucketBound(2) - 1,
                  t.getBucketBound(0) + t.getBucketBound(1) + t.getBucketBound(2) + 3,
                  4, // count (4) is out of sync with number of recorded events in buckets (3)
                  buckets);
          assertEquals("FastSnapshot.getMin()", s1.getMin(), s2.getMin());
          assertEquals("FastSnapshot.getMax()", s1.getMax(), s2.getMax());
          assertEquals("FastSnapshot.getValue(0.95)", (long) s1.getValue(0.95), (long) s2.getValue(0.95));
      }
  
  }
  