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 <nmich...@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 <nmich...@salesforce.com>
    
    Reviewers: Enrico Olivelli <eolive...@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
si...@apache.org.

Reply via email to