| /* |
| * Copyright 2015 Facebook, Inc. |
| * |
| * 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. |
| */ |
| |
| #include <folly/stats/TimeseriesHistogram.h> |
| #include <folly/stats/TimeseriesHistogram-defs.h> |
| |
| #include <gtest/gtest.h> |
| |
| using namespace std; |
| using namespace folly; |
| using std::chrono::seconds; |
| |
| namespace IntMTMHTS { |
| enum Levels { |
| MINUTE, |
| TEN_MINUTE, |
| HOUR, |
| ALLTIME, |
| NUM_LEVELS, |
| }; |
| |
| const seconds kDurations[] = { |
| seconds(60), seconds(600), seconds(3600), seconds(0) |
| }; |
| }; |
| |
| namespace IntMHTS { |
| enum Levels { |
| MINUTE, |
| HOUR, |
| ALLTIME, |
| NUM_LEVELS, |
| }; |
| |
| const seconds kDurations[] = { |
| seconds(60), seconds(3600), seconds(0) |
| }; |
| }; |
| |
| typedef std::mt19937 RandomInt32; |
| |
| TEST(TimeseriesHistogram, Percentile) { |
| RandomInt32 random(5); |
| // [10, 109], 12 buckets including above and below |
| { |
| TimeseriesHistogram<int> h(10, 10, 110, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| EXPECT_EQ(0, h.getPercentileEstimate(0, IntMTMHTS::ALLTIME)); |
| |
| EXPECT_EQ(12, h.getNumBuckets()); |
| EXPECT_EQ(10, h.getBucketSize()); |
| EXPECT_EQ(10, h.getMin()); |
| EXPECT_EQ(110, h.getMax()); |
| |
| for (int i = 0; i < h.getNumBuckets(); ++i) { |
| EXPECT_EQ(4, h.getBucket(i).numLevels()); |
| } |
| |
| int maxVal = 120; |
| h.addValue(seconds(0), 0); |
| h.addValue(seconds(0), maxVal); |
| for (int i = 0; i < 98; i++) { |
| h.addValue(seconds(0), random() % maxVal); |
| } |
| |
| h.update(std::chrono::duration_cast<std::chrono::seconds>( |
| std::chrono::system_clock::now().time_since_epoch())); |
| // bucket 0 stores everything below min, so its minimum |
| // is the lowest possible number |
| EXPECT_EQ(std::numeric_limits<int>::min(), |
| h.getPercentileBucketMin(1, IntMTMHTS::ALLTIME)); |
| EXPECT_EQ(110, h.getPercentileBucketMin(99, IntMTMHTS::ALLTIME)); |
| |
| EXPECT_EQ(-2, h.getPercentileEstimate(0, IntMTMHTS::ALLTIME)); |
| EXPECT_EQ(-1, h.getPercentileEstimate(1, IntMTMHTS::ALLTIME)); |
| EXPECT_EQ(119, h.getPercentileEstimate(99, IntMTMHTS::ALLTIME)); |
| EXPECT_EQ(120, h.getPercentileEstimate(100, IntMTMHTS::ALLTIME)); |
| } |
| } |
| |
| TEST(TimeseriesHistogram, String) { |
| RandomInt32 random(5); |
| // [10, 109], 12 buckets including above and below |
| { |
| TimeseriesHistogram<int> hist(10, 10, 110, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| int maxVal = 120; |
| hist.addValue(seconds(0), 0); |
| hist.addValue(seconds(0), maxVal); |
| for (int i = 0; i < 98; i++) { |
| hist.addValue(seconds(0), random() % maxVal); |
| } |
| |
| hist.update(seconds(0)); |
| |
| const char* const kStringValues1[IntMTMHTS::NUM_LEVELS] = { |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| }; |
| |
| CHECK_EQ(IntMTMHTS::NUM_LEVELS, hist.getNumLevels()); |
| |
| for (int level = 0; level < hist.getNumLevels(); ++level) { |
| EXPECT_EQ(kStringValues1[level], hist.getString(level)); |
| } |
| |
| const char* const kStringValues2[IntMTMHTS::NUM_LEVELS] = { |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| "-2147483648:12:4,10:8:13,20:8:24,30:6:34,40:13:46,50:8:54,60:7:64," |
| "70:7:74,80:8:84,90:10:94,100:3:103,110:10:115", |
| }; |
| |
| CHECK_EQ(IntMTMHTS::NUM_LEVELS, hist.getNumLevels()); |
| |
| for (int level = 0; level < hist.getNumLevels(); ++level) { |
| EXPECT_EQ(kStringValues2[level], hist.getString(level)); |
| } |
| } |
| } |
| |
| TEST(TimeseriesHistogram, Clear) { |
| { |
| TimeseriesHistogram<int> hist(10, 0, 100, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| for (int now = 0; now < 3600; now++) { |
| for (int i = 0; i < 100; i++) { |
| hist.addValue(seconds(now), i, 2); // adds each item 2 times |
| } |
| } |
| |
| // check clearing |
| hist.clear(); |
| |
| for (int b = 0; b < hist.getNumBuckets(); ++b) { |
| EXPECT_EQ(0, hist.getBucket(b).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(b).count(IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(b).count(IntMTMHTS::HOUR)); |
| EXPECT_EQ(0, hist.getBucket(b).count(IntMTMHTS::ALLTIME)); |
| } |
| |
| for (int pct = 0; pct <= 100; pct++) { |
| EXPECT_EQ(0, hist.getPercentileBucketMin(pct, IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, hist.getPercentileBucketMin(pct, IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(0, hist.getPercentileBucketMin(pct, IntMTMHTS::HOUR)); |
| EXPECT_EQ(0, hist.getPercentileBucketMin(pct, IntMTMHTS::ALLTIME)); |
| |
| EXPECT_EQ(0, hist.getPercentileEstimate(pct, IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, hist.getPercentileEstimate(pct, IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(0, hist.getPercentileEstimate(pct, IntMTMHTS::HOUR)); |
| EXPECT_EQ(0, hist.getPercentileEstimate(pct, IntMTMHTS::ALLTIME)); |
| } |
| } |
| } |
| |
| |
| TEST(TimeseriesHistogram, Basic) { |
| { |
| TimeseriesHistogram<int> hist(10, 0, 100, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| for (int now = 0; now < 3600; now++) { |
| for (int i = 0; i < 100; i++) { |
| hist.addValue(seconds(now), i); |
| } |
| } |
| |
| hist.update(seconds(3599)); |
| for (int pct = 1; pct <= 100; pct++) { |
| int expected = (pct - 1) / 10 * 10; |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::MINUTE)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, |
| IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::HOUR)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::ALLTIME)); |
| } |
| |
| for (int b = 1; (b + 1) < hist.getNumBuckets(); ++b) { |
| EXPECT_EQ(600, hist.getBucket(b).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(6000, hist.getBucket(b).count(IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(36000, hist.getBucket(b).count(IntMTMHTS::HOUR)); |
| EXPECT_EQ(36000, hist.getBucket(b).count(IntMTMHTS::ALLTIME)); |
| } |
| EXPECT_EQ(0, hist.getBucket(0).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(hist.getNumBuckets() - 1).count( |
| IntMTMHTS::MINUTE)); |
| } |
| |
| // ----------------- |
| |
| { |
| TimeseriesHistogram<int> hist(10, 0, 100, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| for (int now = 0; now < 3600; now++) { |
| for (int i = 0; i < 100; i++) { |
| hist.addValue(seconds(now), i, 2); // adds each item 2 times |
| } |
| } |
| |
| hist.update(seconds(3599)); |
| for (int pct = 1; pct <= 100; pct++) { |
| int expected = (pct - 1) / 10 * 10; |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::MINUTE)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, |
| IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::HOUR)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::ALLTIME)); |
| } |
| |
| for (int b = 1; (b + 1) < hist.getNumBuckets(); ++b) { |
| EXPECT_EQ(600 * 2, hist.getBucket(b).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(6000 * 2, hist.getBucket(b).count(IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(36000 * 2, hist.getBucket(b).count(IntMTMHTS::HOUR)); |
| EXPECT_EQ(36000 * 2, hist.getBucket(b).count(IntMTMHTS::ALLTIME)); |
| } |
| EXPECT_EQ(0, hist.getBucket(0).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(hist.getNumBuckets() - 1).count( |
| IntMTMHTS::MINUTE)); |
| } |
| |
| // ----------------- |
| |
| { |
| TimeseriesHistogram<int> hist(10, 0, 100, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| for (int now = 0; now < 3600; now++) { |
| for (int i = 0; i < 50; i++) { |
| hist.addValue(seconds(now), i * 2, 2); // adds each item 2 times |
| } |
| } |
| |
| hist.update(seconds(3599)); |
| for (int pct = 1; pct <= 100; pct++) { |
| int expected = (pct - 1) / 10 * 10; |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::MINUTE)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, |
| IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::HOUR)); |
| EXPECT_EQ(expected, hist.getPercentileBucketMin(pct, IntMTMHTS::ALLTIME)); |
| } |
| |
| EXPECT_EQ(0, hist.getBucket(0).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(0).count(IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(0).count(IntMTMHTS::HOUR)); |
| EXPECT_EQ(0, hist.getBucket(0).count(IntMTMHTS::ALLTIME)); |
| EXPECT_EQ(0, hist.getBucket(hist.getNumBuckets() - 1).count( |
| IntMTMHTS::MINUTE)); |
| EXPECT_EQ(0, |
| hist.getBucket(hist.getNumBuckets() - 1). |
| count(IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(0, hist.getBucket(hist.getNumBuckets() - 1).count( |
| IntMTMHTS::HOUR)); |
| EXPECT_EQ(0, |
| hist.getBucket(hist.getNumBuckets() - 1).count( |
| IntMTMHTS::ALLTIME)); |
| |
| for (int b = 1; (b + 1) < hist.getNumBuckets(); ++b) { |
| EXPECT_EQ(600, hist.getBucket(b).count(IntMTMHTS::MINUTE)); |
| EXPECT_EQ(6000, hist.getBucket(b).count(IntMTMHTS::TEN_MINUTE)); |
| EXPECT_EQ(36000, hist.getBucket(b).count(IntMTMHTS::HOUR)); |
| EXPECT_EQ(36000, hist.getBucket(b).count(IntMTMHTS::ALLTIME)); |
| } |
| |
| for (int i = 0; i < 100; ++i) { |
| hist.addValue(seconds(3599), 200 + i); |
| } |
| hist.update(seconds(3599)); |
| EXPECT_EQ(100, |
| hist.getBucket(hist.getNumBuckets() - 1).count( |
| IntMTMHTS::ALLTIME)); |
| |
| } |
| } |
| |
| TEST(TimeseriesHistogram, QueryByInterval) { |
| TimeseriesHistogram<int> mhts(8, 8, 120, |
| MultiLevelTimeSeries<int>( |
| 60, IntMHTS::NUM_LEVELS, |
| IntMHTS::kDurations)); |
| |
| mhts.update(seconds(0)); |
| |
| int curTime; |
| for (curTime = 0; curTime < 7200; curTime++) { |
| mhts.addValue(seconds(curTime), 1); |
| } |
| for (curTime = 7200; curTime < 7200 + 3540; curTime++) { |
| mhts.addValue(seconds(curTime), 10); |
| } |
| for (curTime = 7200 + 3540; curTime < 7200 + 3600; curTime++) { |
| mhts.addValue(seconds(curTime), 100); |
| } |
| |
| mhts.update(seconds(7200 + 3600 - 1)); |
| |
| struct TimeInterval { |
| TimeInterval(int s, int e) |
| : start(s), end(e) {} |
| |
| std::chrono::seconds start; |
| std::chrono::seconds end; |
| }; |
| TimeInterval intervals[12] = { |
| { curTime - 60, curTime }, |
| { curTime - 3600, curTime }, |
| { curTime - 7200, curTime }, |
| { curTime - 3600, curTime - 60 }, |
| { curTime - 7200, curTime - 60 }, |
| { curTime - 7200, curTime - 3600 }, |
| { curTime - 50, curTime - 20 }, |
| { curTime - 3020, curTime - 20 }, |
| { curTime - 7200, curTime - 20 }, |
| { curTime - 3000, curTime - 1000 }, |
| { curTime - 7200, curTime - 1000 }, |
| { curTime - 7200, curTime - 3600 }, |
| }; |
| |
| int expectedSums[12] = { |
| 6000, 41400, 32400, 35400, 32129, 16200, 3000, 33600, 32308, 20000, 27899, |
| 16200 |
| }; |
| |
| int expectedCounts[12] = { |
| 60, 3600, 7200, 3540, 7139, 3600, 30, 3000, 7178, 2000, 6199, 3600 |
| }; |
| |
| // The first 7200 values added all fell below the histogram minimum, |
| // and went into the bucket that tracks all of the too-small values. |
| // This bucket reports a minimum value of the smallest possible integer. |
| int belowMinBucket = std::numeric_limits<int>::min(); |
| |
| int expectedValues[12][3] = { |
| {96, 96, 96}, |
| { 8, 8, 96}, |
| { belowMinBucket, belowMinBucket, 8}, // alltime |
| { 8, 8, 8}, |
| { belowMinBucket, belowMinBucket, 8}, // alltime |
| { belowMinBucket, belowMinBucket, 8}, // alltime |
| {96, 96, 96}, |
| { 8, 8, 96}, |
| { belowMinBucket, belowMinBucket, 8}, // alltime |
| { 8, 8, 8}, |
| { belowMinBucket, belowMinBucket, 8}, // alltime |
| { belowMinBucket, belowMinBucket, 8} // alltime |
| }; |
| |
| for (int i = 0; i < 12; i++) { |
| const auto& itv = intervals[i]; |
| int s = mhts.sum(itv.start, itv.end); |
| EXPECT_EQ(expectedSums[i], s); |
| |
| int c = mhts.count(itv.start, itv.end); |
| EXPECT_EQ(expectedCounts[i], c); |
| } |
| |
| // 3 levels |
| for (int i = 1; i <= 100; i++) { |
| EXPECT_EQ(96, mhts.getPercentileBucketMin(i, 0)); |
| EXPECT_EQ(96, mhts.getPercentileBucketMin(i, seconds(curTime - 60), |
| seconds(curTime))); |
| EXPECT_EQ(8, mhts.getPercentileBucketMin(i, seconds(curTime - 3540), |
| seconds(curTime - 60))); |
| } |
| |
| EXPECT_EQ(8, mhts.getPercentileBucketMin(1, 1)); |
| EXPECT_EQ(8, mhts.getPercentileBucketMin(98, 1)); |
| EXPECT_EQ(96, mhts.getPercentileBucketMin(99, 1)); |
| EXPECT_EQ(96, mhts.getPercentileBucketMin(100, 1)); |
| |
| EXPECT_EQ(belowMinBucket, mhts.getPercentileBucketMin(1, 2)); |
| EXPECT_EQ(belowMinBucket, mhts.getPercentileBucketMin(66, 2)); |
| EXPECT_EQ(8, mhts.getPercentileBucketMin(67, 2)); |
| EXPECT_EQ(8, mhts.getPercentileBucketMin(99, 2)); |
| EXPECT_EQ(96, mhts.getPercentileBucketMin(100, 2)); |
| |
| // 0 is currently the value for bucket 0 (below min) |
| for (int i = 0; i < 12; i++) { |
| const auto& itv = intervals[i]; |
| int v = mhts.getPercentileBucketMin(1, itv.start, itv.end); |
| EXPECT_EQ(expectedValues[i][0], v); |
| |
| v = mhts.getPercentileBucketMin(50, itv.start, itv.end); |
| EXPECT_EQ(expectedValues[i][1], v); |
| |
| v = mhts.getPercentileBucketMin(99, itv.start, itv.end); |
| EXPECT_EQ(expectedValues[i][2], v); |
| } |
| |
| for (int i = 0; i < 12; i++) { |
| const auto& itv = intervals[i]; |
| int c = mhts.count(itv.start, itv.end); |
| // Some of the older intervals that fall in the alltime bucket |
| // are off by 1 or 2 in their estimated counts. |
| size_t tolerance = 0; |
| if (itv.start <= seconds(curTime - 7200)) { |
| tolerance = 2; |
| } else if (itv.start <= seconds(curTime - 3000)) { |
| tolerance = 1; |
| } |
| size_t actualCount = (itv.end - itv.start).count(); |
| size_t estimatedCount = mhts.count(itv.start, itv.end); |
| EXPECT_GE(actualCount, estimatedCount); |
| EXPECT_LE(actualCount - tolerance, estimatedCount); |
| } |
| } |
| |
| TEST(TimeseriesHistogram, SingleUniqueValue) { |
| int values[] = {-1, 0, 500, 1000, 1500}; |
| for (int ii = 0; ii < 5; ++ii) { |
| int value = values[ii]; |
| TimeseriesHistogram<int> h(10, 0, 1000, |
| MultiLevelTimeSeries<int>( |
| 60, IntMTMHTS::NUM_LEVELS, |
| IntMTMHTS::kDurations)); |
| |
| const int kNumIters = 1000; |
| for (int jj = 0; jj < kNumIters; ++jj) { |
| h.addValue(seconds(time(nullptr)), value); |
| } |
| h.update(seconds(time(nullptr))); |
| // since we've only added one unique value, all percentiles should |
| // be that value |
| EXPECT_EQ(h.getPercentileEstimate(10, 0), value); |
| EXPECT_EQ(h.getPercentileEstimate(50, 0), value); |
| EXPECT_EQ(h.getPercentileEstimate(99, 0), value); |
| |
| // Things get trickier if there are multiple unique values. |
| const int kNewValue = 750; |
| for (int kk = 0; kk < 2*kNumIters; ++kk) { |
| h.addValue(seconds(time(nullptr)), kNewValue); |
| } |
| h.update(seconds(time(nullptr))); |
| EXPECT_NEAR(h.getPercentileEstimate(50, 0), kNewValue+5, 5); |
| if (value >= 0 && value <= 1000) { |
| // only do further testing if value is within our bucket range, |
| // else estimates can be wildly off |
| if (kNewValue > value) { |
| EXPECT_NEAR(h.getPercentileEstimate(10, 0), value+5, 5); |
| EXPECT_NEAR(h.getPercentileEstimate(99, 0), kNewValue+5, 5); |
| } else { |
| EXPECT_NEAR(h.getPercentileEstimate(10, 0), kNewValue+5, 5); |
| EXPECT_NEAR(h.getPercentileEstimate(99, 0), value+5, 5); |
| } |
| } |
| } |
| } |