// =================================================================================================
// Copyright 2013 Twitter, Inc.
// -------------------------------------------------------------------------------------------------
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this work except in compliance with the License.
// You may obtain a copy of the License in the LICENSE file, or at:
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// =================================================================================================
package com.twitter.common.stats;
import java.util.List;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import com.twitter.common.objectsize.ObjectSizeCalculator;
import com.twitter.common.quantity.Amount;
import com.twitter.common.quantity.Data;
import com.twitter.common.quantity.Time;
import com.twitter.common.stats.testing.RealHistogram;
import com.twitter.common.util.testing.FakeClock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static com.twitter.common.stats.WindowedApproxHistogram.DEFAULT_MAX_MEMORY;
/**
* Tests WindowedHistogram.
*/
public class WindowedHistogramTest {
@Test
public void testEmptyWinHistogram() {
WindowedApproxHistogram wh = new WindowedApproxHistogram();
assertEquals(0L, wh.getQuantile(0.0));
}
@Test
public void testWinHistogramWithEdgeCases() {
FakeClock clock = new FakeClock();
Amount<Long, Time> window = Amount.of(100L, Time.MILLISECONDS);
int slices = 10;
long sliceDuration = window.as(Time.NANOSECONDS) / slices;
WindowedApproxHistogram h =
new WindowedApproxHistogram(window, slices, DEFAULT_MAX_MEMORY, clock);
h.add(Long.MIN_VALUE);
clock.advance(Amount.of(2 * sliceDuration, Time.NANOSECONDS));
assertEquals(Long.MIN_VALUE, h.getQuantile(0.0));
assertEquals(Long.MIN_VALUE, h.getQuantile(0.5));
assertEquals(Long.MIN_VALUE, h.getQuantile(1.0));
h.add(Long.MAX_VALUE);
clock.advance(Amount.of(2 * sliceDuration, Time.NANOSECONDS));
assertEquals(Long.MIN_VALUE, h.getQuantile(0.0));
assertEquals(Long.MIN_VALUE, h.getQuantile(0.25));
assertEquals(Long.MAX_VALUE, h.getQuantile(0.75));
assertEquals(Long.MAX_VALUE, h.getQuantile(1.0));
}
@Test
public void testClearedWinHistogram() {
FakeClock clock = new FakeClock();
Amount<Long, Time> window = Amount.of(100L, Time.MILLISECONDS);
int slices = 10;
Amount<Long, Time> sliceDuration = Amount.of(
window.as(Time.NANOSECONDS) / slices, Time.NANOSECONDS);
WindowedHistogram<?> h = createFullHistogram(window, slices, clock);
long p0 = h.getQuantile(0.1);
long p50 = h.getQuantile(0.5);
long p90 = h.getQuantile(0.9);
assertFalse(0 == p0);
assertFalse(0 == p50);
assertFalse(0 == p90);
h.clear();
assertEquals(0, h.getQuantile(0.1));
assertEquals(0, h.getQuantile(0.5));
assertEquals(0, h.getQuantile(0.9));
// reload the histogram with the exact same values than before
fillHistogram(h, sliceDuration, slices, clock);
assertEquals(p0, h.getQuantile(0.1));
assertEquals(p50, h.getQuantile(0.5));
assertEquals(p90, h.getQuantile(0.9));
}
@Test
public void testSimpleWinHistogram() {
FakeClock clock = new FakeClock();
Amount<Long, Time> window = Amount.of(100L, Time.MILLISECONDS);
int slices = 10;
WindowedHistogram<?> wh = createFullHistogram(window, slices, clock);
// check that the global distribution is the aggregation of all underlying histograms
for (int i = 1; i <= slices; i++) {
double q = (double) i / slices;
assertEquals(i, wh.getQuantile(q), 1.0);
}
// advance in time an forget about old values
long sliceDuration = window.as(Time.NANOSECONDS) / slices;
clock.advance(Amount.of(sliceDuration, Time.NANOSECONDS));
for (int j = 0; j < 1000; j++) {
wh.add(11);
}
assertEquals(2, wh.getQuantile(0.05), 1.0);
assertEquals(11, wh.getQuantile(0.99), 1.0);
}
@Test
public void testWinHistogramWithGap() {
FakeClock clock = new FakeClock();
Amount<Long, Time> window = Amount.of(100L, Time.MILLISECONDS);
int slices = 10;
WindowedHistogram<?> wh = createFullHistogram(window, slices, clock);
// wh is a WindowedHistogram of 10 slices + the empty current with values from 1 to 10
// [1][2][3][4][5][6][7][8][9][10][.]
// ^
for (int j = 0; j < 1000; j++) {
wh.add(100);
}
// [1][2][3][4][5][6][7][8][9][10][100]
// ^
// quantiles are computed based on [1] -> [10]
clock.advance(Amount.of((slices - 1) * 100L / slices, Time.MILLISECONDS));
for (int j = 0; j < 1000; j++) {
wh.add(200);
}
// [1][2][3][4][5][6][7][8][200][10][100]
// ^
// quantiles are computed based on [10][100][1][2][3][4][5][6][7][8]
// and removing old ones [10][100][.][.][.][.][.][.][.][.]
// all the histograms between 100 and 200 are old and shouldn't matter in the computation of
// quantiles.
assertEquals(10L, wh.getQuantile(0.25), 1.0);
assertEquals(100L, wh.getQuantile(0.75), 1.0);
clock.advance(Amount.of(100L / slices, Time.MILLISECONDS));
// [1][2][3][4][5][6][7][8][200][10][100]
// ^
// quantiles are computed based on [100][1][2][3][4][5][6][7][8][200]
// and removing old ones [100][.][.][.][.][.][.][.][.][200]
assertEquals(100L, wh.getQuantile(0.25), 1.0);
assertEquals(200L, wh.getQuantile(0.75), 1.0);
// advance a lot in time, everything should be "forgotten"
clock.advance(Amount.of(500L, Time.MILLISECONDS));
assertEquals(0L, wh.getQuantile(0.5), 1.0);
}
@Test
public void testWinHistogramMemory() {
ImmutableList.Builder<Amount<Long, Data>> builder = ImmutableList.builder();
builder.add(Amount.of(8L, Data.KB));
builder.add(Amount.of(12L, Data.KB));
builder.add(Amount.of(16L, Data.KB));
builder.add(Amount.of(20L, Data.KB));
builder.add(Amount.of(24L, 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();
// large estimation of the memory used outside of buffers
long fixSize = Amount.of(4, Data.KB).as(Data.BYTES);
for (Amount<Long, Data> maxSize: sizes) {
WindowedApproxHistogram hist = new WindowedApproxHistogram(
Amount.of(60L, Time.SECONDS), 6, maxSize);
hist.add(1L);
hist.getQuantile(0.5);
long size = ObjectSizeCalculator.getObjectSize(hist);
// reverting CI JVM seems to have different memory consumption than mine
//assertTrue(size < fixSize + maxSize.as(Data.BYTES));
}
}
@Test
public void testWinHistogramAccuracy() {
FakeClock ticker = new FakeClock();
Amount<Long, Time> window = Amount.of(100L, Time.MILLISECONDS);
int slices = 10;
Amount<Long, Time> sliceDuration = Amount.of(
window.as(Time.NANOSECONDS) / slices, Time.NANOSECONDS);
WindowedHistogram<?> wh = createFullHistogram(window, slices, ticker);
RealHistogram rh = fillHistogram(new RealHistogram(), sliceDuration, slices, new FakeClock());
assertEquals(wh.getQuantile(0.5), rh.getQuantile(0.5));
assertEquals(wh.getQuantile(0.75), rh.getQuantile(0.75));
assertEquals(wh.getQuantile(0.9), rh.getQuantile(0.9));
assertEquals(wh.getQuantile(0.99), rh.getQuantile(0.99));
}
/**
* @return a WindowedHistogram with different value in each underlying Histogram
*/
private WindowedHistogram<?> createFullHistogram(
Amount<Long, Time> duration, int slices, FakeClock clock) {
long sliceDuration = duration.as(Time.NANOSECONDS) / slices;
WindowedApproxHistogram wh = new WindowedApproxHistogram(duration, slices,
DEFAULT_MAX_MEMORY, clock);
clock.advance(Amount.of(1L, Time.NANOSECONDS));
return fillHistogram(wh, Amount.of(sliceDuration, Time.NANOSECONDS), slices, clock);
}
private <H extends Histogram> H fillHistogram(H h,
Amount<Long, Time> sliceDuration, int slices, FakeClock clock) {
for (int i = 1; i <= slices; i++) {
for (int j = 0; j < 1000; j++) {
h.add(i);
}
clock.advance(sliceDuration);
}
return h;
}
}