You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bookkeeper.apache.org by si...@apache.org on 2018/04/25 09:10:07 UTC

[bookkeeper] branch branch-4.7 updated: Issue #1363: Fast and Garbage-Free Codahale Statistics Timers

This is an automated email from the ASF dual-hosted git repository.

sijie pushed a commit to branch branch-4.7
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/branch-4.7 by this push:
     new 31a4292  Issue #1363: Fast and Garbage-Free Codahale Statistics Timers
31a4292 is described below

commit 31a42924b6efa0a246b2c27189152cc5b1bc2baa
Author: Nicolas Michael <nm...@salesforce.com>
AuthorDate: Wed Apr 25 02:09:36 2018 -0700

    Issue #1363: Fast and Garbage-Free Codahale Statistics Timers
    
    This change introduces a new Codahale-based metrics provider "FastCodahaleMetricsProvider" in the codahale-metrics-provider artifact, which can be activated through the "statsProviderClass" config parameter (no changes made to the existing "CodahaleMetricsProvider"). This new "FastCodahaleMetricsProvider" brings a new timer implementation "FastTimer", which extends Codahale's Timer class, provides an identical API, is registered in Codahale's timer registry, and can be queried through [...]
    
    The improvements of FastTimer over Codahale default Timer with ExponentiallyDecayingReservoirs are:
    * entirely garbage-free Timer updates (only snapshot creations when querying a timer allocate a small number of Java objects)
    * 3x faster Timer updates (i.e. 3x shorter code-path to update a timer)
    * half the memory footprint (in default configuration)
    
    FastTimer reduces GC pause times in our experiments as described in Issue 1363 by half (same as entirely deactivating all timers).
    
    The primary functional differences between FastTimer and Codahale default Timer with ExponentiallyDecayingReservoirs are:
    * FastTimer keeps values for a fixed, configurable time period (default: 60 seconds) rather than exponentially decaying them
    * FastTimer bucketizes event times rather than storing discrete values. Percentile values depend on the bucket resolution.
    
    Design Considerations
    ---------------------
    
    The design goals of this timer implementation are for it to be
     - fast (i.e. few instructions to update a timer)
     - scalable (i.e. little synchronization cost for concurrent timer updates)
     - garbage-free for timer updates (i.e. no object allocation for timer updates)
     - space-efficient (i.e. as little memory footprint as possible while achieving first three goals)
     - provide similar functionality as Codahale's default timers with ExponentiallyDecayingReservoirs
    
    This implementation provides rate and response times over a configurable sliding time window. Data
    is stored in upfront allocated circular arrays, in which each array element holds data
    for one second. Data is overwritten in a circular fashion without the allocation of new data
    structures and is therefore garbage-free for all timer updates.
    
    This implementation does not store individual response times, but instead allocates bucketized counters
    upfront, which are incremented for any event falling into a particular response time bucket. A
    fine-grained bucket definition (intended for capturing successsful events) and a coarse-grained
    bucket definition (intended to capture failure or timed-out events) are provided.
    
    To improve scalability of concurrent timer updates, most data structures are replicated HASH_SIZE
    times, and calling threads updating a timer are hashed to individual instances. Performance tests
    (see below) have shown that this implementation is light-weight enough to achieve slightly better
    scalability than Codahale's default timers even without hashing, and can further improve scalability
    if hashing is used.
    
    Trading off performance and scalability vs. memory footprint, we need to be conservative in the hash
    size we chose. Different flavors of this timer implementation have been evaluated using JMH
    micro-benchmarks (see microbenchmarks/src/main/java/org/apache/bookkeeper/stats/TimerBenchmark.java),
    comparing implementations of FastTimer with a time window of 60 seconds and
    - (DEV1)   a HASH_SIZE of 3 for all data structures (meters, counters, min/max, and response time buckets)
    - (DEV2)   a HASH_SIZE of 1 for all data structures (meters, counters, min/max, and response time buckets)
    - (FINAL)  a HASH_SIZE of 3 for meters, counters, min/max, and no hashing for response time buckets
    to the default timer implementation
    - (BASE-E) Codahale Timer with ExponentiallyDecayingReservoir (default as used by bookkeeper code)
    - (BASE-T) Codahale Timer with SlidingTimeWindowReservoir configured to 60 seconds
    - (BASE-S) Codahale Timer with SlidingWindowReservoir configured to hold 100,000 events.
    
    Based on results below, implementation (FINAL) was chosen as the final FastTimer implementation, as it
    achieves nearly the same throughput as (DEV1) at nearly the same memory footprint as (DEV2), and
    ultimately achieves roughly 3x higher throughput and scalability that Codahale's default implementation
    at around half the memory footprint.
    
    The following results have been collected on an eight core x86 server running at 3.2 GHz (updated
    timers are shared across 4 threads):
    
    ```
    Config   Timer Impl             Timers   Threads      ops/ms   Alloc B/op   Kb/TimerPair
    ----------------------------------------------------------------------------------------
     DEV1    FastTimer (Hash 3)          1         4   11487.904        0                253
     DEV1    FastTimer (Hash 3)         10         4   22621.702        0                253
     DEV1    FastTimer (Hash 3)        100         4   21781.319        0                253
     DEV2    FastTimer (Hash 1)          1         4    5138.143        0                 88
     DEV2    FastTimer (Hash 1)         10         4   22902.195        0                 88
     DEV2    FastTimer (Hash 1)        100         4   19173.085        0                 88
     FINAL   FastTimer (Hash 3/1)        1         4    9291.002        0                 99
     FINAL   FastTimer (Hash 3/1)       10         4   16379.940        0                 99
     FINAL   FastTimer (Hash 3/1)      100         4   16751.020        0                 99
     BASE-E  CodahaleTimer               1         4    3845.187       82.609            189
     BASE-E  CodahaleTimer              10         4    7262.445       35.035            189
     BASE-E  CodahaleTimer             100         4    7051.77        32.843            189
     BASE-T  CodahaleTimer/TimeWindow    1         4     102.479       90.851            174
     BASE-T  CodahaleTimer/TimeWindow   10         4      68.852       84.812            174
     BASE-T  CodahaleTimer/TimeWindow  100         4     153.444      136.436            174
     BASE-S  CodahaleTimer/SlidingWdw    1         4    4670.543        0               2103
     BASE-S  CodahaleTimer/SlidingWdw   10         4   13696.168        0               2103
     BASE-S  CodahaleTimer/SlidingWdw  100         4   12541.936        0               2103
    ```
    
    - ops/ms is the number of timer updates per millisecond.
    - Alloc B/op is the number of bytes allocated per timer update
    - Kb/TimerPair is the heap footprint per pair of timers (one with fine-grained, one with coarse-grained buckets)
    
    The following test results include snapshot creation every 109 timer updates (typically, we would assume
    snapshot creation to be much less frequent), and show that also with snapshots in the mix, FastTimer outperforms
    Codahale default Timers both with respect to throughput and scalability as well as object allocation:
    
    ```
    Config   Timer Impl             Timers   Threads      ops/ms   Alloc B/op
    -------------------------------------------------------------------------
     FINAL   FastTimer (Hash 3/1)        1         4    1569.953       23.707
     FINAL   FastTimer (Hash 3/1)       10         4    7316.794       24.073
     FINAL   FastTimer (Hash 3/1)      100         4    6498.215       24.073
     BASE-E  CodahaleTimer               1         4     246.953      481.771
     BASE-E  CodahaleTimer              10         4    1989.134      476.807
     BASE-E  CodahaleTimer             100         4    1514.729      468.624
     BASE-T  CodahaleTimer/TimeWindow    1         4       6.063    43795.810
     BASE-T  CodahaleTimer/TimeWindow   10         4      44.651    33916.315
     BASE-T  CodahaleTimer/TimeWindow  100         4     180.431    12330.939
     BASE-S  CodahaleTimer/SlidingWdw    1         4      17.439    14683.756
     BASE-S  CodahaleTimer/SlidingWdw   10         4     107.257    14683.745
     BASE-S  CodahaleTimer/SlidingWdw  100         4     236.538     9767.106
    ```
    
    Unfortunately Codahale does not have a Timer interface we can implement, and some Codahale
    base classes are assuming instances of Timer (for example, our JettyServices instantiate a
    Codahale MetricsServlet, which instantiates a Codahale MetricsModule, which only serializes
    timers that are instances of Timer class into the json output stream). Unless we wanted to
    reimplement or override all these base classes, we can't just implement Codahale's Metered and Sampling
    interfaces. Instead we have to extend its Timer class, even though we're not using any of its
    inherited functionality or data structures. The inherited (unused) member variables of Codahale Timer
    consume slightly less than 512 byte per FastTimer (measured around 425 byte in Codahale 3.1).
    Above memory footprint results include ~ 1 kb of inherited (unused) data structures, which comprise
    around 1% of FastTimer's overall memory footprint.
    
    In terms of functionality, FastTimer provides the same functionality as Codahale's timers
    (in default configuration with ExponentiallyDecayingReservoirs), with the following exceptions:
    - Statistics are kept for a fixed amount of time (rather than exponentially decayed), by
      default 60 seconds. As a consequence, getMeanRate(), getOneMinuteRate(),  getFiveMinuteRate()
      and getFifteenMinuteRate() all return the same value if FastTimer is configured to use a
      60 second time window.
    - FastTimer and FastSnapshot only record bucketized instead of discrete response times. As a
      consequence, the accuracy of percentiles depends on bucket granularity. FastSnapshot also
      can't return discrete values: getValues() returns an empty array, and size returns 0.
    
    Author: Nicolas Michael <nm...@salesforce.com>
    
    Reviewers: Enrico Olivelli <eo...@gmail.com>, Sijie Guo <si...@apache.org>
    
    This closes #1364 from nicmichael/FastTimer, closes #1363
    
    (cherry picked from commit ed3b0105d0e1adbb118641cc39f886659d140361)
    
    Signed-off-by: Sijie Guo <si...@apache.org>
    
    Signed-off-by: Sijie Guo <si...@apache.org>
