From 74b54e1159e0f08c6442e72c5db0b1e7a6cea501 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Fri, 25 Sep 2020 10:40:23 -0400 Subject: [PATCH 01/15] Implement LockFreeExponentiallyDecayingReservoir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implementation has several advantages over the existing ExponentiallyDecayingReservoir: * It exclusively uses the precise system clock (nanotime/clock.tick) instead of a combination of nanotime and currentTimeMillis so it's not vulnerable to unexpected NTP clock jumps. * Lock free for substantially better performance under concurrent load[1] and improved performance in uncontended use[2] * Allows the rescale threshold to be configured programatically. Potential trade-offs: * Updates which occur concurrently with rescaling may be discarded if the orphaned state node is updated after rescale has replaced it. * In the worst case, all concurrent threads updating the reservoir may attempt to rescale rather than a single thread holding an exclusive write lock. It's expected that the configuration is set such that rescaling is substantially less common than updating at peak load. [1] substantially better performance under concurrent load 32 concurrent update threads ``` Benchmark (reservoirType) Mode Cnt Score Error Units ReservoirBenchmarks.updateReservoir EXPO_DECAY avgt 5 8235.861 ± 1306.404 ns/op ReservoirBenchmarks.updateReservoir LOCK_FREE_EXPO_DECAY avgt 5 758.315 ± 36.916 ns/op ``` [2] improved performance in uncontended use 1 benchmark thread ``` Benchmark (reservoirType) Mode Cnt Score Error Units ReservoirBenchmarks.updateReservoir EXPO_DECAY avgt 5 92.845 ± 36.478 ns/op ReservoirBenchmarks.updateReservoir LOCK_FREE_EXPO_DECAY avgt 5 49.168 ± 1.033 ns/op ``` --- ...ockFreeExponentiallyDecayingReservoir.java | 246 ++++++++++++++++++ .../ExponentiallyDecayingReservoirTest.java | 74 ++++-- 2 files changed, 305 insertions(+), 15 deletions(-) create mode 100644 metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java new file mode 100644 index 0000000000..edbe6929bc --- /dev/null +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -0,0 +1,246 @@ +package com.codahale.metrics; + +import com.codahale.metrics.WeightedSnapshot.WeightedSample; + +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BiConsumer; + +/** + * A lock-free exponentially-decaying random reservoir of {@code long}s. Uses Cormode et al's + * forward-decaying priority reservoir sampling method to produce a statistically representative + * sampling reservoir, exponentially biased towards newer entries. + * + * @see + * Cormode et al. Forward Decay: A Practical Time Decay Model for Streaming Systems. ICDE '09: + * Proceedings of the 2009 IEEE International Conference on Data Engineering (2009) + * + * {@link LockFreeExponentiallyDecayingReservoir} is based closely on the {@link ExponentiallyDecayingReservoir}, + * however it provides looser guarantees while completely avoiding locks. + * + * Looser guarantees: + * + * + * @author Carter Kozak + */ +public final class LockFreeExponentiallyDecayingReservoir implements Reservoir { + + private static final AtomicReferenceFieldUpdater stateUpdater = + AtomicReferenceFieldUpdater.newUpdater(LockFreeExponentiallyDecayingReservoir.class, State.class, "state"); + + private final double alpha; + private final int size; + private final long rescaleThresholdNanos; + private final Clock clock; + + private volatile State state; + + private final class State { + private final long startTick; + private final AtomicLong count; + private final ConcurrentSkipListMap values; + + State(long startTick, AtomicLong count, ConcurrentSkipListMap values) { + this.startTick = startTick; + this.values = values; + this.count = count; + } + + private void update(long value, long timestampNanos) { + double itemWeight = weight(timestampNanos - startTick); + WeightedSample sample = new WeightedSample(value, itemWeight); + double priority = itemWeight / ThreadLocalRandom.current().nextDouble(); + + long newCount = count.incrementAndGet(); + if (newCount <= size || values.isEmpty()) { + values.put(priority, sample); + } else { + Double first = values.firstKey(); + if (first < priority && values.putIfAbsent(priority, sample) == null) { + // Always remove an item + while (values.remove(first) == null) { + first = values.firstKey(); + } + } + } + } + + /* "A common feature of the above techniques—indeed, the key technique that + * allows us to track the decayed weights efficiently—is that they maintain + * counts and other quantities based on g(ti − L), and only scale by g(t − L) + * at query time. But while g(ti −L)/g(t−L) is guaranteed to lie between zero + * and one, the intermediate values of g(ti − L) could become very large. For + * polynomial functions, these values should not grow too large, and should be + * effectively represented in practice by floating point values without loss of + * precision. For exponential functions, these values could grow quite large as + * new values of (ti − L) become large, and potentially exceed the capacity of + * common floating point types. However, since the values stored by the + * algorithms are linear combinations of g values (scaled sums), they can be + * rescaled relative to a new landmark. That is, by the analysis of exponential + * decay in Section III-A, the choice of L does not affect the final result. We + * can therefore multiply each value based on L by a factor of exp(−α(L′ − L)), + * and obtain the correct value as if we had instead computed relative to a new + * landmark L′ (and then use this new L′ at query time). This can be done with + * a linear pass over whatever data structure is being used." + */ + State rescale(long newTick) { + long durationNanos = newTick - startTick; + double durationSeconds = durationNanos / 1_000_000_000D; + double scalingFactor = Math.exp(-alpha * durationSeconds); + final AtomicLong newCount; + ConcurrentSkipListMap newValues = new ConcurrentSkipListMap<>(); + if (Double.compare(scalingFactor, 0) != 0) { + RescalingConsumer consumer = new RescalingConsumer(scalingFactor, newValues); + values.forEach(consumer); + // make sure the counter is in sync with the number of stored samples. + newCount = new AtomicLong(consumer.count); + } else { + newCount = new AtomicLong(); + } + return new State(newTick, newCount, newValues); + } + } + + private static final class RescalingConsumer implements BiConsumer { + private final double scalingFactor; + private final ConcurrentSkipListMap values; + private long count; + + RescalingConsumer(double scalingFactor, ConcurrentSkipListMap values) { + this.scalingFactor = scalingFactor; + this.values = values; + } + + @Override + public void accept(Double priority, WeightedSample sample) { + double newWeight = sample.weight * scalingFactor; + if (Double.compare(newWeight, 0) == 0) { + return; + } + WeightedSample newSample = new WeightedSample(sample.value, newWeight); + if (values.put(priority * scalingFactor, newSample) == null) { + count++; + } + } + } + + private LockFreeExponentiallyDecayingReservoir(int size, double alpha, Duration rescaleThreshold, Clock clock) { + this.alpha = alpha; + this.size = size; + this.clock = clock; + this.rescaleThresholdNanos = rescaleThreshold.toNanos(); + this.state = new State(clock.getTick(), new AtomicLong(), new ConcurrentSkipListMap<>()); + } + + @Override + public int size() { + return (int) Math.min(size, state.count.get()); + } + + @Override + public void update(long value) { + long now = clock.getTick(); + rescaleIfNeeded(now).update(value, now); + } + + private State rescaleIfNeeded(long currentTick) { + State stateSnapshot = this.state; + long lastScaleTick = stateSnapshot.startTick; + if (currentTick - lastScaleTick >= rescaleThresholdNanos) { + State newState = stateSnapshot.rescale(currentTick); + if (stateUpdater.compareAndSet(this, stateSnapshot, newState)) { + // newState successfully installed + return newState; + } + // Otherwise another thread has won the race and we can return the result of a volatile read. + // It's possible this has taken so long that another update is required, however that's unlikely + // and no worse than the standard race between a rescale and update. + return this.state; + } + return stateSnapshot; + } + + @Override + public Snapshot getSnapshot() { + State stateSnapshot = rescaleIfNeeded(clock.getTick()); + return new WeightedSnapshot(stateSnapshot.values.values()); + } + + private double weight(long durationNanos) { + double durationSeconds = durationNanos / 1_000_000_000D; + return Math.exp(alpha * durationSeconds); + } + + public static Builder builder() { + return new Builder(); + } + + /** + * By default this uses a size of 1028 elements, which offers a 99.9% + * confidence level with a 5% margin of error assuming a normal distribution, and an alpha + * factor of 0.015, which heavily biases the reservoir to the past 5 minutes of measurements. + */ + public static final class Builder { + private static final int DEFAULT_SIZE = 1028; + private static final double DEFAULT_ALPHA = 0.015D; + private static final Duration DEFAULT_RESCALE_THRESHOLD = Duration.ofHours(1); + + private int size = DEFAULT_SIZE; + private double alpha = DEFAULT_ALPHA; + private Duration rescaleThreshold = DEFAULT_RESCALE_THRESHOLD; + private Clock clock = Clock.defaultClock(); + + private Builder() {} + + /** + * Maximum number of samples to keep in the reservoir. Once this number is reached older samples are + * replaced (based on weight, with some amount of random jitter). + */ + public Builder size(int value) { + this.size = value; + return this; + } + + /** + * Alpha is the exponential decay factor. Higher values bias results more heavily toward newer values. + */ + public Builder alpha(double value) { + this.alpha = value; + return this; + } + + /** + * Interval at which this reservoir is rescaled. + */ + public Builder rescaleThreshold(Duration value) { + this.rescaleThreshold = Objects.requireNonNull(value, "rescaleThreshold is required"); + return this; + } + + /** + * Clock instance used for decay. + */ + public Builder clock(Clock value) { + this.clock = Objects.requireNonNull(value, "clock is required"); + return this; + } + + public Reservoir build() { + return new LockFreeExponentiallyDecayingReservoir(size, alpha, rescaleThreshold, clock); + } + } +} diff --git a/metrics-core/src/test/java/com/codahale/metrics/ExponentiallyDecayingReservoirTest.java b/metrics-core/src/test/java/com/codahale/metrics/ExponentiallyDecayingReservoirTest.java index feccc847b8..870863798d 100644 --- a/metrics-core/src/test/java/com/codahale/metrics/ExponentiallyDecayingReservoirTest.java +++ b/metrics-core/src/test/java/com/codahale/metrics/ExponentiallyDecayingReservoirTest.java @@ -2,17 +2,63 @@ import com.codahale.metrics.Timer.Context; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import java.util.Arrays; +import java.util.Collection; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +@RunWith(Parameterized.class) public class ExponentiallyDecayingReservoirTest { + + public enum ReservoirFactory { + EXPONENTIALLY_DECAYING() { + @Override + Reservoir create(int size, double alpha, Clock clock) { + return new ExponentiallyDecayingReservoir(size, alpha, clock); + } + }, + + LOCK_FREE_EXPONENTIALLY_DECAYING() { + @Override + Reservoir create(int size, double alpha, Clock clock) { + return LockFreeExponentiallyDecayingReservoir.builder() + .size(size) + .alpha(alpha) + .clock(clock) + .build(); + } + }; + + abstract Reservoir create(int size, double alpha, Clock clock); + + Reservoir create(int size, double alpha) { + return create(size, alpha, Clock.defaultClock()); + } + } + + @Parameterized.Parameters(name = "{index}: {0}") + public static Collection reservoirs() { + return Arrays.stream(ReservoirFactory.values()) + .map(value -> new Object[] {value}) + .collect(Collectors.toList()); + } + + private final ReservoirFactory reservoirFactory; + + public ExponentiallyDecayingReservoirTest(ReservoirFactory reservoirFactory) { + this.reservoirFactory = reservoirFactory; + } + @Test public void aReservoirOf100OutOf1000Elements() { - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(100, 0.99); + final Reservoir reservoir = reservoirFactory.create(100, 0.99); for (int i = 0; i < 1000; i++) { reservoir.update(i); } @@ -30,7 +76,7 @@ public void aReservoirOf100OutOf1000Elements() { @Test public void aReservoirOf100OutOf10Elements() { - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(100, 0.99); + final Reservoir reservoir = reservoirFactory.create(100, 0.99); for (int i = 0; i < 10; i++) { reservoir.update(i); } @@ -48,7 +94,7 @@ public void aReservoirOf100OutOf10Elements() { @Test public void aHeavilyBiasedReservoirOf100OutOf1000Elements() { - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000, 0.01); + final Reservoir reservoir = reservoirFactory.create(1000, 0.01); for (int i = 0; i < 100; i++) { reservoir.update(i); } @@ -68,7 +114,7 @@ public void aHeavilyBiasedReservoirOf100OutOf1000Elements() { @Test public void longPeriodsOfInactivityShouldNotCorruptSamplingState() { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(10, 0.15, clock); + final Reservoir reservoir = reservoirFactory.create(10, 0.15, clock); // add 1000 values at a rate of 10 values/second for (int i = 0; i < 1000; i++) { @@ -102,7 +148,7 @@ public void longPeriodsOfInactivityShouldNotCorruptSamplingState() { @Test public void longPeriodsOfInactivity_fetchShouldResample() { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(10, + final Reservoir reservoir = reservoirFactory.create(10, 0.015, clock); @@ -128,7 +174,7 @@ public void longPeriodsOfInactivity_fetchShouldResample() { @Test public void emptyReservoirSnapshot_shouldReturnZeroForAllValues() { - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(100, 0.015, + final Reservoir reservoir = reservoirFactory.create(100, 0.015, new ManualClock()); Snapshot snapshot = reservoir.getSnapshot(); @@ -141,7 +187,7 @@ public void emptyReservoirSnapshot_shouldReturnZeroForAllValues() { @Test public void removeZeroWeightsInSamplesToPreventNaNInMeanValues() { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1028, 0.015, clock); + final Reservoir reservoir = reservoirFactory.create(1028, 0.015, clock); Timer timer = new Timer(reservoir, clock); Context context = timer.time(); @@ -168,7 +214,7 @@ public void multipleUpdatesAfterlongPeriodsOfInactivityShouldNotCorruptSamplingS // Run the test several times. for (int attempt = 0; attempt < 10; attempt++) { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(10, + final Reservoir reservoir = reservoirFactory.create(10, 0.015, clock); @@ -253,7 +299,7 @@ public void multipleUpdatesAfterlongPeriodsOfInactivityShouldNotCorruptSamplingS @Test public void spotLift() { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000, + final Reservoir reservoir = reservoirFactory.create(1000, 0.015, clock); @@ -279,7 +325,7 @@ public void spotLift() { @Test public void spotFall() { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000, + final Reservoir reservoir = reservoirFactory.create(1000, 0.015, clock); @@ -305,7 +351,7 @@ public void spotFall() { @Test public void quantiliesShouldBeBasedOnWeights() { final ManualClock clock = new ManualClock(); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(1000, + final Reservoir reservoir = reservoirFactory.create(1000, 0.015, clock); for (int i = 0; i < 40; i++) { @@ -340,9 +386,7 @@ public void clockWrapShouldNotRescale() { private void testShortPeriodShouldNotRescale(long startTimeNanos) { final ManualClock clock = new ManualClock(startTimeNanos); - final ExponentiallyDecayingReservoir reservoir = new ExponentiallyDecayingReservoir(10, - 1, - clock); + final Reservoir reservoir = reservoirFactory.create(10, 1, clock); reservoir.update(1000); assertThat(reservoir.getSnapshot().size()).isEqualTo(1); @@ -360,7 +404,7 @@ private void testShortPeriodShouldNotRescale(long startTimeNanos) { assertThat(snapshot.size()).isEqualTo(1); } - private static void assertAllValuesBetween(ExponentiallyDecayingReservoir reservoir, + private static void assertAllValuesBetween(Reservoir reservoir, double min, double max) { for (double i : reservoir.getSnapshot().getValues()) { From 42ec9769d4e18a93372a76e7c2184ecfb395dd3e Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 5 Oct 2020 10:29:15 -0400 Subject: [PATCH 02/15] Precompute alpha at nanosecond scale Avoid expensive division operations on each rescale and update --- .../LockFreeExponentiallyDecayingReservoir.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index edbe6929bc..b0b5487192 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -39,10 +39,11 @@ */ public final class LockFreeExponentiallyDecayingReservoir implements Reservoir { + private static final double SECONDS_PER_NANO = .000000001D; private static final AtomicReferenceFieldUpdater stateUpdater = AtomicReferenceFieldUpdater.newUpdater(LockFreeExponentiallyDecayingReservoir.class, State.class, "state"); - private final double alpha; + private final double alphaNanos; private final int size; private final long rescaleThresholdNanos; private final Clock clock; @@ -99,8 +100,7 @@ private void update(long value, long timestampNanos) { */ State rescale(long newTick) { long durationNanos = newTick - startTick; - double durationSeconds = durationNanos / 1_000_000_000D; - double scalingFactor = Math.exp(-alpha * durationSeconds); + double scalingFactor = Math.exp(-alphaNanos * durationNanos); final AtomicLong newCount; ConcurrentSkipListMap newValues = new ConcurrentSkipListMap<>(); if (Double.compare(scalingFactor, 0) != 0) { @@ -139,7 +139,11 @@ public void accept(Double priority, WeightedSample sample) { } private LockFreeExponentiallyDecayingReservoir(int size, double alpha, Duration rescaleThreshold, Clock clock) { - this.alpha = alpha; + // Scale alpha to nanoseconds + this.alphaNanos = alpha * SECONDS_PER_NANO; + if (Double.compare(alphaNanos, 0) == 0) { + throw new IllegalArgumentException("Alpha value " + alpha + " is to small to be scaled to nanoseconds"); + } this.size = size; this.clock = clock; this.rescaleThresholdNanos = rescaleThreshold.toNanos(); @@ -181,8 +185,7 @@ public Snapshot getSnapshot() { } private double weight(long durationNanos) { - double durationSeconds = durationNanos / 1_000_000_000D; - return Math.exp(alpha * durationSeconds); + return Math.exp(alphaNanos * durationNanos); } public static Builder builder() { From 645b01d55eb103ee3f3f7417cf2cfe12e8db2af8 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 5 Oct 2020 10:38:15 -0400 Subject: [PATCH 03/15] Optimize weightedsample allocations on update --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index b0b5487192..78e82a1ed4 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -63,15 +63,17 @@ private final class State { private void update(long value, long timestampNanos) { double itemWeight = weight(timestampNanos - startTick); - WeightedSample sample = new WeightedSample(value, itemWeight); double priority = itemWeight / ThreadLocalRandom.current().nextDouble(); long newCount = count.incrementAndGet(); if (newCount <= size || values.isEmpty()) { - values.put(priority, sample); + values.put(priority, new WeightedSample(value, itemWeight)); } else { Double first = values.firstKey(); - if (first < priority && values.putIfAbsent(priority, sample) == null) { + if (first < priority + // Optimization: Avoid WeightedSample allocation in the hot path when priority is lower than + // the existing minimum. + && values.putIfAbsent(priority, new WeightedSample(value, itemWeight)) == null) { // Always remove an item while (values.remove(first) == null) { first = values.firstKey(); From 7a760479a24331255e5b5c4b90d621048b5ba1bf Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 5 Oct 2020 14:25:23 -0400 Subject: [PATCH 04/15] Refactor `update` to avoid isEmpty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` Benchmark (reservoirType) Mode Cnt Score Error Units ReservoirBenchmarks.updateReservoir EXPO_DECAY avgt 5 8365.308 ± 1880.683 ns/op ReservoirBenchmarks.updateReservoir LOCK_FREE_EXPO_DECAY avgt 5 73.966 ± 5.305 ns/op ``` --- ...ockFreeExponentiallyDecayingReservoir.java | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 78e82a1ed4..38adefb162 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -52,6 +52,7 @@ public final class LockFreeExponentiallyDecayingReservoir implements Reservoir { private final class State { private final long startTick; + // Count is updated after samples are successfully added to the map. private final AtomicLong count; private final ConcurrentSkipListMap values; @@ -64,21 +65,18 @@ private final class State { private void update(long value, long timestampNanos) { double itemWeight = weight(timestampNanos - startTick); double priority = itemWeight / ThreadLocalRandom.current().nextDouble(); + long currentCount = count.get(); + if (currentCount < size) { + addSample(priority, value, itemWeight); + } else if (values.firstKey() < priority) { + addSample(priority, value, itemWeight); + } + } - long newCount = count.incrementAndGet(); - if (newCount <= size || values.isEmpty()) { - values.put(priority, new WeightedSample(value, itemWeight)); - } else { - Double first = values.firstKey(); - if (first < priority - // Optimization: Avoid WeightedSample allocation in the hot path when priority is lower than - // the existing minimum. - && values.putIfAbsent(priority, new WeightedSample(value, itemWeight)) == null) { - // Always remove an item - while (values.remove(first) == null) { - first = values.firstKey(); - } - } + private void addSample(double priority, long value, double itemWeight) { + if (values.putIfAbsent(priority, new WeightedSample(value, itemWeight)) == null + && count.incrementAndGet() > size) { + values.pollFirstEntry(); } } From d34c67b385f11a258b14a8a93ff4de6eef16426d Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 5 Oct 2020 14:52:26 -0400 Subject: [PATCH 05/15] Code review from @spkrka --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 38adefb162..f426272162 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -66,9 +66,7 @@ private void update(long value, long timestampNanos) { double itemWeight = weight(timestampNanos - startTick); double priority = itemWeight / ThreadLocalRandom.current().nextDouble(); long currentCount = count.get(); - if (currentCount < size) { - addSample(priority, value, itemWeight); - } else if (values.firstKey() < priority) { + if (currentCount < size || values.firstKey() < priority) { addSample(priority, value, itemWeight); } } From 3cc803237e4a2b65e8a8b4de59e12d3ba5f17641 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 5 Oct 2020 17:55:11 -0400 Subject: [PATCH 06/15] remove unnecessary validation --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index f426272162..6fe1b90cd4 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -139,9 +139,6 @@ public void accept(Double priority, WeightedSample sample) { private LockFreeExponentiallyDecayingReservoir(int size, double alpha, Duration rescaleThreshold, Clock clock) { // Scale alpha to nanoseconds this.alphaNanos = alpha * SECONDS_PER_NANO; - if (Double.compare(alphaNanos, 0) == 0) { - throw new IllegalArgumentException("Alpha value " + alpha + " is to small to be scaled to nanoseconds"); - } this.size = size; this.clock = clock; this.rescaleThresholdNanos = rescaleThreshold.toNanos(); From 915ba6723d9a41d88f7e8da0a7d550401b09112f Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Tue, 6 Oct 2020 10:06:29 -0400 Subject: [PATCH 07/15] readable decimals --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 6fe1b90cd4..6ea6bcea86 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -39,7 +39,7 @@ */ public final class LockFreeExponentiallyDecayingReservoir implements Reservoir { - private static final double SECONDS_PER_NANO = .000000001D; + private static final double SECONDS_PER_NANO = .000_000_001D; private static final AtomicReferenceFieldUpdater stateUpdater = AtomicReferenceFieldUpdater.newUpdater(LockFreeExponentiallyDecayingReservoir.class, State.class, "state"); From 9518a8bc4cbe31929a0ef5d836e85307a7e7a590 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Tue, 6 Oct 2020 12:55:11 -0400 Subject: [PATCH 08/15] require positive size --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 6ea6bcea86..5f3312d04b 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -209,6 +209,10 @@ private Builder() {} * replaced (based on weight, with some amount of random jitter). */ public Builder size(int value) { + if (value <= 0) { + throw new IllegalArgumentException( + "LockFreeExponentiallyDecayingReservoir size must be positive: " + value); + } this.size = value; return this; } From 10f4c02ed82d59db3e414e4d5f0be4fb6ef65ef6 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Tue, 6 Oct 2020 13:05:12 -0400 Subject: [PATCH 09/15] optimize rescaleIfNeeded below the 35b inline threshold This makes the check more likely to be optimized without forcing analysis of the less common doRescale path. Before: ``` private LockFreeExponentiallyDecayingReservoir$State rescaleIfNeeded(long) 0: aload_0 1: getfield #56 // Field state:Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State; 4: astore_3 5: aload_3 6: invokestatic #81 // Method com/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State.access$600:(Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State;)J 9: lstore 4 11: lload_1 12: lload 4 14: lsub 15: aload_0 16: getfield #36 // Field rescaleThresholdNanos:J 19: lcmp 20: iflt 51 23: aload_3 24: lload_1 25: invokevirtual #85 // Method com/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State.rescale:(J)Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State; 28: astore 6 30: getstatic #88 // Field stateUpdater:Ljava/util/concurrent/atomic/AtomicReferenceFieldUpdater; 33: aload_0 34: aload_3 35: aload 6 37: invokevirtual #92 // Method java/util/concurrent/atomic/AtomicReferenceFieldUpdater.compareAndSet:(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z 40: ifeq 46 43: aload 6 45: areturn 46: aload_0 47: getfield #56 // Field state:Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State; 50: areturn 51: aload_3 52: areturn ``` After: ``` private LockFreeExponentiallyDecayingReservoir$State rescaleIfNeeded(long) 0: aload_0 1: getfield #56 // Field state:Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State; 4: astore_3 5: lload_1 6: aload_3 7: invokestatic #81 // Method com/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State.access$600:(Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State;)J 10: lsub 11: aload_0 12: getfield #36 // Field rescaleThresholdNanos:J 15: lcmp 16: iflt 26 19: aload_0 20: lload_1 21: aload_3 22: invokespecial #85 // Method doRescale:(JLcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State;)Lcom/codahale/metrics/LockFreeExponentiallyDecayingReservoir$State; 25: areturn 26: aload_3 27: areturn ``` --- ...ockFreeExponentiallyDecayingReservoir.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 5f3312d04b..59cc8890d5 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -157,22 +157,27 @@ public void update(long value) { } private State rescaleIfNeeded(long currentTick) { + // This method is optimized for size so the check may be quickly inlined. + // Rescaling occurs substantially less frequently than the check itself. State stateSnapshot = this.state; - long lastScaleTick = stateSnapshot.startTick; - if (currentTick - lastScaleTick >= rescaleThresholdNanos) { - State newState = stateSnapshot.rescale(currentTick); - if (stateUpdater.compareAndSet(this, stateSnapshot, newState)) { - // newState successfully installed - return newState; - } - // Otherwise another thread has won the race and we can return the result of a volatile read. - // It's possible this has taken so long that another update is required, however that's unlikely - // and no worse than the standard race between a rescale and update. - return this.state; + if (currentTick - stateSnapshot.startTick >= rescaleThresholdNanos) { + return doRescale(currentTick, stateSnapshot); } return stateSnapshot; } + private State doRescale(long currentTick, State stateSnapshot) { + State newState = stateSnapshot.rescale(currentTick); + if (stateUpdater.compareAndSet(this, stateSnapshot, newState)) { + // newState successfully installed + return newState; + } + // Otherwise another thread has won the race and we can return the result of a volatile read. + // It's possible this has taken so long that another update is required, however that's unlikely + // and no worse than the standard race between a rescale and update. + return this.state; + } + @Override public Snapshot getSnapshot() { State stateSnapshot = rescaleIfNeeded(clock.getTick()); From 0aa78bd13bfc482db8cf8c9247e5886f7abfe4f2 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 5 Oct 2020 15:52:26 -0400 Subject: [PATCH 10/15] Avoid incrementing count when the value has reached size. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` Benchmark (reservoirType) Mode Cnt Score Error Units ReservoirBenchmarks.updateReservoir EXPO_DECAY avgt 5 8309.300 ± 1900.398 ns/op ReservoirBenchmarks.updateReservoir LOCK_FREE_EXPO_DECAY avgt 5 70.028 ± 0.887 ns/op ``` --- ...ockFreeExponentiallyDecayingReservoir.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 59cc8890d5..b3ffbd7d6d 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -6,7 +6,7 @@ import java.util.Objects; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.BiConsumer; @@ -43,20 +43,32 @@ public final class LockFreeExponentiallyDecayingReservoir implements Reservoir { private static final AtomicReferenceFieldUpdater stateUpdater = AtomicReferenceFieldUpdater.newUpdater(LockFreeExponentiallyDecayingReservoir.class, State.class, "state"); - private final double alphaNanos; private final int size; private final long rescaleThresholdNanos; private final Clock clock; private volatile State state; - private final class State { + private static final class State { + + private static final AtomicIntegerFieldUpdater countUpdater = AtomicIntegerFieldUpdater.newUpdater(State.class, "count"); + + private final double alphaNanos; + private final int size; private final long startTick; // Count is updated after samples are successfully added to the map. - private final AtomicLong count; private final ConcurrentSkipListMap values; - State(long startTick, AtomicLong count, ConcurrentSkipListMap values) { + private volatile int count; + + State( + double alphaNanos, + int size, + long startTick, + int count, + ConcurrentSkipListMap values) { + this.alphaNanos = alphaNanos; + this.size = size; this.startTick = startTick; this.values = values; this.count = count; @@ -65,15 +77,15 @@ private final class State { private void update(long value, long timestampNanos) { double itemWeight = weight(timestampNanos - startTick); double priority = itemWeight / ThreadLocalRandom.current().nextDouble(); - long currentCount = count.get(); - if (currentCount < size || values.firstKey() < priority) { - addSample(priority, value, itemWeight); + boolean mapIsFull = count >= size; + if (!mapIsFull || values.firstKey() < priority) { + addSample(priority, value, itemWeight, mapIsFull); } } - private void addSample(double priority, long value, double itemWeight) { + private void addSample(double priority, long value, double itemWeight, boolean bypassIncrement) { if (values.putIfAbsent(priority, new WeightedSample(value, itemWeight)) == null - && count.incrementAndGet() > size) { + && (bypassIncrement || countUpdater.incrementAndGet(this) > size)) { values.pollFirstEntry(); } } @@ -99,24 +111,28 @@ private void addSample(double priority, long value, double itemWeight) { State rescale(long newTick) { long durationNanos = newTick - startTick; double scalingFactor = Math.exp(-alphaNanos * durationNanos); - final AtomicLong newCount; + final int newCount; ConcurrentSkipListMap newValues = new ConcurrentSkipListMap<>(); if (Double.compare(scalingFactor, 0) != 0) { RescalingConsumer consumer = new RescalingConsumer(scalingFactor, newValues); values.forEach(consumer); // make sure the counter is in sync with the number of stored samples. - newCount = new AtomicLong(consumer.count); + newCount = consumer.count; } else { - newCount = new AtomicLong(); + newCount = 0; } - return new State(newTick, newCount, newValues); + return new State(alphaNanos, size, newTick, newCount, newValues); + } + + private double weight(long durationNanos) { + return Math.exp(alphaNanos * durationNanos); } } private static final class RescalingConsumer implements BiConsumer { private final double scalingFactor; private final ConcurrentSkipListMap values; - private long count; + private int count; RescalingConsumer(double scalingFactor, ConcurrentSkipListMap values) { this.scalingFactor = scalingFactor; @@ -138,16 +154,16 @@ public void accept(Double priority, WeightedSample sample) { private LockFreeExponentiallyDecayingReservoir(int size, double alpha, Duration rescaleThreshold, Clock clock) { // Scale alpha to nanoseconds - this.alphaNanos = alpha * SECONDS_PER_NANO; + double alphaNanos = alpha * SECONDS_PER_NANO; this.size = size; this.clock = clock; this.rescaleThresholdNanos = rescaleThreshold.toNanos(); - this.state = new State(clock.getTick(), new AtomicLong(), new ConcurrentSkipListMap<>()); + this.state = new State(alphaNanos, size, clock.getTick(), 0, new ConcurrentSkipListMap<>()); } @Override public int size() { - return (int) Math.min(size, state.count.get()); + return Math.min(size, state.count); } @Override @@ -184,10 +200,6 @@ public Snapshot getSnapshot() { return new WeightedSnapshot(stateSnapshot.values.values()); } - private double weight(long durationNanos) { - return Math.exp(alphaNanos * durationNanos); - } - public static Builder builder() { return new Builder(); } From 4036d08327e66ee59d59a944b78b3cdddc553bb9 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Wed, 7 Oct 2020 10:02:45 -0400 Subject: [PATCH 11/15] rescale cannot exceed size elements --- .../LockFreeExponentiallyDecayingReservoir.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index b3ffbd7d6d..0803716a61 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -111,15 +111,19 @@ private void addSample(double priority, long value, double itemWeight, boolean b State rescale(long newTick) { long durationNanos = newTick - startTick; double scalingFactor = Math.exp(-alphaNanos * durationNanos); - final int newCount; + int newCount = 0; ConcurrentSkipListMap newValues = new ConcurrentSkipListMap<>(); if (Double.compare(scalingFactor, 0) != 0) { RescalingConsumer consumer = new RescalingConsumer(scalingFactor, newValues); values.forEach(consumer); // make sure the counter is in sync with the number of stored samples. newCount = consumer.count; - } else { - newCount = 0; + } + // It's possible that more values were added while the map was scanned, those with the + // minimum priorities are removed. + while (newCount > size) { + newValues.pollFirstEntry(); + newCount--; } return new State(alphaNanos, size, newTick, newCount, newValues); } From ded3c38ca6936b32fd8800184189d152b4161cd8 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Wed, 7 Oct 2020 10:06:35 -0400 Subject: [PATCH 12/15] line length --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 0803716a61..2f44d688c7 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -51,7 +51,8 @@ public final class LockFreeExponentiallyDecayingReservoir implements Reservoir { private static final class State { - private static final AtomicIntegerFieldUpdater countUpdater = AtomicIntegerFieldUpdater.newUpdater(State.class, "count"); + private static final AtomicIntegerFieldUpdater countUpdater = + AtomicIntegerFieldUpdater.newUpdater(State.class, "count"); private final double alphaNanos; private final int size; From f6626f2a42f564fb96de0577786c951a1052edbc Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Wed, 7 Oct 2020 11:20:54 -0400 Subject: [PATCH 13/15] requireNonNull validation --- .../metrics/LockFreeExponentiallyDecayingReservoir.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java index 2f44d688c7..cf258f046c 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java +++ b/metrics-core/src/main/java/com/codahale/metrics/LockFreeExponentiallyDecayingReservoir.java @@ -123,7 +123,7 @@ State rescale(long newTick) { // It's possible that more values were added while the map was scanned, those with the // minimum priorities are removed. while (newCount > size) { - newValues.pollFirstEntry(); + Objects.requireNonNull(newValues.pollFirstEntry(), "Expected an entry"); newCount--; } return new State(alphaNanos, size, newTick, newCount, newValues); From 515e98e2a432f2ef2ac29ca8805eed7e243c3b2f Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Fri, 9 Oct 2020 10:19:48 -0400 Subject: [PATCH 14/15] Include LockFreeExponentiallyDecayingReservoir in ReservoirBenchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Results with 32 concurrent threads on a 14c/28t Xeon W-2175 ``` Benchmark Mode Cnt Score Error Units ReservoirBenchmark.perfExponentiallyDecayingReservoir avgt 4 8.817 ± 0.310 us/op ReservoirBenchmark.perfLockFreeExponentiallyDecayingReservoir avgt 4 0.076 ± 0.002 us/op ReservoirBenchmark.perfSlidingTimeWindowArrayReservoir avgt 4 14.890 ± 0.489 us/op ReservoirBenchmark.perfSlidingTimeWindowReservoir avgt 4 39.066 ± 27.583 us/op ReservoirBenchmark.perfSlidingWindowReservoir avgt 4 4.257 ± 0.187 us/op ReservoirBenchmark.perfUniformReservoir avgt 4 0.704 ± 0.040 us/op ``` --- .../codahale/metrics/benchmarks/ReservoirBenchmark.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java b/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java index 5d2e7885ac..dc5390fb30 100644 --- a/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java +++ b/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java @@ -1,6 +1,8 @@ package com.codahale.metrics.benchmarks; import com.codahale.metrics.ExponentiallyDecayingReservoir; +import com.codahale.metrics.LockFreeExponentiallyDecayingReservoir; +import com.codahale.metrics.Reservoir; import com.codahale.metrics.SlidingTimeWindowArrayReservoir; import com.codahale.metrics.SlidingTimeWindowReservoir; import com.codahale.metrics.SlidingWindowReservoir; @@ -23,6 +25,7 @@ public class ReservoirBenchmark { private final UniformReservoir uniform = new UniformReservoir(); private final ExponentiallyDecayingReservoir exponential = new ExponentiallyDecayingReservoir(); + private final Reservoir lockFreeExponential = LockFreeExponentiallyDecayingReservoir.builder().build(); private final SlidingWindowReservoir sliding = new SlidingWindowReservoir(1000); private final SlidingTimeWindowReservoir slidingTime = new SlidingTimeWindowReservoir(200, TimeUnit.MILLISECONDS); private final SlidingTimeWindowArrayReservoir arrTime = new SlidingTimeWindowArrayReservoir(200, TimeUnit.MILLISECONDS); @@ -60,6 +63,12 @@ public Object perfSlidingTimeWindowReservoir() { return slidingTime; } + @Benchmark + public Object perfLockFreeExponentiallyDecayingReservoir() { + lockFreeExponential.update(nextValue); + return slidingTime; + } + public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(".*" + ReservoirBenchmark.class.getSimpleName() + ".*") From 550112a82c70549cb8b4acffffad8eeff8b8e812 Mon Sep 17 00:00:00 2001 From: Carter Kozak Date: Mon, 12 Oct 2020 08:53:51 -0400 Subject: [PATCH 15/15] Update metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java --- .../com/codahale/metrics/benchmarks/ReservoirBenchmark.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java b/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java index dc5390fb30..b7c6801d2f 100644 --- a/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java +++ b/metrics-benchmarks/src/main/java/com/codahale/metrics/benchmarks/ReservoirBenchmark.java @@ -66,7 +66,7 @@ public Object perfSlidingTimeWindowReservoir() { @Benchmark public Object perfLockFreeExponentiallyDecayingReservoir() { lockFreeExponential.update(nextValue); - return slidingTime; + return lockFreeExponential; } public static void main(String[] args) throws RunnerException {