package io.pcp.parfait; import java.util.Arrays; import net.jcip.annotations.GuardedBy; import net.jcip.annotations.ThreadSafe; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; /** * <p> * Counter which keeps track of the increments only over a particular * {@link TimeWindow}, expiring old increments after the window has elapsed. For * example, a period of Uses the resolution of the supplied TimeWindow to group * events together for efficiency. For example, when supplied with a TimeWindow * with a period of 60 seconds and a resolution of 5 seconds, 12 'buckets' will * be created to keep count of events. This means that as each bucket is * overwritten after 60 seconds, the total count may be understated by as much * as (5/60) ≅ 8%. This, however, means that the memory footprint is * approximately that of just the 12 counters, rather than having to track the * time of each individual event. * </p> * <p> * Currently uses very coarse-grained locking (each {@link #get()} or * {@link #inc(long)} takes and holds a shared lock for the duration); this may * prove too contentious and require change later. * </p> */ @ThreadSafe public class TimeWindowCounter implements Counter { @GuardedBy("lock") private long overallValue; @GuardedBy("lock") private final long[] interimValues; @GuardedBy("lock") private int headIndex = 0; @GuardedBy("lock") private long headTime; private final Object lock = new Object(); private final Supplier<Long> timeSource; private final TimeWindow window; public TimeWindowCounter(TimeWindow window) { this(window, new SystemTimePoller()); } @VisibleForTesting TimeWindowCounter(TimeWindow window, Supplier<Long> timeSource) { this.window = window; this.interimValues = new long[(int) (window.getBuckets())]; this.timeSource = timeSource; this.headTime = timeSource.get(); } @Override public void inc(long increment) { synchronized (lock) { cleanState(); overallValue += increment; interimValues[headIndex] += increment; } } /** * Clean out old data from the buckets, getting us ready to enter a new * bucket. interimValues, headTime, and headIndex comprise a circular buffer * of the last n sub-values, and the start time of the head bucket. On each * write or get, we progressively clear out entries in the circular buffer * until headTime is within one 'tick' of the current time; we have then * found the correct bucket. */ @GuardedBy("lock") private void cleanState() { long eventTime = timeSource.get(); long bucketsToSkip = (eventTime - headTime) / window.getResolution(); while (bucketsToSkip > 0) { headIndex = (headIndex + 1) % interimValues.length; bucketsToSkip--; overallValue -= interimValues[headIndex]; interimValues[headIndex] = 0L; headTime += window.getResolution(); } } public Long get() { synchronized (lock) { cleanState(); return overallValue; } } @Override public void inc() { inc(1L); } @Override public String toString() { return String.format("last %s=%s", window.getName(), overallValue); } @VisibleForTesting String counterState() { synchronized (lock) { return Arrays.toString(interimValues); } } }