package se.l4.vibe.percentile; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLongArray; import se.l4.vibe.VibeException; /** * A {@link PercentileCounter} that uses a fixed set of buckets that values are * sorted into. * * <p> * Each bucket represent a range, the first value given in this array is the start * of the first bucket and the last one is the upper bound for the <i>next to last</i> * bucket. * * <p> * Example: * <pre> * [0, 100, 400, 500] * </pre> * * Buckets created: * <ol> * <li>0-100</li> * <li>101-400</li> * <li>401-500</li> * <li>501-*</li> * </ol> * * @author Andreas Holstenson * */ public class BucketPercentileCounter implements PercentileCounter { private final int[] limits; private volatile AtomicLongArray buckets; private final AtomicLong total; public BucketPercentileCounter(int... limits) { for(int i=1, n=limits.length; i<n; i++) { if(limits[i-1] >= limits[i]) { throw new VibeException("Limits must be in ascending order"); } } this.limits = limits; buckets = new AtomicLongArray(limits.length); total = new AtomicLong(); } @Override public void add(long value) { int i = getBucket((int) value); if(i == -1) return; while(true) { long current = total.get(); if(total.compareAndSet(current, current+value)) { buckets.incrementAndGet(i); break; } } } @Override public void reset() { AtomicLongArray buckets = new AtomicLongArray(limits.length); while(true) { long current = total.get(); if(total.compareAndSet(current, 0)) { this.buckets = buckets; break; } } } @Override public PercentileSnapshot get() { long[] values = new long[limits.length]; int samples = 0; for(int i=0, n=limits.length; i<n; i++) { values[i] = buckets.get(i); samples += values[i]; } long total = this.total.get(); return new BucketSnapshot(samples, total, values, limits); } int getBucket(int time) { int low = 0; int high = limits.length - 1; int i = 0; while(low <= high) { i = (low + high) / 2; int d = limits[i] - time; if(d < 0) { low = i + 1; } else if(d > 0) { high = i - 1; } else { return i; } } if(limits[i] >= time) { return i - 1; } else { return i; } } private static class BucketSnapshot implements PercentileSnapshot { private long samples; private long total; private long[] buckets; private int[] limits; public BucketSnapshot(long samples, long total, long[] buckets, int[] limits) { this.samples = samples; this.total = total; this.buckets = buckets; this.limits = limits; } @Override public long getTotal() { return total; } @Override public long getSamples() { return samples; } @Override public long estimatePercentile(int percentile) { long sum = 0; long cutoff = (long) Math.ceil((percentile / 100.0) * samples) - 1; for(int i=0, n=buckets.length-1; i<n; i++) { sum += buckets[i]; if(sum >= cutoff) { return limits[i+1]; } } return -1; } @Override public PercentileSnapshot add(PercentileSnapshot other) { BucketSnapshot s = (BucketSnapshot) other; long[] newBuckets = new long[buckets.length]; for(int i=0, n=newBuckets.length; i<n; i++) { newBuckets[i] = buckets[i] + s.buckets[i]; } return new BucketSnapshot( samples + s.samples, total + s.total, newBuckets, limits ); } @Override public PercentileSnapshot remove(PercentileSnapshot other) { BucketSnapshot s = (BucketSnapshot) other; long[] newBuckets = new long[buckets.length]; for(int i=0, n=newBuckets.length; i<n; i++) { newBuckets[i] = buckets[i] - s.buckets[i]; } return new BucketSnapshot( samples - s.samples, total - s.total, newBuckets, limits ); } } }