From 84e7464c8c699022b09bb37cafd326576d3dcc7f Mon Sep 17 00:00:00 2001 From: Ariel Weisberg Date: Tue, 30 Jan 2024 14:43:35 -0500 Subject: [PATCH] Fix slowTickIfNecessary with infrequently used EWMA EWMA.tickIfNecessary does an amount of work that is linear to the amount of time that has passed since the last time the EWMA was ticked. For infrequently used EWMA this can lead to pauses observed in the 700-800 millisecond range after a few hundred days. It's not really necessary to perform every tick as all that is doing is slowly approaching the smallest representable positive number in a double. Instead pick a number close to zero and if the number of ticks required is greater then that don't do the ticks just set the value to close to zero immediately. Actually approaching the smallest representable number is still measurably slow and not particularly useful. To avoid changing the observed output of the EWMA (which previous was only 0.0 if never used) set it close to Double.MIN_NORMAL rather then to 0.0 --- .../main/java/com/codahale/metrics/EWMA.java | 8 ++++ .../metrics/ExponentialMovingAverages.java | 37 +++++++++++++++++-- .../ExponentialMovingAveragesTest.java | 26 +++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 metrics-core/src/test/java/com/codahale/metrics/ExponentialMovingAveragesTest.java diff --git a/metrics-core/src/main/java/com/codahale/metrics/EWMA.java b/metrics-core/src/main/java/com/codahale/metrics/EWMA.java index 2d2e658093..641bb728c1 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/EWMA.java +++ b/metrics-core/src/main/java/com/codahale/metrics/EWMA.java @@ -81,6 +81,14 @@ public void update(long n) { uncounted.add(n); } + /** + * Set the rate to the smallest possible positive value. Used to avoid calling tick a large number of times. + */ + public void reset() { + uncounted.reset(); + rate = Double.MIN_NORMAL; + } + /** * Mark the passage of time and decay the current rate accordingly. */ diff --git a/metrics-core/src/main/java/com/codahale/metrics/ExponentialMovingAverages.java b/metrics-core/src/main/java/com/codahale/metrics/ExponentialMovingAverages.java index 9879d280ee..0a12129c7d 100644 --- a/metrics-core/src/main/java/com/codahale/metrics/ExponentialMovingAverages.java +++ b/metrics-core/src/main/java/com/codahale/metrics/ExponentialMovingAverages.java @@ -11,8 +11,28 @@ */ public class ExponentialMovingAverages implements MovingAverages { + /** + * If ticking would reduce even Long.MAX_VALUE in the 15 minute EWMA below this target then don't bother + * ticking in a loop and instead reset all the EWMAs. + */ + private static final double maxTickZeroTarget = 0.0001; + private static final int maxTicks; private static final long TICK_INTERVAL = TimeUnit.SECONDS.toNanos(5); + static + { + int m3Ticks = 1; + final EWMA m3 = EWMA.fifteenMinuteEWMA(); + m3.update(Long.MAX_VALUE); + do + { + m3.tick(); + m3Ticks++; + } + while (m3.getRate(TimeUnit.SECONDS) > maxTickZeroTarget); + maxTicks = m3Ticks; + } + private final EWMA m1Rate = EWMA.oneMinuteEWMA(); private final EWMA m5Rate = EWMA.fiveMinuteEWMA(); private final EWMA m15Rate = EWMA.fifteenMinuteEWMA(); @@ -51,10 +71,19 @@ public void tickIfNecessary() { final long newIntervalStartTick = newTick - age % TICK_INTERVAL; if (lastTick.compareAndSet(oldTick, newIntervalStartTick)) { final long requiredTicks = age / TICK_INTERVAL; - for (long i = 0; i < requiredTicks; i++) { - m1Rate.tick(); - m5Rate.tick(); - m15Rate.tick(); + if (requiredTicks >= maxTicks) { + m1Rate.reset(); + m5Rate.reset(); + m15Rate.reset(); + } + else + { + for (long i = 0; i < requiredTicks; i++) + { + m1Rate.tick(); + m5Rate.tick(); + m15Rate.tick(); + } } } } diff --git a/metrics-core/src/test/java/com/codahale/metrics/ExponentialMovingAveragesTest.java b/metrics-core/src/test/java/com/codahale/metrics/ExponentialMovingAveragesTest.java new file mode 100644 index 0000000000..2a84940065 --- /dev/null +++ b/metrics-core/src/test/java/com/codahale/metrics/ExponentialMovingAveragesTest.java @@ -0,0 +1,26 @@ +package com.codahale.metrics; + +import java.util.concurrent.TimeUnit; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ExponentialMovingAveragesTest +{ + @Test + public void testMaxTicks() + { + final Clock clock = mock(Clock.class); + when(clock.getTick()).thenReturn(0L, Long.MAX_VALUE); + final ExponentialMovingAverages ema = new ExponentialMovingAverages(clock); + ema.update(Long.MAX_VALUE); + ema.tickIfNecessary(); + final long secondNanos = TimeUnit.SECONDS.toNanos(1); + assertEquals(ema.getM1Rate(), Double.MIN_NORMAL * secondNanos, 0.0); + assertEquals(ema.getM5Rate(), Double.MIN_NORMAL * secondNanos, 0.0); + assertEquals(ema.getM15Rate(), Double.MIN_NORMAL * secondNanos, 0.0); + } +}