---
 .../stats/codahale/CodahaleStatsLogger.java        |   2 +-
 .../codahale/FastCodahaleMetricsProvider.java}     |  23 +-
 .../stats/codahale/FastCodahaleStatsLogger.java    |  92 ++++
 .../bookkeeper/stats/codahale/FastSnapshot.java    | 108 ++++
 .../bookkeeper/stats/codahale/FastTimer.java       | 612 +++++++++++++++++++++
 .../stats/codahale/CodahaleOpStatsTest.java        |  10 +
 .../bookkeeper/stats/codahale/FastTimerTest.java   | 219 ++++++++
 .../main/resources/bookkeeper/findbugsExclude.xml  |   3 +
 microbenchmarks/run.sh                             |   2 +-
 .../bookkeeper/stats/StatsLoggerBenchmark.java     |   4 +-
 .../bookkeeper/stats/codahale/TimerBenchmark.java  | 214 +++++++
 11 files changed, 1272 insertions(+), 17 deletions(-)

diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/CodahaleStatsLogger.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/CodahaleStatsLogger.java
index 1f6bc18..dba2e12 100644
--- a/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/CodahaleStatsLogger.java
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/CodahaleStatsLogger.java
@@ -97,7 +97,7 @@ public class CodahaleStatsLogger implements StatsLogger {
     @Override
     public StatsLogger scope(String scope) {
         String scopeName;
-        if (0 == basename.length()) {
+        if (basename == null || 0 == basename.length()) {
             scopeName = scope;
         } else {
             scopeName = name(basename, scope);
diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastCodahaleMetricsProvider.java
similarity index 57%
copy from bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java
copy to bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastCodahaleMetricsProvider.java
index 54efaee..cc5e615 100644
--- a/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastCodahaleMetricsProvider.java
@@ -16,23 +16,18 @@
  */
 package org.apache.bookkeeper.stats.codahale;
 
-import static org.junit.Assert.assertEquals;
-
-import org.apache.bookkeeper.stats.OpStatsData;
-import org.apache.bookkeeper.stats.OpStatsLogger;
-import org.junit.Test;
+import org.apache.bookkeeper.stats.StatsLogger;
+import org.apache.bookkeeper.stats.StatsProvider;
 
 /**
- * Unit test of {@link CodahaleOpStatsLogger}.
+ * A {@link StatsProvider} implemented based on <i>Codahale</i> metrics library.
  */
+@SuppressWarnings("deprecation")
+public class FastCodahaleMetricsProvider extends CodahaleMetricsProvider {
 
-public class CodahaleOpStatsTest {
-    @Test
-    public void testToOpStatsData() {
-        OpStatsLogger logger = new CodahaleMetricsProvider().getStatsLogger("test").getOpStatsLogger("testLogger");
-        logger.registerSuccessfulValue(1);
-        // the following should not throw any exception
-        OpStatsData statsData = logger.toOpStatsData();
-        assertEquals(1, statsData.getNumSuccessfulEvents());
+    @Override
+    public StatsLogger getStatsLogger(String name) {
+        initIfNecessary();
+        return new FastCodahaleStatsLogger(getMetrics(), name);
     }
 }
diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastCodahaleStatsLogger.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastCodahaleStatsLogger.java
new file mode 100644
index 0000000..39ac54d
--- /dev/null
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastCodahaleStatsLogger.java
@@ -0,0 +1,92 @@
+/**
+ * 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 com.codahale.metrics.MetricRegistry.name;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.bookkeeper.stats.OpStatsLogger;
+import org.apache.bookkeeper.stats.StatsLogger;
+
+/**
+ * A {@link StatsLogger} implemented based on <i>Codahale</i> metrics library.
+ */
+public class FastCodahaleStatsLogger extends CodahaleStatsLogger {
+
+    private static final ConcurrentHashMap<String, CodahaleOpStatsLogger> statsLoggerCache =
+            new ConcurrentHashMap<String, CodahaleOpStatsLogger>();
+
+    FastCodahaleStatsLogger(MetricRegistry metrics, String basename) {
+        super(metrics, basename);
+    }
+
+    @Override
+    @SuppressFBWarnings(
+            value = {
+                    "JLM_JSR166_UTILCONCURRENT_MONITORENTER",
+                    "AT_OPERATION_SEQUENCE_ON_CONCURRENT_ABSTRACTION"
+            },
+            justification = "We use synchronized (statsLoggerCache) to make get/put atomic")
+    public OpStatsLogger getOpStatsLogger(String statName) {
+        CodahaleOpStatsLogger logger;
+        String nameSuccess = name(basename, statName);
+        logger = statsLoggerCache.get(nameSuccess);
+        if (logger == null) {
+            synchronized (statsLoggerCache) {
+                // check again now that we have the lock
+                logger = statsLoggerCache.get(nameSuccess);
+                if (logger == null) {
+                    String nameFailure = name(basename, statName + "-fail");
+                    FastTimer success;
+                    FastTimer failure;
+                    Map<String, Timer> timers = metrics.getTimers();
+                    success = timers != null ? (FastTimer) timers.get(nameSuccess) : null;
+                    if (success == null) {
+                        success = new FastTimer(60, FastTimer.Buckets.fine);
+                        metrics.register(nameSuccess, success);
+                    }
+                    failure = timers != null ? (FastTimer) timers.get(nameFailure) : null;
+                    if (failure == null) {
+                        failure = new FastTimer(60, FastTimer.Buckets.coarse);
+                        metrics.register(nameFailure, failure);
+                    }
+                    logger = new CodahaleOpStatsLogger(success, failure);
+                    statsLoggerCache.put(nameSuccess, logger);
+                }
+            }
+        }
+        return logger;
+    }
+
+    @Override
+    public StatsLogger scope(String scope) {
+        String scopeName;
+        if (basename == null || 0 == basename.length()) {
+            scopeName = scope;
+        } else {
+            scopeName = name(basename, scope);
+        }
+        return new FastCodahaleStatsLogger(metrics, scopeName);
+    }
+
+}
diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastSnapshot.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastSnapshot.java
new file mode 100644
index 0000000..ee6466e
--- /dev/null
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastSnapshot.java
@@ -0,0 +1,108 @@
+/**
+ * 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 com.codahale.metrics.Snapshot;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.io.OutputStream;
+
+/**
+ * A snapshot of a FastTimer.
+ */
+public class FastSnapshot extends Snapshot {
+
+    private static final long[] EMPTY_VALUES = new long[] {};
+
+    private final FastTimer timer;
+    private final long min;
+    private final long max;
+    private final long sum;
+    private final long cnt;
+    private final long[] values;
+
+    @SuppressFBWarnings(
+            value = "EI_EXPOSE_REP2",
+            justification = "long[] values is newly created array; FastTimer does not hold on to reference")
+    public FastSnapshot(FastTimer timer, long min, long max, long sum, long cnt, long[] values) {
+        this.timer = timer;
+        this.min = min;
+        this.max = max;
+        this.sum = sum;
+        this.cnt = cnt;
+        this.values = values;
+    }
+
+    @Override
+    public double getValue(double quantile) {
+        if (cnt == 0 || values == null) {
+            return 0;
+        }
+        long qcnt = 0;
+        for (int i = 0; i < values.length; i++) {
+            qcnt += values[i];
+            if (((double) qcnt) / ((double) cnt) > quantile) {
+                return timer.getBucketBound(i);
+            }
+        }
+        return timer.getBucketBound(values.length);
+    }
+
+    @Override
+    public long[] getValues() {
+        return EMPTY_VALUES; // values in this snapshot represent percentile buckets, but not discrete values
+    }
+
+    @Override
+    public int size() {
+        return 0; // values in this snapshot represent percentile buckets, but not discrete values
+    }
+
+    @Override
+    public long getMax() {
+        return max;
+    }
+
+    @Override
+    public double getMean() {
+        return cnt > 0 ? ((double) sum) / ((double) cnt) : 0;
+    }
+
+    @Override
+    public long getMin() {
+        return min;
+    }
+
+    @Override
+    public double getStdDev() {
+        if (cnt < 2 || values == null) {
+            return 0;
+        }
+        double avg = getMean();
+        double var = 0;
+        for (int i = 0; i < values.length; i++) {
+            double val = timer.getBucketValue(i);
+            var += ((double) values[i]) * Math.pow(val - avg, 2);
+         }
+        return Math.sqrt(var / ((double) cnt));
+    }
+
+    @Override
+    public void dump(OutputStream output) {
+        // values in this snapshot represent percentile buckets, but not discrete values
+    }
+
+}
\ No newline at end of file
diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastTimer.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastTimer.java
new file mode 100644
index 0000000..935b95f
--- /dev/null
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/main/java/org/apache/bookkeeper/stats/codahale/FastTimer.java
@@ -0,0 +1,612 @@
+/**
+ * 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 com.codahale.metrics.Reservoir;
+import com.codahale.metrics.Snapshot;
+import com.codahale.metrics.Timer;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * A fast and (nearly) garbage-free Rate and Response Times Timer.
+ * FastTimer uses circular arrays which are allocated upfront.
+ * Timer updates or queries never allocate new objects and thus never
+ * create garbage.
+ * A small number of new objects are allocated for snapshots when
+ * calling getSnapshot().
+ */
+public class FastTimer extends Timer {
+
+    /*
+     * Design Considerations
+     * ---------------------
+     *
+     * The design goals of this timer implementation are for it to be
+     *  - fast (i.e. few instructions to update a timer)
+     *  - scalable (i.e. little synchronization cost for concurrent timer updates)
+     *  - garbage-free for timer updates (i.e. no object allocation for timer updates)
+     *  - space-efficient (i.e. as little memory footprint as possible while achieving first three goals)
+     *  - provide similar functionality as Codahale's default timers with ExponentiallyDecayingReservoirs
+     *
+     * This implementation provides rate and response times over a configurable sliding time window. Data
+     * is stored in upfront allocated circular arrays, in which each array element holds data
+     * for one second. Data is overwritten in a circular fashion without the allocation of new data
+     * structures and is therefore garbage-free for all timer updates.
+     *
+     * This implementation does not store individual response times, but instead allocates bucketized counters
+     * upfront, which are incremented for any event falling into a particular response time bucket. A
+     * fine-grained bucket definition (intended for capturing successsful events) and a coarse-grained
+     * bucket definition (intended to capture failure or timed-out events) are provided.
+     *
+     * To improve scalability of concurrent timer updates, most data structures are replicated HASH_SIZE
+     * times, and calling threads updating a timer are hashed to individual instances. Performance tests
+     * (see below) have shown that this implementation is light-weight enough to achieve slightly better
+     * scalability than Codahale's default timers even without hashing, and can further improve scalability
+     * if hashing is used.
+     *
+     * Trading off performance and scalability vs. memory footprint, we need to be conservative in the hash
+     * size we chose. Different flavors of this timer implementation have been evaluated using JMH
+     * micro-benchmarks (see microbenchmarks/src/main/java/org/apache/bookkeeper/stats/TimerBenchmark.java),
+     * comparing implementations of FastTimer with a time window of 60 seconds and
+     * - (DEV1)   a HASH_SIZE of 3 for all data structures (meters, counters, min/max, and response time buckets)
+     * - (DEV2)   a HASH_SIZE of 1 for all data structures (meters, counters, min/max, and response time buckets)
+     * - (FINAL)  a HASH_SIZE of 3 for meters, counters, min/max, and no hashing for response time buckets
+     * to the default timer implementation
+     * - (BASE-E) Codahale Timer with ExponentiallyDecayingReservoir (default as used by bookkeeper code)
+     * - (BASE-T) Codahale Timer with SlidingTimeWindowReservoir configured to 60 seconds
+     * - (BASE-S) Codahale Timer with SlidingWindowReservoir configured to hold 100,000 events.
+     *
+     * Based on results below, implementation (FINAL) was chosen as the final FastTimer implementation, as it
+     * achieves nearly the same throughput as (DEV1) at nearly the same memory footprint as (DEV2), and
+     * ultimately achieves roughly 3x higher throughput and scalability that Codahale's default implementation
+     * at around half the memory footprint.
+     *
+     * The following results have been collected on an eight core x86 server running at 3.2 GHz (updated
+     * timers are shared across 4 threads):
+     *
+     * Config   Timer Impl             Timers   Threads      ops/ms   Alloc B/op   Kb/TimerPair
+     * ----------------------------------------------------------------------------------------
+     *  DEV1    FastTimer (Hash 3)          1         4   11487.904        0                253
+     *  DEV1    FastTimer (Hash 3)         10         4   22621.702        0                253
+     *  DEV1    FastTimer (Hash 3)        100         4   21781.319        0                253
+     *  DEV2    FastTimer (Hash 1)          1         4    5138.143        0                 88
+     *  DEV2    FastTimer (Hash 1)         10         4   22902.195        0                 88
+     *  DEV2    FastTimer (Hash 1)        100         4   19173.085        0                 88
+     *  FINAL   FastTimer (Hash 3/1)        1         4    9291.002        0                 99
+     *  FINAL   FastTimer (Hash 3/1)       10         4   16379.940        0                 99
+     *  FINAL   FastTimer (Hash 3/1)      100         4   16751.020        0                 99
+     *  BASE-E  CodahaleTimer               1         4    3845.187       82.609            189
+     *  BASE-E  CodahaleTimer              10         4    7262.445       35.035            189
+     *  BASE-E  CodahaleTimer             100         4    7051.77        32.843            189
+     *  BASE-T  CodahaleTimer/TimeWindow    1         4     102.479       90.851            174
+     *  BASE-T  CodahaleTimer/TimeWindow   10         4      68.852       84.812            174
+     *  BASE-T  CodahaleTimer/TimeWindow  100         4     153.444      136.436            174
+     *  BASE-S  CodahaleTimer/SlidingWdw    1         4    4670.543        0               2103 (size=100000)
+     *  BASE-S  CodahaleTimer/SlidingWdw   10         4   13696.168        0               2103
+     *  BASE-S  CodahaleTimer/SlidingWdw  100         4   12541.936        0               2103
+     *
+     * - ops/ms is the number of timer updates per millisecond.
+     * - Alloc B/op is the number of bytes allocated per timer update
+     * - Kb/TimerPair is the heap footprint per pair of timers (one with fine-grained, one with coarse-grained buckets)
+     *
+     * The following test results include snapshot creation every 109 timer updates (typically, we would assume
+     * snapshot creation to be much less frequent), and show that also with snapshots in the mix, FastTimer outperforms
+     * Codahale default Timers both with respect to throughput and scalability as well as object allocation:
+     *
+     * Config   Timer Impl             Timers   Threads      ops/ms   Alloc B/op
+     * -------------------------------------------------------------------------
+     *  FINAL   FastTimer (Hash 3/1)        1         4    1569.953       23.707
+     *  FINAL   FastTimer (Hash 3/1)       10         4    7316.794       24.073
+     *  FINAL   FastTimer (Hash 3/1)      100         4    6498.215       24.073
+     *  BASE-E  CodahaleTimer               1         4     246.953      481.771
+     *  BASE-E  CodahaleTimer              10         4    1989.134      476.807
+     *  BASE-E  CodahaleTimer             100         4    1514.729      468.624
+     *  BASE-T  CodahaleTimer/TimeWindow    1         4       6.063    43795.810
+     *  BASE-T  CodahaleTimer/TimeWindow   10         4      44.651    33916.315
+     *  BASE-T  CodahaleTimer/TimeWindow  100         4     180.431    12330.939
+     *  BASE-S  CodahaleTimer/SlidingWdw    1         4      17.439    14683.756
+     *  BASE-S  CodahaleTimer/SlidingWdw   10         4     107.257    14683.745
+     *  BASE-S  CodahaleTimer/SlidingWdw  100         4     236.538     9767.106
+     *
+     * Unfortunately Codahale does not have a Timer interface we can implement, and some Codahale
+     * base classes are assuming instances of Timer (for example, our JettyServices instantiate a
+     * Codahale MetricsServlet, which instantiates a Codahale MetricsModule, which only serializes
+     * timers that are instances of Timer class into the json output stream). Unless we wanted to
+     * reimplement or override all these base classes, we can't just implement Codahale's Metered and Sampling
+     * interfaces. Instead we have to extend its Timer class, even though we're not using any of its
+     * inherited functionality or data structures. The inherited (unused) member variables of Codahale Timer
+     * consume slightly less than 512 byte per FastTimer (measured around 425 byte in Codahale 3.1).
+     * Above memory footprint results include ~ 1 kb of inherited (unused) data structures, which comprise
+     * around 1% of FastTimer's overall memory footprint.
+     *
+     * In terms of functionality, FastTimer provides the same functionality as Codahale's timers
+     * (in default configuration with ExponentiallyDecayingReservoirs), with the following exceptions:
+     * - Statistics are kept for a fixed amount of time (rather than exponentially decayed), by
+     *   default 60 seconds. As a consequence, getMeanRate(), getOneMinuteRate(),  getFiveMinuteRate()
+     *   and getFifteenMinuteRate() all return the same value if FastTimer is configured to use a
+     *   60 second time window.
+     * - FastTimer and FastSnapshot only record bucketized instead of discrete response times. As a
+     *   consequence, the accuracy of percentiles depends on bucket granularity. FastSnapshot also
+     *   can't return discrete values: getValues() returns an empty array, and size returns 0.
+     */
+
+    /**
+     * For improved scalability, threads are hased to meters, counters, and min/max values based on
+     * HASH_SIZE. Note that response time buckets are *not* hashed to reduce memory footprint, and we
+     * assume that concurrent updates of the same response time bucket are infrequent.
+     * The hash size could be made configurable in the future (if ever seems necessary). For now, we just
+     * hard-code it to 3 based on above performance results.
+     */
+    private static final int HASH_SIZE = 3;
+
+    /**
+     * This timer stores rate and response times on a per-second basis for a configurable amount of time
+     * (default: 60 seconds).
+     * Note that larger time windows increase the memory footprint of this timer (nearly linear).
+     */
+    private static final int TIME_WINDOW = 60;
+
+    /*
+     * Buckets for percentiles store response times according to the definition in BUCKET_SPEC in the
+     * form of { numerOfBuckets , nanosecondResolutionPerBucket }.
+     *
+     * BUCKET_SPEC_FINE:
+     * This bucket definition provides fine-grained timing for small values, and more coarse-grained timing
+     * for larger values. We expect this timer to be used primarily for I/O operations that typically
+     * range in milliseconds (or sub-milliseconds), with sporadic outliers in the single-digit second
+     * range. For values larger than 10 seconds, we only keep the maximum value, but no distribution.
+     *
+     * BUCKET_SPEC_COARSE:
+     * This bucket specification provides coarse-grained timing for events in the range of 1 - 20 seconds
+     * with 1 second granularity.
+     *
+     * If this timer is used for timing of events with significantly different value distribution,
+     * other bucket definitions may be specified.
+     *
+     * Note that a larger number of buckets increases the memory footprint of this timer nearly linear
+     * (as the number of buckets largely dominate the timer's overall memory footprint).
+     */
+    private static final long[][] BUCKET_SPEC_FINE = new long[][] {
+        { 100 ,     100000}, // 100 buckets of  0.1 ms (  0.1 -   10.0 ms)
+        {  90 ,    1000000}, //  90 buckets of    1 ms (   10 -    100 ms)
+        {  90 ,   10000000}, //  90 buckets of   10 ms (  100 -  1,000 ms)
+        {   9 , 1000000000}, //   9 buckets of 1000 ms (1,000 - 10,000 ms)
+     };                      // + 1 (default) bucket for all values > 10,000 ms
+
+    private static final long[][] BUCKET_SPEC_COARSE = new long[][] {
+        {  20 , 1000000000}, //  20 buckets of 1000 ms (1,000 - 20,000 ms)
+    };                      // + 1 (default) bucket for all values > 20,000 ms
+
+    /**
+     * Defines the response time buckets to use.
+     * - fine: BUCKET_SPEC_FINE
+     * - coarse: BUCKET_SPEC_COARSE
+     * - none: no response time buckets
+     */
+    public enum Buckets {
+        fine,
+        coarse,
+        none
+    }
+
+    // index into the second dimension of BUCKET_SPEC arrays
+    private static final int BS_NUMBUCKETS = 0;
+    private static final int BS_RESOLUTION = 1;
+
+    /*
+     * approximate space requirements for an instance of FastTimer:
+     * 4096 + (TIME_WINDOW + 2) * ((HASH_SIZE * 28) + (NUMBUCKETS * 4))
+     *
+     * For timeWindow=60 and Buckets.fine:   ~ 81 kb
+     * For timeWindow=60 and Buckets.coarse: ~ 14 kb
+     */
+
+    private final long[][] bucketSpec;
+    private final int numBuckets;
+    private final long[] bucketBounds;
+    private final int timeWindow;
+    private final int startTime;
+
+    private final AtomicLong[] counter; // indexed by [hash]
+    private final Object[] locks; // indexed by [hash]
+    private final int[] lastTime;
+    private int lastTimeBucket = 0;
+
+    private final int[][] meter;    // indexed by [hash][time]
+    private final int[][] buckets;  // indexed by [bucket][time]
+    private final long[][] min;     // indexed by [hash][time]
+    private final long[][] max;     // indexed by [hash][time]
+    private final long[][] sum;     // indexed by [hash][time]
+
+    /**
+     * A Dummy reservoir implementation.
+     * Since we have to extend Codahale's Timer class (see above), we inherit all its member
+     * objects as well. By default, Timer instantiates a ExponentiallyDecayingReservoir. Since
+     * we're not making use of it, we instead instantiate our own DummyReservoir to reduce
+     * memory footprint.
+     */
+    private static class DummyReservoir implements Reservoir {
+
+        @Override
+        public int size() {
+            return 0;
+        }
+
+        @Override
+        public void update(long value) {
+        }
+
+        @Override
+        public Snapshot getSnapshot() {
+            return null;
+        }
+
+    }
+
+    /**
+     * Constructs a new timer with default time window (60 seconds) and
+     * default time buckets (fine).
+     */
+    public FastTimer() {
+        this(TIME_WINDOW, Buckets.fine);
+    }
+
+    /**
+     * Constructs a new timer.
+     * @param timeWindowSeconds the time window (in seconds) for this timer
+     * @param buckets the type of buckets to use for response times
+     */
+    public FastTimer(int timeWindowSeconds, Buckets buckets) {
+        super(new DummyReservoir());
+        this.timeWindow = timeWindowSeconds + 2; // 2 extra seconds for housekeeping
+
+        switch (buckets) {
+        case fine:
+            bucketSpec = BUCKET_SPEC_FINE;
+            break;
+        case coarse:
+            bucketSpec = BUCKET_SPEC_COARSE;
+            break;
+        default:
+            bucketSpec = null;
+        }
+
+        // initialize buckets
+        int bucketCnt = 0;
+        for (int i = 0; bucketSpec != null && i < bucketSpec.length; i++) {
+            bucketCnt += bucketSpec[i][BS_NUMBUCKETS];
+        }
+        numBuckets = (bucketCnt > 0 ? bucketCnt + 1 : 0);
+        if (numBuckets > 0) {
+            bucketBounds = new long[bucketSpec.length];
+            long bound = 0;
+            for (int i = 0; i < bucketSpec.length; i++) {
+                bound += bucketSpec[i][BS_NUMBUCKETS] * bucketSpec[i][BS_RESOLUTION];
+                bucketBounds[i] = bound;
+            }
+        } else {
+            bucketBounds = null;
+        }
+
+        this.startTime = getTime();
+
+        counter = new AtomicLong[HASH_SIZE];
+        for (int i = 0; i < counter.length; i++) {
+            counter[i] = new AtomicLong(0);
+        }
+        meter = new int[HASH_SIZE][timeWindow];
+        if (numBuckets > 0) {
+            this.buckets = new int[numBuckets][timeWindow];
+        } else {
+            this.buckets = null;
+        }
+        sum = new long[HASH_SIZE][timeWindow];
+        min = new long[HASH_SIZE][timeWindow];
+        max = new long[HASH_SIZE][timeWindow];
+
+        lastTime = new int[HASH_SIZE];
+        locks = new Object[HASH_SIZE];
+        for (int h = 0; h < locks.length; h++) {
+            locks[h] = new Object();
+        }
+    }
+
+    /**
+     * Returns the number of response time buckets used by this timer.
+     * @return the number of response time buckets
+     */
+    public int getNumberOfBuckets() {
+        return numBuckets;
+    }
+
+    /**
+     * Figure out which percentile bucket an event of a given duration belongs into.
+     * @param duration the duration (in nanoseconds)
+     * @return the bucket
+     */
+    public int getBucket(long duration) {
+        if (numBuckets == 0) {
+            return -1;
+        }
+        int bucket = 0;
+        long lowbound = 0;
+        for (int i = 0; i < bucketSpec.length; i++) {
+            if (duration <= bucketBounds[i]) {
+                return bucket + (int) ((duration - lowbound - 1) / bucketSpec[i][BS_RESOLUTION]);
+            } else {
+                bucket += bucketSpec[i][BS_NUMBUCKETS];
+                lowbound = bucketBounds[i];
+            }
+        }
+        return numBuckets - 1;
+    }
+
+    /**
+     * Returns the upper bucket bound (inclusive) of a given bucket.
+     * @param b the bucket
+     * @return the bound (in nanoseconds)
+     */
+    public long getBucketBound(int b) {
+        if (numBuckets == 0) {
+            return -1;
+        }
+        int bucket = 0;
+        long lowbound = 0;
+        for (int i = 0; i < bucketSpec.length; i++) {
+            if (b < bucket + bucketSpec[i][BS_NUMBUCKETS]) {
+                return lowbound + ((long) ((b + 1) - bucket)) * bucketSpec[i][BS_RESOLUTION];
+            } else {
+                bucket += bucketSpec[i][BS_NUMBUCKETS];
+                lowbound = bucketBounds[i];
+            }
+        }
+        return Long.MAX_VALUE;
+    }
+
+    /**
+     * Returns the average value of a given bucket (the mean between its lower and upper bound).
+     * @param b the bucket
+     * @return the average value (in nanoseconds)
+     */
+    public long getBucketValue(int b) {
+        if (numBuckets == 0) {
+            return -1;
+        }
+        if (b == 0) {
+            return getBucketBound(0) / 2;
+        }
+        if (b == numBuckets - 1) {
+            return 2 * getBucketBound(numBuckets - 2);
+        }
+        return (getBucketBound(b - 1) + getBucketBound(b)) / 2;
+    }
+
+    /**
+     * Hashes a thread to a hash index.
+     * @return the hash index
+     */
+    private int getHash() {
+        // hashing threads to timers is cheaper than ThreadLocal timers
+        return (int) (Thread.currentThread().getId() % HASH_SIZE);
+    }
+
+    /**
+     * Returns the current absolute time (in seconds).
+     * @return the current absolute time (in seconds)
+     */
+    protected int getTime() {
+        return (int) TimeUnit.NANOSECONDS.toSeconds(System.nanoTime());
+    }
+
+    /**
+     * Returns the current second (relative to start time) and, if necessary, performs house-keeping.
+     * @param hash the hash of the calling thread
+     * @return the current time since start (in seconds)
+     */
+    private int getNow(int hash) {
+        int now = getTime() - startTime;
+
+        // check whether we need to do housekeeping
+        if (now > lastTime[hash]) {
+            synchronized (locks[hash]) {
+                // now that we have the lock, check again
+                if (now > lastTime[hash]) {
+                    int tstop = (now + 2) % timeWindow;
+
+                    // clear meter for next time period
+                    for (int t = (lastTime[hash] + 2) % timeWindow; t != tstop; t = (t + 1) % timeWindow) {
+                        meter[hash][t] = 0;
+                    }
+
+                    // clear histo for next time period
+                    for (int t = (lastTime[hash] + 2) % timeWindow; t != tstop; t = (t + 1) % timeWindow) {
+                        sum[hash][t] = 0;
+                        min[hash][t] = 0;
+                        max[hash][t] = 0;
+                    }
+
+                    lastTime[hash] = now;
+                }
+            }
+        }
+
+        // check whether we need to do bucket housekeeping
+        // (we have to do this separately since buckets aren't hashed)
+        if (numBuckets > 0 && now > lastTimeBucket) {
+            synchronized (buckets) {
+                // now that we have the lock, check again
+                if (now > lastTimeBucket) {
+                    int tstop = (now + 2) % timeWindow;
+                    for (int b = 0; b < numBuckets; b++) {
+                        synchronized (buckets[b]) {
+                            for (int t = (lastTimeBucket + 2) % timeWindow; t != tstop; t = (t + 1) % timeWindow) {
+                                buckets[b][t] = 0;
+                            }
+                        }
+                    }
+                    lastTimeBucket = now;
+                }
+            }
+        }
+
+        return now % timeWindow;
+    }
+
+    /**
+     * Returns the average per-second rate of events this timer has seen.
+     * The computed rate is calculated for past seconds (not including the current second, which is still being
+     * updated). If the specified time exceeds the time window of this timer, the only rate of the configured time
+     * window is reported.
+     * @param seconds the number of seconds over which to calculate the average rate
+     * @return the average rate (per second).
+     */
+    public double getRate(int seconds) {
+        seconds = Math.min(seconds, timeWindow - 2);
+        int t = getNow(getHash()) - 1; // start from last completed second
+        int secFrom = t - seconds;
+        long sum = 0;
+        for (int h = 0; h < HASH_SIZE; h++) {
+            for (int i = t; i > secFrom; i--) {
+                // no need to synchronize for reading (meter (int) is written atomically)
+                sum += meter[h][(timeWindow + i) % timeWindow];
+            }
+        }
+        return ((double) sum) / (double) seconds;
+    }
+
+    /**
+     * Returns the all-time count of events this timer has seen.
+     * @return the all-time count of events
+     */
+    @Override
+    public long getCount() {
+        long sum = 0;
+        for (AtomicLong c : counter) {
+            sum += c.get();
+        }
+        return sum;
+    }
+
+    @Override
+    public double getFifteenMinuteRate() {
+        return getRate(15 * 60);
+    }
+
+    @Override
+    public double getFiveMinuteRate() {
+        return getRate(5 * 60);
+    }
+
+    @Override
+    public double getMeanRate() {
+        return getRate(Integer.MAX_VALUE);
+    }
+
+    @Override
+    public double getOneMinuteRate() {
+        return getRate(60);
+    }
+
+    /**
+     * Returns a snapshot of this timer.
+     * The computed snapshot is calculated over the complete time interval supported by
+     * this timer.
+     * @return a snapshot of this timer
+     */
+    @Override
+    public Snapshot getSnapshot() {
+        long sum = 0;
+        long cnt = 0;
+        long min = 0;
+        long max = 0;
+
+        // get time and trigger housekeeping
+        int now = getNow(0) - 1; // start from last completed second
+        int secFrom = now - (timeWindow - 2);
+
+        for (int i = 1; i < HASH_SIZE; i++) {
+            getNow(i);
+        }
+
+        long[] buckets = (numBuckets > 0 ? new long[numBuckets] : null);
+        for (int i = now; i > secFrom; i--) {
+            int t = (timeWindow + i) % timeWindow;
+            for (int h = 0; h < HASH_SIZE; h++) {
+                synchronized (locks[h]) {
+                    sum += this.sum[h][t];
+                    cnt += this.meter[h][t];
+                    if ((this.min[h][t] < min && this.min[h][t] > 0) || min == 0) {
+                        min = this.min[h][t];
+                    }
+                    if (this.max[h][t] > max) {
+                        max = this.max[h][t];
+                    }
+                }
+            }
+            // no need to synchronize for reading (buckets (int) is written atomically)
+            for (int b = 0; b < numBuckets; b++) {
+                buckets[b] += this.buckets[b][t];
+            }
+        }
+
+        return new FastSnapshot(this, min, max, sum, cnt, buckets);
+    }
+
+    /**
+     * Add an event to this timer.
+     * @param duration the time duration of the event
+     * @param unit the unit of time duration
+     */
+    @Override
+    public void update(long duration, TimeUnit unit) {
+        update(unit.toNanos(duration));
+    }
+
+    /**
+     * Add an event to this timer.
+     * @param duration the time duration of the event (in nanoseconds)
+     */
+    private void update(long duration) {
+        if (duration < 1) {
+            // we can't time anything that took less than 1 ns (caller gave us wrong value)
+            duration = 1;
+        }
+        int h = getHash();
+        int t = getNow(h);
+        counter[h].incrementAndGet();
+
+        int b = getBucket(duration);
+        synchronized (locks[h]) {
+            meter[h][t]++;
+            sum[h][t] += duration;
+            if (duration < min[h][t] || min[h][t] == 0) {
+                min[h][t] = duration;
+            }
+            if (duration > max[h][t]) {
+                max[h][t] = duration;
+            }
+        }
+        if (numBuckets > 0) {
+            synchronized (buckets[b]) {
+                buckets[b][t]++;
+            }
+        }
+    }
+
+}
diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java
index 54efaee..3d69ddb 100644
--- a/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/CodahaleOpStatsTest.java
@@ -35,4 +35,14 @@ public class CodahaleOpStatsTest {
         OpStatsData statsData = logger.toOpStatsData();
         assertEquals(1, statsData.getNumSuccessfulEvents());
     }
+
+    @Test
+    public void testToFastOpStatsData() {
+        OpStatsLogger logger = new FastCodahaleMetricsProvider().getStatsLogger("test").getOpStatsLogger("testLogger");
+        logger.registerSuccessfulValue(1);
+        // the following should not throw any exception
+        OpStatsData statsData = logger.toOpStatsData();
+        assertEquals(1, statsData.getNumSuccessfulEvents());
+    }
+
 }
diff --git a/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/FastTimerTest.java b/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/FastTimerTest.java
new file mode 100644
index 0000000..34ae5c3
--- /dev/null
+++ b/bookkeeper-stats-providers/codahale-metrics-provider/src/test/java/org/apache/bookkeeper/stats/codahale/FastTimerTest.java
@@ -0,0 +1,219 @@
+/**
+ * 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 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 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));
+    }
+
+}
diff --git a/buildtools/src/main/resources/bookkeeper/findbugsExclude.xml b/buildtools/src/main/resources/bookkeeper/findbugsExclude.xml
index c5f7f07..7bed0b7 100644
--- a/buildtools/src/main/resources/bookkeeper/findbugsExclude.xml
+++ b/buildtools/src/main/resources/bookkeeper/findbugsExclude.xml
@@ -124,6 +124,9 @@
     <Class name="~org\.apache\.bookkeeper\.stats\.generated.*" />
   </Match>
   <Match>
+    <Class name="~org\.apache\.bookkeeper\.stats\.codahale\.generated.*" />
+  </Match>
+  <Match>
     <Class name="~org\.apache\.bookkeeper\.common\.generated.*" />
   </Match>
 
diff --git a/microbenchmarks/run.sh b/microbenchmarks/run.sh
index 0d59321..6d1348c 100755
--- a/microbenchmarks/run.sh
+++ b/microbenchmarks/run.sh
@@ -15,5 +15,5 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-java -Xms1G -Xmx1G -Djdk.nio.maxCachedBufferSize=0 -Djava.net.preferIPv4Stack=true -Duser.timezone=UTC -XX:-MaxFDLimit -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ResizeTLAB -XX:-ResizePLAB -XX:MetaspaceSize=128m -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80 -XX:+ParallelRefProcEnabled -XX:StackShadowPages=20 -XX:+UseCompressedOops -XX:+DisableExplicitGC -XX:StringTableSize=1000003 -XX:InitiatingHeapOccupancyPercent=40 -jar target/benchmarks.jar -prof gc -prof stack:lines=5;ti [...]
+java -Xms1G -Xmx1G -Djdk.nio.maxCachedBufferSize=0 -Djava.net.preferIPv4Stack=true -Duser.timezone=UTC -XX:-MaxFDLimit -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+ResizeTLAB -XX:-ResizePLAB -XX:MetaspaceSize=128m -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80 -XX:+ParallelRefProcEnabled -XX:StackShadowPages=20 -XX:+UseCompressedOops -XX:+DisableExplicitGC -XX:StringTableSize=1000003 -XX:InitiatingHeapOccupancyPercent=40 -jar target/benchmarks.jar $@ -prof gc -prof stack:lines=5 [...]
 
diff --git a/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/StatsLoggerBenchmark.java b/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/StatsLoggerBenchmark.java
index 9b5bdc0..0a2cd06 100644
--- a/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/StatsLoggerBenchmark.java
+++ b/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/StatsLoggerBenchmark.java
@@ -28,6 +28,7 @@ import org.apache.bookkeeper.stats.OpStatsLogger;
 import org.apache.bookkeeper.stats.StatsLogger;
 import org.apache.bookkeeper.stats.StatsProvider;
 import org.apache.bookkeeper.stats.codahale.CodahaleMetricsProvider;
+import org.apache.bookkeeper.stats.codahale.FastCodahaleMetricsProvider;
 import org.apache.bookkeeper.stats.prometheus.PrometheusMetricsProvider;
 import org.apache.bookkeeper.stats.twitter.ostrich.OstrichProvider;
 import org.apache.bookkeeper.stats.twitter.science.TwitterStatsProvider;
@@ -58,13 +59,14 @@ public class StatsLoggerBenchmark {
     static {
         providers.put("Prometheus", PrometheusMetricsProvider::new);
         providers.put("Codahale", CodahaleMetricsProvider::new);
+        providers.put("FastCodahale", FastCodahaleMetricsProvider::new);
         providers.put("Twitter", TwitterStatsProvider::new);
         providers.put("Ostrich", OstrichProvider::new);
     }
 
     @State(Scope.Benchmark)
     public static class LoggerState {
-        @Param({ "Prometheus" })//, "Codahale", "Twitter", "Ostrich" })
+        @Param({ "Prometheus", "Codahale", "FastCodahale", "Twitter", "Ostrich" })
         private String statsProvider;
 
         private Counter counter;
diff --git a/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/codahale/TimerBenchmark.java b/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/codahale/TimerBenchmark.java
new file mode 100644
index 0000000..5d1cf06
--- /dev/null
+++ b/microbenchmarks/src/main/java/org/apache/bookkeeper/stats/codahale/TimerBenchmark.java
@@ -0,0 +1,214 @@
+/**
+ * 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 java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.bookkeeper.stats.OpStatsLogger;
+import org.apache.bookkeeper.stats.StatsLogger;
+import org.apache.bookkeeper.stats.codahale.CodahaleMetricsProvider;
+import org.apache.bookkeeper.stats.codahale.FastCodahaleMetricsProvider;
+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)
+        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: java -Xmx1g -cp target/benchmarks.jar org.apache.bookkeeper.stats.codahale.TimerBenchmark <codahale|fast>
+     * @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
+            }
+        }
+    }
+
+}

-- 
To stop receiving notification emails like this one, please contact
sijie@apache.org.