Add values to a rolling window and retrieve percentile calculations such as median, 90th, 99th, etc.

The underlying data structure contains a circular array of buckets that "roll" over time.

For example, if the time window is configured to 60 seconds with 12 buckets of 5 seconds each, values will be captured in each 5 second bucket and rotate each 5 seconds.

This means that percentile calculations are for the "rolling window" of 55-60 seconds up to 5 seconds ago.

Each bucket will contain a circular array of long values and if more than the configured amount (1000 values for example) it will wrap around and overwrite values until time passes and a new bucket is allocated. This sampling approach for high volume metrics is done to conserve memory and reduce sorting time when calculating percentiles.

        public void RollingPercentile_ValueIsZeroAfterRollingWindowPassesAndNoTraffic()
        {
            MockedTime time = new MockedTime();
            HystrixRollingPercentile p = new HystrixRollingPercentile(time, timeInMilliseconds, numberOfBuckets, bucketDataLength, enabled);
            p.AddValue(1000);
            p.AddValue(1000);
            p.AddValue(1000);
            p.AddValue(2000);
            p.AddValue(4000);

            Assert.AreEqual(1, p.Buckets.Size);

            // no bucket turnover yet so percentile not yet generated
            Assert.AreEqual(0, p.GetPercentile(50));

            time.Increment(6000);

            // still only 1 bucket until we touch it again
            Assert.AreEqual(1, p.Buckets.Size);

            // a bucket has been created so we have a new percentile
            Assert.AreEqual(1500, p.GetPercentile(50));

            // let 1 minute pass
            time.Increment(60000);

            // no data in a minute should mean all buckets are empty (or reset) so we should not have any percentiles
            Assert.AreEqual(0, p.GetPercentile(50));
        }
        public void RollingPercentile_DoesNothingWhenDisabled()
        {
            MockedTime time = new MockedTime();
            int previousTime = 0;
            HystrixRollingPercentile p = new HystrixRollingPercentile(time, timeInMilliseconds, numberOfBuckets, bucketDataLength, HystrixPropertyFactory.AsProperty(false));
            int length = SampleData2.GetLength(0);
            for (int i = 0; i < length; i++)
            {
                int timeInMillisecondsSinceStart = SampleData2[i, 0];
                int latency = SampleData2[i, 1];
                time.Increment(timeInMillisecondsSinceStart - previousTime);
                previousTime = timeInMillisecondsSinceStart;
                p.AddValue(latency);
            }

            Assert.AreEqual(-1, p.GetPercentile(50));
            Assert.AreEqual(-1, p.GetPercentile(75));
            Assert.AreEqual(-1, p.GetMean());
        }
        public void RollingPercentile_SampleDataOverTime2()
        {
            TestContext.WriteLine("\n\n***************************** testSampleDataOverTime2 \n");
            MockedTime time = new MockedTime();
            int previousTime = 0;
            HystrixRollingPercentile p = new HystrixRollingPercentile(time, timeInMilliseconds, numberOfBuckets, bucketDataLength, enabled);
            int length = SampleData2.GetLength(0);
            for (int i = 0; i < length; i++)
            {
                int timeInMillisecondsSinceStart = SampleData2[i, 0];
                int latency = SampleData2[i, 1];
                time.Increment(timeInMillisecondsSinceStart - previousTime);
                previousTime = timeInMillisecondsSinceStart;
                p.AddValue(latency);
            }

            TestContext.WriteLine("0.01: " + p.GetPercentile(0.01));
            TestContext.WriteLine("Median: " + p.GetPercentile(50));
            TestContext.WriteLine("90th: " + p.GetPercentile(90));
            TestContext.WriteLine("99th: " + p.GetPercentile(99));
            TestContext.WriteLine("99.5th: " + p.GetPercentile(99.5));
            TestContext.WriteLine("99.99: " + p.GetPercentile(99.99));

            if (p.GetPercentile(50) > 90 || p.GetPercentile(50) < 50)
            {
                Assert.Fail("We expect around 60-70 but got: " + p.GetPercentile(50));
            }

            if (p.GetPercentile(99) < 400)
            {
                Assert.Fail("We expect to see some high values over 400 but got: " + p.GetPercentile(99));
            }
        }
        public void RollingPercentile_Rolling()
        {
            MockedTime time = new MockedTime();
            HystrixRollingPercentile p = new HystrixRollingPercentile(time, timeInMilliseconds, numberOfBuckets, bucketDataLength, enabled);
            p.AddValue(1000);
            p.AddValue(1000);
            p.AddValue(1000);
            p.AddValue(2000);

            Assert.AreEqual(1, p.Buckets.Size);

            // no bucket turnover yet so percentile not yet generated
            Assert.AreEqual(0, p.GetPercentile(50));

            time.Increment(6000);

            // still only 1 bucket until we touch it again
            Assert.AreEqual(1, p.Buckets.Size);

            // a bucket has been created so we have a new percentile
            Assert.AreEqual(1000, p.GetPercentile(50));

            // now 2 buckets since getting a percentile causes bucket retrieval
            Assert.AreEqual(2, p.Buckets.Size);

            p.AddValue(1000);
            p.AddValue(500);

            // should still be 2 buckets
            Assert.AreEqual(2, p.Buckets.Size);

            p.AddValue(200);
            p.AddValue(200);
            p.AddValue(1600);
            p.AddValue(200);
            p.AddValue(1600);
            p.AddValue(1600);

            // we haven't progressed to a new bucket so the percentile should be the same and ignore the most recent bucket
            Assert.AreEqual(1000, p.GetPercentile(50));

            // increment to another bucket so we include all of the above in the PercentileSnapshot
            time.Increment(6000);

            // the rolling version should have the same data as creating a snapshot like this
            HystrixRollingPercentile.PercentileSnapshot ps = new HystrixRollingPercentile.PercentileSnapshot(1000, 1000, 1000, 2000, 1000, 500, 200, 200, 1600, 200, 1600, 1600);

            Assert.AreEqual(ps.GetPercentile(0.15), p.GetPercentile(0.15));
            Assert.AreEqual(ps.GetPercentile(0.50), p.GetPercentile(0.50));
            Assert.AreEqual(ps.GetPercentile(0.90), p.GetPercentile(0.90));
            Assert.AreEqual(ps.GetPercentile(0.995), p.GetPercentile(0.995));

            TestContext.WriteLine("100th: " + ps.GetPercentile(100) + "  " + p.GetPercentile(100));
            TestContext.WriteLine("99.5th: " + ps.GetPercentile(99.5) + "  " + p.GetPercentile(99.5));
            TestContext.WriteLine("99th: " + ps.GetPercentile(99) + "  " + p.GetPercentile(99));
            TestContext.WriteLine("90th: " + ps.GetPercentile(90) + "  " + p.GetPercentile(90));
            TestContext.WriteLine("50th: " + ps.GetPercentile(50) + "  " + p.GetPercentile(50));
            TestContext.WriteLine("10th: " + ps.GetPercentile(10) + "  " + p.GetPercentile(10));

            // mean = 1000+1000+1000+2000+1000+500+200+200+1600+200+1600+1600/12
            Assert.AreEqual(991, ps.Mean);
        }
        public void RollingPercentile_SampleDataOverTime1()
        {
            TestContext.WriteLine("\n\n***************************** testSampleDataOverTime1 \n");

            MockedTime time = new MockedTime();
            HystrixRollingPercentile p = new HystrixRollingPercentile(time, timeInMilliseconds, numberOfBuckets, bucketDataLength, enabled);
            int previousTime = 0;
            int length = SampleData1.GetLength(0);
            for (int i = 0; i < length; i++)
            {
                int timeInMillisecondsSinceStart = SampleData1[i, 0];
                int latency = SampleData1[i, 1];
                time.Increment(timeInMillisecondsSinceStart - previousTime);
                previousTime = timeInMillisecondsSinceStart;
                p.AddValue(latency);
            }

            TestContext.WriteLine("0.01: " + p.GetPercentile(0.01));
            TestContext.WriteLine("Median: " + p.GetPercentile(50));
            TestContext.WriteLine("90th: " + p.GetPercentile(90));
            TestContext.WriteLine("99th: " + p.GetPercentile(99));
            TestContext.WriteLine("99.5th: " + p.GetPercentile(99.5));
            TestContext.WriteLine("99.99: " + p.GetPercentile(99.99));

            TestContext.WriteLine("Median: " + p.GetPercentile(50));
            TestContext.WriteLine("Median: " + p.GetPercentile(50));
            TestContext.WriteLine("Median: " + p.GetPercentile(50));

            /*
             * In a loop as a use case was found where very different values were calculated in subsequent requests.
             */
            for (int i = 0; i < 10; i++)
            {
                if (p.GetPercentile(50) > 5)
                {
                    Assert.Fail("We expect around 2 but got: " + p.GetPercentile(50));
                }

                if (p.GetPercentile(99.5) < 20)
                {
                    Assert.Fail("We expect to see some high values over 20 but got: " + p.GetPercentile(99.5));
                }
            }
        }