package com.twitter.common.stats;
import java.util.Arrays;
import java.util.List;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.pantsbuild.junit.annotations.TestParallel;
import com.twitter.common.objectsize.ObjectSizeCalculator;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Data;
import com.twitter.common.stats.ApproximateHistogram;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@TestParallel
public class ApproximateHistogramTest {
final int b = 10;
final int h = 3;
@Test
public void testCollapse() {
ApproximateHistogram hist = new ApproximateHistogram();
long[] buf1 = {2,5,7};
long[] buf2 = {3,8,9};
long[] expected = {3,7,9};
long[] result = new long[3];
// [2,5,7] weight 2 and [3,8,9] weight 3
// weight x array + concat = [2,2,5,5,7,7,3,3,3,8,8,8,9,9,9]
// sort = [2,2,3,3,3,5,5,7,7,8,8,8,9,9,9]
// select every nth elems = [3,7,9] (n = sum weight / 2, ie. 5/3 = 2)
// [2,2,3,3,3,5,5,7,7,8,8,8,9,9,9]
// . . ^ . . . . ^ . . . . ^ . .
// [-------] [-------] [-------] we make 3 packets of 5 elements and take the middle
ApproximateHistogram.collapse(buf1, 2, buf2, 3, result);
assertArrayEquals(result, expected);
long[] buf3 = {2, 5, 7, 9};
long[] buf4 = {3, 8, 9, 12};
long[] expected2 = {3, 7, 9, 12};
long[] result2 = new long[4];
ApproximateHistogram.collapse(buf3, 2, buf4, 2, result2);
assertArrayEquals(expected2, result2);
}
@Test
public void testRecCollapse() {
long[] empty = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
long[] full = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
ApproximateHistogram hist = new ApproximateHistogram(b, h);
assertArrayEquals(empty, hist.buffer[0]);
assertArrayEquals(empty, hist.buffer[1]);
initializeValues(hist, b, Suppliers.ofInstance(1L));
assertArrayEquals(full, hist.buffer[0]);
assertArrayEquals(empty, hist.buffer[1]);
initializeValues(hist, b, Suppliers.ofInstance(1L));
assertArrayEquals(full, hist.buffer[0]);
assertArrayEquals(full, hist.buffer[1]);
hist.add(1);
assertEquals(2, hist.currentTop);
// Buffers are not cleared so we can't check that!
assertArrayEquals(full, hist.buffer[2]);
initializeValues(hist, 2 * b, Suppliers.ofInstance(1L));
assertEquals(3, hist.currentTop);
assertArrayEquals(full, hist.buffer[3]);
}
@Test
public void testReachingMaxDepth() {
ApproximateHistogram hist = new ApproximateHistogram(b, h);
initializeValues(hist, 8 * b, Suppliers.ofInstance(1L));
assertEquals(3, hist.currentTop);
hist.add(1);
assertEquals(3, hist.currentTop);
}
@Test
public void testMem() {
for (int b = 10; b < 100; b += 10) {
for (int h = 4; h < 16; h += 4) {
ApproximateHistogram hist = new ApproximateHistogram(b, h);
long actualSize = ObjectSizeCalculator.getObjectSize(hist);
long estimatedSize = ApproximateHistogram.memoryUsage(b, h);
assertTrue("Consume less memory than the constraint", actualSize < estimatedSize);
}
}
}
@Test
public void testMemConstraint() {
ImmutableList.Builder<Amount<Long, Data>> builder = ImmutableList.builder();
builder.add(Amount.of(1L, Data.KB));
builder.add(Amount.of(4L, Data.KB));
builder.add(Amount.of(8L, Data.KB));
builder.add(Amount.of(16L, Data.KB));
builder.add(Amount.of(32L, Data.KB));
builder.add(Amount.of(64L, Data.KB));
builder.add(Amount.of(256L, Data.KB));
builder.add(Amount.of(1L, Data.MB));
builder.add(Amount.of(16L, Data.MB));
builder.add(Amount.of(32L, Data.MB));
List<Amount<Long, Data>> sizes = builder.build();
for (Amount<Long, Data> maxSize: sizes) {
ApproximateHistogram hist = new ApproximateHistogram(maxSize);
for (long i = 0; i < 1000 * 1000; i++) { hist.add(i); }
long size = ObjectSizeCalculator.getObjectSize(hist);
assertTrue(size < maxSize.as(Data.BYTES));
}
}
@Test
public void testLowMemoryPrecision() {
double e = ApproximateHistogram.DEFAULT_PRECISION.getEpsilon();
int n = ApproximateHistogram.DEFAULT_PRECISION.getN();
int defaultDepth = ApproximateHistogram.computeDepth(e, n);
int defaultBufferSize = ApproximateHistogram.computeBufferSize(defaultDepth, n);
ApproximateHistogram hist = new ApproximateHistogram(Amount.of(1L, Data.KB));
int depth = hist.buffer.length - 1;
int bufferSize = hist.buffer[0].length;
assertTrue(depth > defaultDepth);
assertTrue(bufferSize < defaultBufferSize);
}
@Test
public void testHighMemoryPrecision() {
double e = ApproximateHistogram.DEFAULT_PRECISION.getEpsilon();
int n = ApproximateHistogram.DEFAULT_PRECISION.getN();
int defaultDepth = ApproximateHistogram.computeDepth(e, n);
int defaultBufferSize = ApproximateHistogram.computeBufferSize(defaultDepth, n);
ApproximateHistogram hist = new ApproximateHistogram(Amount.of(1L, Data.MB));
int depth = hist.buffer.length - 1;
int bufferSize = hist.buffer[0].length;
assertTrue(depth < defaultDepth);
assertTrue(bufferSize > defaultBufferSize);
}
private void initIndexArray(ApproximateHistogram hist, int b) {
Arrays.fill(hist.indices, b - 1);
int buf0Size = Math.min(b, hist.leafCount);
int buf1Size = Math.max(0, hist.leafCount - buf0Size);
hist.indices[0] = buf0Size - 1;
hist.indices[1] = buf1Size - 1;
}
private long getBiggest(ApproximateHistogram hist) {
int j = hist.biggest(hist.indices);
int idx = hist.indices[j];
hist.indices[j] -= 1;
return hist.buffer[j][idx];
}
@Test
public void testBiggestIndexFinder() {
ApproximateHistogram hist = new ApproximateHistogram(b, h);
int n = 3;
for (int i=1; i <= n; i++) {
hist.add(i);
}
initIndexArray(hist, b);
for (int i=1; i <= n; i++) {
assertEquals(n - i + 1, getBiggest(hist));
}
n = 2 * b;
for (int i=4; i <= n; i++) {
hist.add(i);
}
initIndexArray(hist, b);
for (int i=1; i <= n; i++) {
assertEquals(n - i + 1, getBiggest(hist));
}
hist.add(2*b + 1);
n += 1;
initIndexArray(hist, b);
assertEquals(n, getBiggest(hist));
for (int i=2; i <= n; i += 2) {
assertEquals(n - i + 1, getBiggest(hist));
}
}
@Test
public void testIsBufferEmpty() {
ApproximateHistogram hist = new ApproximateHistogram(b, h);
for (int i=0; i < 3*b; i++) {
hist.add(i);
}
assertEquals(false, hist.isBufferEmpty(2));
assertEquals(true, hist.isBufferEmpty(3));
for (int i=0; i < 2*b; i++) {
hist.add(i);
}
assertEquals(true, hist.isBufferEmpty(2));
assertEquals(false, hist.isBufferEmpty(3));
}
@Test
public void testHistogramWithNegative() {
ApproximateHistogram hist = new ApproximateHistogram();
hist.add(-1L);
assertEquals(-1L, hist.getQuantile(0.0));
assertEquals(-1L, hist.getQuantile(0.5));
assertEquals(-1L, hist.getQuantile(1.0));
}
@Test
public void testHistogramWithEdgeCases() {
ApproximateHistogram hist = new ApproximateHistogram();
hist.add(Long.MIN_VALUE);
assertEquals(Long.MIN_VALUE, hist.getQuantile(0.0));
assertEquals(Long.MIN_VALUE, hist.getQuantile(1.0));
hist.add(Long.MAX_VALUE);
assertEquals(Long.MIN_VALUE, hist.getQuantile(0.0));
assertEquals(Long.MAX_VALUE, hist.getQuantile(1.0));
}
@Test
public void testQueryZerothQuantile() {
// Tests that querying the zeroth quantile does not throw an exception
ApproximateHistogram hist = new ApproximateHistogram(b, h);
initializeValues(hist, 10, Suppliers.ofInstance(1L));
assertEquals(1L, hist.getQuantile(0.0));
}
@Test
public void testSmallDataCase() {
// Tests that querying the zeroth quantile does not throw an exception
ApproximateHistogram hist = new ApproximateHistogram(b, h);
initializeValues(hist, 1, Suppliers.ofInstance(1L));
assertEquals(1L, hist.getQuantile(0.5));
}
@Test
public void testSimpleCase() {
ApproximateHistogram hist = new ApproximateHistogram();
int n = 10;
initializeValues(hist, n, monotonic());
for (int i = 1; i <= n; i++) {
double q = i / 10.0;
assertEquals(i, hist.getQuantile(q), 1.0);
}
}
@Test
public void testGetQuantiles() {
ApproximateHistogram hist = new ApproximateHistogram();
int n = 10;
initializeValues(hist, n, monotonic());
double[] quantiles = new double[n];
for (int i = 0; i < n; i++) {
quantiles[i] = (i + 1) / 10.0;
}
long[] results = hist.getQuantiles(quantiles);
for (int i = 0; i < n; i++) {
long res = results[i];
double q = quantiles[i];
assertEquals(hist.getQuantile(q), res);
}
}
@Test
public void testYetAnotherGetQuantiles() {
// this test originates from issue CSL-586
ApproximateHistogram hist = new ApproximateHistogram();
hist.add(0);
hist.add(4);
hist.add(9);
hist.add(8);
double[] quantiles = new double[]{0.5, 0.9, 0.99};
long[] expected = new long[]{8,9,9};
assertArrayEquals(hist.getQuantiles(quantiles), expected);
}
private static void initializeValues(ApproximateHistogram hist, int n, Supplier<Long> what) {
for (int i=0; i<n ; i++) {
hist.add(what.get());
}
}
private static Supplier<Long> monotonic() {
return new Supplier<Long>() {
long i = 0;
@Override public Long get() { return ++i; }
};
}
}