/* * Copyright 2014 LinkedIn Corp. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License 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.codahale.metrics; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertTrue; import java.lang.Double; import java.lang.InterruptedException; import org.apache.commons.math3.stat.StatUtils; import org.testng.annotations.Test; import com.codahale.metrics.MergeableExponentiallyDecayingReservoir; /** * This class actually tests the combination of MergeableExponentiallyDecayingReservoir and StatUtils. * It includes one test of merge(), but merging/aggregation is mostly tested in TestUnifiedClientStats. */ public class TestMergeableExponentiallyDecayingReservoir { // borrowed from com.linkedin.databus.core.DbusConstants: private static final long NUM_MSECS_IN_SEC = 1000L; private static final long NUM_NSECS_IN_MSEC = 1000000L; @Test public void testEmptyReservoir() { MergeableExponentiallyDecayingReservoir res = new MergeableExponentiallyDecayingReservoir(10, 0.015); // add NO data double[] dataValues = res.getUnsortedValues(); assertEquals("expected empty dataValues array", 0, dataValues.length); double result = StatUtils.percentile(dataValues, 50.0); assertEquals("expected NaN for 50th percentile of empty array", Double.NaN, result); result = StatUtils.max(dataValues); assertEquals("expected NaN for max of empty array", Double.NaN, result); } /** * Tests aggregation (merging) of two "low-level" reservoirs into a third. The reservoirs are created * with different landmark values; the test verifies that the landmark values are the same after merging. * * In particular, create the aggregator first; wait 1 sec, then create the first low-level reservoir (res1); * wait another second, then create the second low-level reservoir (res2). Initially three landmark values * should all differ. (Landmarks are stored only at 1-second granularity.) After merging res1 into the * aggregate, the latter's landmark should equal res1's; res1's should not have changed. After merging res2 * into the aggregate, the latter's landmark should now equal res2's, but res2's similarly should not have * changed. After generating more data and merging res1 into the aggregate again, res1's landmark should * now equal res2's and the aggregate's, i.e., all three values are synchronized. */ @Test public void testReservoirMergeAndLandmarkSynch() throws InterruptedException { // two low-level reservoirs and one aggregator MergeableExponentiallyDecayingReservoir res1; MergeableExponentiallyDecayingReservoir res2; MergeableExponentiallyDecayingReservoir aggr; aggr = new MergeableExponentiallyDecayingReservoir(10, 0.015); Thread.sleep(1000L); res1 = new MergeableExponentiallyDecayingReservoir(10, 0.015); Thread.sleep(1000L); res2 = new MergeableExponentiallyDecayingReservoir(10, 0.015); //long origLandmarkAggr = aggr.getLandmark(); long origLandmarkRes1 = res1.getLandmark(); long origLandmarkRes2 = res2.getLandmark(); assertTrue("expected aggregator to have older landmark value than res1", aggr.getLandmark() < origLandmarkRes1); assertTrue("expected res1 to have older landmark value than res2", origLandmarkRes1 < origLandmarkRes2); // generate some data for both low-level reservoirs, then make sure their landmarks don't change for (int i = 0; i < 10; ++i) { long nowSecs = System.currentTimeMillis() / NUM_MSECS_IN_SEC; long timestamp1 = nowSecs - i - 3L; long timestamp2 = nowSecs - i - 5L; res1.update((double)i, timestamp1); res2.update((double)(i+100), timestamp2); } assertEquals("expected res1 landmark value to be unchanged", origLandmarkRes1, res1.getLandmark()); assertEquals("expected res2 landmark value to be unchanged", origLandmarkRes2, res2.getLandmark()); aggr.merge(res1); assertEquals("expected res1 landmark value to be unchanged", origLandmarkRes1, res1.getLandmark()); assertEquals("expected aggregator landmark value to match res1", origLandmarkRes1, aggr.getLandmark()); aggr.merge(res2); assertEquals("expected res2 landmark value to be unchanged", origLandmarkRes2, res2.getLandmark()); assertEquals("expected aggregator landmark value to match res2", origLandmarkRes2, aggr.getLandmark()); // generate some more data for both low-level reservoirs; their landmarks still should not have changed for (int i = 0; i < 10; ++i) { long nowSecs = System.currentTimeMillis() / NUM_MSECS_IN_SEC; long timestamp1 = nowSecs - i - 1L; long timestamp2 = nowSecs - i - 2L; res1.update((double)(i+200), timestamp1); res2.update((double)(i+300), timestamp2); } assertEquals("expected res1 landmark value to be unchanged", origLandmarkRes1, res1.getLandmark()); assertEquals("expected res2 landmark value to be unchanged", origLandmarkRes2, res2.getLandmark()); aggr.merge(res1); assertEquals("expected aggregator landmark value to be unchanged", origLandmarkRes2, aggr.getLandmark()); assertEquals("expected res1 landmark value to match res2", origLandmarkRes2, res1.getLandmark()); } /** * Using an artificial clock, pass in new data values after "half an hour," and verify that * they replace some of the older values. */ // Both metrics-core and Apache Commons Math use the "R-6" quantile-estimation method, as described // at http://en.wikipedia.org/wiki/Quantile . // // N = 10 // p = 0.5, 0.9, 0.95, 0.99 // h = 5.5, 9.9, 10.45, 10.89 // (assume x[n] for n >= dataValues.length equals x[dataValues.length - 1] == max value) // // Q[50th] = x[5-1] + (5.5 - 5)*(x[5-1+1] - x[5-1]) = 5.0 + 0.5*(6.0 - 5.0) = 5.5 // Q[90th] = x[9-1] + (9.9 - 9)*(x[9-1+1] - x[9-1]) = 9.0 + 0.9*(10.0 - 9.0) = 9.9 // Q[95th] = x[10-1] + (10.45 - 10)*(x[10-1+1] - x[10-1]) = 10.0 + 0.45*(10.0 - 10.0) = 10.0 // Q[99th] = x[10-1] + (10.89 - 10)*(x[10-1+1] - x[10-1]) = 10.0 + 0.89*(10.0 - 10.0) = 10.0 @Test public void testReservoirReplacement() { ManuallyControllableClock clock = new ManuallyControllableClock(); MergeableExponentiallyDecayingReservoir res = new MergeableExponentiallyDecayingReservoir(10, 0.015, clock); clock.advanceTime(1L * NUM_MSECS_IN_SEC * NUM_NSECS_IN_MSEC); // initial data show up 1 sec after reservoir created res.update(3.0); res.update(8.0); res.update(9.0); res.update(4.0); res.update(7.0); res.update(5.0); res.update(2.0); res.update(10.0); res.update(6.0); res.update(1.0); double[] dataValues = res.getUnsortedValues(); assertEquals("expected non-empty dataValues array", 10, dataValues.length); double result = StatUtils.percentile(dataValues, 50.0); assertEquals("unexpected 50th percentile", 5.5, result); result = StatUtils.percentile(dataValues, 90.0); assertEquals("unexpected 90th percentile", 9.9, result); result = StatUtils.percentile(dataValues, 95.0); assertEquals("unexpected 95th percentile", 10.0, result); result = StatUtils.percentile(dataValues, 99.0); assertEquals("unexpected 99th percentile", 10.0, result); result = StatUtils.max(dataValues); assertEquals("unexpected max", 10.0, result); result = StatUtils.min(dataValues); assertEquals("unexpected min", 1.0, result); // Now advance the time and add a couple more values. We don't control the random-number generation, // so we don't know the priorities of either the original 10 data points or the two new ones, but we // do expect the new ones to have higher priorities than most or all of the original set, thanks to // their "newness" (by half an hour) and the alpha value that exponentially weights data from the most // recent 5 minutes. Since they're bigger/smaller than all the rest of the data values, the new max/min // values should reflect them regardless of which older data points they preempted. clock.advanceTime(1800L * NUM_MSECS_IN_SEC * NUM_NSECS_IN_MSEC); // new data show up 30 min after initial set res.update(20.0); res.update(0.0); dataValues = res.getUnsortedValues(); assertEquals("expected size for dataValues array", 10, dataValues.length); result = StatUtils.max(dataValues); assertEquals("unexpected max", 20.0, result); result = StatUtils.min(dataValues); assertEquals("unexpected min", 0.0, result); } @Test public void testReservoirWithIdenticalValues() { MergeableExponentiallyDecayingReservoir res = new MergeableExponentiallyDecayingReservoir(10, 0.015); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); res.update(7.0); double[] dataValues = res.getUnsortedValues(); assertEquals("expected non-empty dataValues array", 10, dataValues.length); double result = StatUtils.percentile(dataValues, 50.0); assertEquals("expected 50th percentile to equal (constant) value of data points", 7.0, result); result = StatUtils.percentile(dataValues, 90.0); assertEquals("expected 90th percentile to equal (constant) value of data points", 7.0, result); result = StatUtils.percentile(dataValues, 95.0); assertEquals("expected 95th percentile to equal (constant) value of data points", 7.0, result); result = StatUtils.percentile(dataValues, 99.0); assertEquals("expected 99th percentile to equal (constant) value of data points", 7.0, result); result = StatUtils.max(dataValues); assertEquals("unexpected max for set of constant data points", 7.0, result); } @Test public void testReservoirWithSingleDatum() { MergeableExponentiallyDecayingReservoir res = new MergeableExponentiallyDecayingReservoir(10, 0.015); res.update(3.0); double[] dataValues = res.getUnsortedValues(); assertEquals("expected non-empty dataValues array", 1, dataValues.length); double result = StatUtils.percentile(dataValues, 50.0); assertEquals("expected 50th percentile to equal value of single data point", 3.0, result); result = StatUtils.percentile(dataValues, 90.0); assertEquals("expected 90th percentile to equal value of single data point", 3.0, result); result = StatUtils.percentile(dataValues, 95.0); assertEquals("expected 95th percentile to equal value of single data point", 3.0, result); result = StatUtils.percentile(dataValues, 99.0); assertEquals("expected 99th percentile to equal value of single data point", 3.0, result); result = StatUtils.max(dataValues); assertEquals("expected max to equal value of single data point", 3.0, result); } public static class ManuallyControllableClock extends Clock { // 20130106 13:22:22 PST, but could be anything... private static long currentTimeNs = 1389043342L * NUM_MSECS_IN_SEC * NUM_NSECS_IN_MSEC; @Override public long getTick() { return currentTimeNs; } @Override public long getTime() { return currentTimeNs / NUM_NSECS_IN_MSEC; } public void advanceTime(long timeIncrementNs) { currentTimeNs += timeIncrementNs; } } }