Loading cmds/statsd/src/anomaly/DurationAnomalyTracker.h +2 −1 Original line number Original line Diff line number Diff line Loading @@ -72,7 +72,8 @@ protected: FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionExpiredAlarm); FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionExpiredAlarm); FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionFiredAlarm); FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionFiredAlarm); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp_UpdatedOnStop); }; }; } // namespace statsd } // namespace statsd Loading cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp +41 −8 Original line number Original line Diff line number Diff line Loading @@ -93,6 +93,7 @@ void MaxDurationTracker::noteStart(const HashableDimensionKey& key, bool conditi } else { } else { duration.state = DurationState::kStarted; duration.state = DurationState::kStarted; duration.lastStartTime = eventTime; duration.lastStartTime = eventTime; startAnomalyAlarm(eventTime); } } duration.startCount = 1; duration.startCount = 1; break; break; Loading @@ -116,12 +117,18 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ case DurationState::kStarted: { case DurationState::kStarted: { duration.startCount--; duration.startCount--; if (forceStop || !mNested || duration.startCount <= 0) { if (forceStop || !mNested || duration.startCount <= 0) { stopAnomalyAlarm(); duration.state = DurationState::kStopped; duration.state = DurationState::kStopped; int64_t durationTime = eventTime - duration.lastStartTime; int64_t durationTime = eventTime - duration.lastStartTime; VLOG("Max, key %s, Stop %lld %lld %lld", key.c_str(), VLOG("Max, key %s, Stop %lld %lld %lld", key.c_str(), (long long)duration.lastStartTime, (long long)eventTime, (long long)duration.lastStartTime, (long long)eventTime, (long long)durationTime); (long long)durationTime); duration.lastDuration += durationTime; duration.lastDuration += durationTime; if (anyStarted()) { // In case any other dimensions are still started, we need to keep the alarm // set. startAnomalyAlarm(eventTime); } VLOG(" record duration: %lld ", (long long)duration.lastDuration); VLOG(" record duration: %lld ", (long long)duration.lastDuration); } } break; break; Loading @@ -146,6 +153,15 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ } } } } bool MaxDurationTracker::anyStarted() { for (auto& pair : mInfos) { if (pair.second.state == kStarted) { return true; } } return false; } void MaxDurationTracker::noteStopAll(const uint64_t eventTime) { void MaxDurationTracker::noteStopAll(const uint64_t eventTime) { std::set<HashableDimensionKey> keys; std::set<HashableDimensionKey> keys; for (const auto& pair : mInfos) { for (const auto& pair : mInfos) { Loading Loading @@ -251,35 +267,52 @@ void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, b switch (it->second.state) { switch (it->second.state) { case kStarted: case kStarted: // if condition becomes false, kStarted -> kPaused. Record the current duration. // If condition becomes false, kStarted -> kPaused. Record the current duration and // stop anomaly alarm. if (!conditionMet) { if (!conditionMet) { stopAnomalyAlarm(); it->second.state = DurationState::kPaused; it->second.state = DurationState::kPaused; it->second.lastDuration += (timestamp - it->second.lastStartTime); it->second.lastDuration += (timestamp - it->second.lastStartTime); if (anyStarted()) { // In case any other dimensions are still started, we need to set the alarm. startAnomalyAlarm(timestamp); } VLOG("MaxDurationTracker Key: %s Started->Paused ", key.c_str()); VLOG("MaxDurationTracker Key: %s Started->Paused ", key.c_str()); } } break; break; case kStopped: case kStopped: // nothing to do if it's stopped. // Nothing to do if it's stopped. break; break; case kPaused: case kPaused: // if condition becomes true, kPaused -> kStarted. and the start time is the condition // If condition becomes true, kPaused -> kStarted. and the start time is the condition // change time. // change time. if (conditionMet) { if (conditionMet) { it->second.state = DurationState::kStarted; it->second.state = DurationState::kStarted; it->second.lastStartTime = timestamp; it->second.lastStartTime = timestamp; startAnomalyAlarm(timestamp); VLOG("MaxDurationTracker Key: %s Paused->Started", key.c_str()); VLOG("MaxDurationTracker Key: %s Paused->Started", key.c_str()); } } break; break; } } if (it->second.lastDuration > mDuration) { // Note that we don't update mDuration here since it's only updated during noteStop. mDuration = it->second.lastDuration; } } } int64_t MaxDurationTracker::predictAnomalyTimestampNs(const DurationAnomalyTracker& anomalyTracker, int64_t MaxDurationTracker::predictAnomalyTimestampNs(const DurationAnomalyTracker& anomalyTracker, const uint64_t currentTimestamp) const { const uint64_t currentTimestamp) const { ALOGE("Max duration producer does not support anomaly timestamp prediction!!!"); // The allowed time we can continue in the current state is the return currentTimestamp; // (anomaly threshold) - max(elapsed time of the started mInfos). int64_t maxElapsed = 0; for (auto it = mInfos.begin(); it != mInfos.end(); ++it) { if (it->second.state == DurationState::kStarted) { int64_t duration = it->second.lastDuration + (currentTimestamp - it->second.lastStartTime); if (duration > maxElapsed) { maxElapsed = duration; } } } int64_t threshold = anomalyTracker.getAnomalyThreshold(); return currentTimestamp + threshold - maxElapsed; } } void MaxDurationTracker::dumpStates(FILE* out, bool verbose) const { void MaxDurationTracker::dumpStates(FILE* out, bool verbose) const { Loading cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h +5 −0 Original line number Original line Diff line number Diff line Loading @@ -60,6 +60,9 @@ public: void dumpStates(FILE* out, bool verbose) const override; void dumpStates(FILE* out, bool verbose) const override; private: private: // Returns true if at least one of the mInfos is started. bool anyStarted(); std::unordered_map<HashableDimensionKey, DurationInfo> mInfos; std::unordered_map<HashableDimensionKey, DurationInfo> mInfos; void noteConditionChanged(const HashableDimensionKey& key, bool conditionMet, void noteConditionChanged(const HashableDimensionKey& key, bool conditionMet, Loading @@ -72,6 +75,8 @@ private: FRIEND_TEST(MaxDurationTrackerTest, TestCrossBucketBoundary); FRIEND_TEST(MaxDurationTrackerTest, TestCrossBucketBoundary); FRIEND_TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition); FRIEND_TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition); FRIEND_TEST(MaxDurationTrackerTest, TestStopAll); FRIEND_TEST(MaxDurationTrackerTest, TestStopAll); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp); }; }; } // namespace statsd } // namespace statsd Loading cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp +173 −13 Original line number Original line Diff line number Diff line Loading @@ -204,8 +204,53 @@ TEST(MaxDurationTrackerTest, TestCrossBucketBoundary_nested) { } } TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { const std::vector<HashableDimensionKey> conditionKey = {key1}; vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); ConditionKey conditionKey1; MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 1, "1"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; /** Start in first bucket, stop in second bucket. Condition turns on and off in the first bucket and again turns on and off in the second bucket. */ uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t eventStartTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC; uint64_t conditionStarts1 = bucketStartTimeNs + 11 * NS_PER_SEC; uint64_t conditionStops1 = bucketStartTimeNs + 14 * NS_PER_SEC; uint64_t conditionStarts2 = bucketStartTimeNs + bucketSizeNs + 5 * NS_PER_SEC; uint64_t conditionStops2 = conditionStarts2 + 10 * NS_PER_SEC; uint64_t eventStopTimeNs = conditionStops2 + 8 * NS_PER_SEC; int64_t metricId = 1; MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, 0, bucketStartTimeNs, bucketSizeNs, true, {}); EXPECT_TRUE(tracker.mAnomalyTrackers.empty()); tracker.noteStart(key1, false, eventStartTimeNs, conditionKey1); tracker.noteConditionChanged(key1, true, conditionStarts1); tracker.noteConditionChanged(key1, false, conditionStops1); unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1, &buckets); EXPECT_EQ(0U, buckets.size()); tracker.noteConditionChanged(key1, true, conditionStarts2); tracker.noteConditionChanged(key1, false, conditionStops2); tracker.noteStop(key1, eventStopTimeNs, false); tracker.flushIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + 1, &buckets); EXPECT_EQ(1U, buckets.size()); vector<DurationBucket> item = buckets.begin()->second; EXPECT_EQ(1UL, item.size()); EXPECT_EQ(13ULL * NS_PER_SEC, item[0].mDuration); } TEST(MaxDurationTrackerTest, TestAnomalyDetection) { const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; const HashableDimensionKey key1 = getMockedDimensionKey(TagId, 1, "1"); vector<Matcher> dimensionInCondition; vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); Loading @@ -214,34 +259,149 @@ TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; EXPECT_CALL(*wizard, query(_, conditionKey1, _, _)) // #4 .WillOnce(Return(ConditionState::kFalse)); unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketNum = 0; uint64_t bucketNum = 0; uint64_t eventStartTimeNs = bucketStartTimeNs + 1; uint64_t eventStartTimeNs = 13000000000; int64_t durationTimeNs = 2 * 1000; int64_t durationTimeNs = 2 * 1000; int64_t metricId = 1; int64_t metricId = 1; Alert alert; alert.set_id(101); alert.set_metric_id(1); alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); alert.set_num_buckets(2); const int32_t refPeriodSec = 45; alert.set_refractory_period_secs(refPeriodSec); sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey); MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, true, {}); true, {anomalyTracker}); EXPECT_TRUE(tracker.mAnomalyTrackers.empty()); tracker.noteStart(key1, true, eventStartTimeNs, conditionKey1); tracker.noteStart(key1, true, eventStartTimeNs, conditionKey1); sp<const AnomalyAlarm> alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ((long long)(53ULL * NS_PER_SEC), (long long)(alarm->timestampSec * NS_PER_SEC)); // Remove the anomaly alarm when the duration is no longer fully met. tracker.noteConditionChanged(key1, false, eventStartTimeNs + 15 * NS_PER_SEC); EXPECT_EQ(0U, anomalyTracker->mAlarms.size()); // Since the condition was off for 10 seconds, the anomaly should trigger 10 sec later. tracker.noteConditionChanged(key1, true, eventStartTimeNs + 25 * NS_PER_SEC); EXPECT_EQ(1U, anomalyTracker->mAlarms.size()); alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ((long long)(63ULL * NS_PER_SEC), (long long)(alarm->timestampSec * NS_PER_SEC)); } // This tests that we correctly compute the predicted time of an anomaly assuming that the current // state continues forward as-is. TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp) { const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; tracker.onSlicedConditionMayChange(eventStartTimeNs + 5); vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); tracker.noteStop(key1, eventStartTimeNs + durationTimeNs, false); ConditionKey conditionKey1; MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; ConditionKey conditionKey2; conditionKey2[StringToId("APP_BACKGROUND")] = {getMockedDimensionKey(TagId, 4, "2")}; tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1, &buckets); unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; EXPECT_TRUE(buckets.find(eventKey) != buckets.end()); EXPECT_EQ(1u, buckets[eventKey].size()); /** EXPECT_EQ(5ULL, buckets[eventKey][0].mDuration); * Suppose we have two sub-dimensions that we're taking the MAX over. In the first of these * nested dimensions, we enter the pause state after 3 seconds. When we resume, the second * dimension has already been running for 4 seconds. Thus, we have 40-4=36 seconds remaining * before we trigger the anomaly. */ uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketNum = 0; uint64_t eventStartTimeNs = bucketStartTimeNs + 5 * NS_PER_SEC; // Condition is off at start. uint64_t conditionStarts1 = bucketStartTimeNs + 11 * NS_PER_SEC; uint64_t conditionStops1 = bucketStartTimeNs + 14 * NS_PER_SEC; uint64_t conditionStarts2 = bucketStartTimeNs + 20 * NS_PER_SEC; uint64_t eventStartTimeNs2 = conditionStarts2 - 4 * NS_PER_SEC; int64_t metricId = 1; Alert alert; alert.set_id(101); alert.set_metric_id(1); alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); alert.set_num_buckets(2); const int32_t refPeriodSec = 45; alert.set_refractory_period_secs(refPeriodSec); sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey); MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, true, {anomalyTracker}); tracker.noteStart(key1, false, eventStartTimeNs, conditionKey1); tracker.noteConditionChanged(key1, true, conditionStarts1); tracker.noteConditionChanged(key1, false, conditionStops1); tracker.noteStart(key2, true, eventStartTimeNs2, conditionKey2); // Condition is on already. tracker.noteConditionChanged(key1, true, conditionStarts2); EXPECT_EQ(1U, anomalyTracker->mAlarms.size()); auto alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ(conditionStarts2 + 36 * NS_PER_SEC, (unsigned long long)(alarm->timestampSec * NS_PER_SEC)); } // Suppose that within one tracker there are two dimensions A and B. // Suppose A starts, then B starts, and then A stops. We still need to set an anomaly based on the // elapsed duration of B. TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp_UpdatedOnStop) { const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); ConditionKey conditionKey1; MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; ConditionKey conditionKey2; conditionKey2[StringToId("APP_BACKGROUND")] = {getMockedDimensionKey(TagId, 4, "2")}; unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; /** * Suppose we have two sub-dimensions that we're taking the MAX over. In the first of these * nested dimensions, are started for 8 seconds. When we stop, the other nested dimension has * been started for 5 seconds. So we can only allow 35 more seconds from now. */ uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketNum = 0; uint64_t eventStartTimeNs1 = bucketStartTimeNs + 5 * NS_PER_SEC; // Condition is off at start. uint64_t eventStopTimeNs1 = bucketStartTimeNs + 13 * NS_PER_SEC; uint64_t eventStartTimeNs2 = bucketStartTimeNs + 8 * NS_PER_SEC; int64_t metricId = 1; Alert alert; alert.set_id(101); alert.set_metric_id(1); alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); alert.set_num_buckets(2); const int32_t refPeriodSec = 45; alert.set_refractory_period_secs(refPeriodSec); sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey); MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, true, {anomalyTracker}); tracker.noteStart(key1, true, eventStartTimeNs1, conditionKey1); tracker.noteStart(key2, true, eventStartTimeNs2, conditionKey2); tracker.noteStop(key1, eventStopTimeNs1, false); EXPECT_EQ(1U, anomalyTracker->mAlarms.size()); auto alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ(eventStopTimeNs1 + 35 * NS_PER_SEC, (unsigned long long)(alarm->timestampSec * NS_PER_SEC)); } } } // namespace statsd } // namespace statsd Loading Loading
cmds/statsd/src/anomaly/DurationAnomalyTracker.h +2 −1 Original line number Original line Diff line number Diff line Loading @@ -72,7 +72,8 @@ protected: FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionExpiredAlarm); FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionExpiredAlarm); FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionFiredAlarm); FRIEND_TEST(OringDurationTrackerTest, TestAnomalyDetectionFiredAlarm); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp_UpdatedOnStop); }; }; } // namespace statsd } // namespace statsd Loading
cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp +41 −8 Original line number Original line Diff line number Diff line Loading @@ -93,6 +93,7 @@ void MaxDurationTracker::noteStart(const HashableDimensionKey& key, bool conditi } else { } else { duration.state = DurationState::kStarted; duration.state = DurationState::kStarted; duration.lastStartTime = eventTime; duration.lastStartTime = eventTime; startAnomalyAlarm(eventTime); } } duration.startCount = 1; duration.startCount = 1; break; break; Loading @@ -116,12 +117,18 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ case DurationState::kStarted: { case DurationState::kStarted: { duration.startCount--; duration.startCount--; if (forceStop || !mNested || duration.startCount <= 0) { if (forceStop || !mNested || duration.startCount <= 0) { stopAnomalyAlarm(); duration.state = DurationState::kStopped; duration.state = DurationState::kStopped; int64_t durationTime = eventTime - duration.lastStartTime; int64_t durationTime = eventTime - duration.lastStartTime; VLOG("Max, key %s, Stop %lld %lld %lld", key.c_str(), VLOG("Max, key %s, Stop %lld %lld %lld", key.c_str(), (long long)duration.lastStartTime, (long long)eventTime, (long long)duration.lastStartTime, (long long)eventTime, (long long)durationTime); (long long)durationTime); duration.lastDuration += durationTime; duration.lastDuration += durationTime; if (anyStarted()) { // In case any other dimensions are still started, we need to keep the alarm // set. startAnomalyAlarm(eventTime); } VLOG(" record duration: %lld ", (long long)duration.lastDuration); VLOG(" record duration: %lld ", (long long)duration.lastDuration); } } break; break; Loading @@ -146,6 +153,15 @@ void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const uint64_ } } } } bool MaxDurationTracker::anyStarted() { for (auto& pair : mInfos) { if (pair.second.state == kStarted) { return true; } } return false; } void MaxDurationTracker::noteStopAll(const uint64_t eventTime) { void MaxDurationTracker::noteStopAll(const uint64_t eventTime) { std::set<HashableDimensionKey> keys; std::set<HashableDimensionKey> keys; for (const auto& pair : mInfos) { for (const auto& pair : mInfos) { Loading Loading @@ -251,35 +267,52 @@ void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, b switch (it->second.state) { switch (it->second.state) { case kStarted: case kStarted: // if condition becomes false, kStarted -> kPaused. Record the current duration. // If condition becomes false, kStarted -> kPaused. Record the current duration and // stop anomaly alarm. if (!conditionMet) { if (!conditionMet) { stopAnomalyAlarm(); it->second.state = DurationState::kPaused; it->second.state = DurationState::kPaused; it->second.lastDuration += (timestamp - it->second.lastStartTime); it->second.lastDuration += (timestamp - it->second.lastStartTime); if (anyStarted()) { // In case any other dimensions are still started, we need to set the alarm. startAnomalyAlarm(timestamp); } VLOG("MaxDurationTracker Key: %s Started->Paused ", key.c_str()); VLOG("MaxDurationTracker Key: %s Started->Paused ", key.c_str()); } } break; break; case kStopped: case kStopped: // nothing to do if it's stopped. // Nothing to do if it's stopped. break; break; case kPaused: case kPaused: // if condition becomes true, kPaused -> kStarted. and the start time is the condition // If condition becomes true, kPaused -> kStarted. and the start time is the condition // change time. // change time. if (conditionMet) { if (conditionMet) { it->second.state = DurationState::kStarted; it->second.state = DurationState::kStarted; it->second.lastStartTime = timestamp; it->second.lastStartTime = timestamp; startAnomalyAlarm(timestamp); VLOG("MaxDurationTracker Key: %s Paused->Started", key.c_str()); VLOG("MaxDurationTracker Key: %s Paused->Started", key.c_str()); } } break; break; } } if (it->second.lastDuration > mDuration) { // Note that we don't update mDuration here since it's only updated during noteStop. mDuration = it->second.lastDuration; } } } int64_t MaxDurationTracker::predictAnomalyTimestampNs(const DurationAnomalyTracker& anomalyTracker, int64_t MaxDurationTracker::predictAnomalyTimestampNs(const DurationAnomalyTracker& anomalyTracker, const uint64_t currentTimestamp) const { const uint64_t currentTimestamp) const { ALOGE("Max duration producer does not support anomaly timestamp prediction!!!"); // The allowed time we can continue in the current state is the return currentTimestamp; // (anomaly threshold) - max(elapsed time of the started mInfos). int64_t maxElapsed = 0; for (auto it = mInfos.begin(); it != mInfos.end(); ++it) { if (it->second.state == DurationState::kStarted) { int64_t duration = it->second.lastDuration + (currentTimestamp - it->second.lastStartTime); if (duration > maxElapsed) { maxElapsed = duration; } } } int64_t threshold = anomalyTracker.getAnomalyThreshold(); return currentTimestamp + threshold - maxElapsed; } } void MaxDurationTracker::dumpStates(FILE* out, bool verbose) const { void MaxDurationTracker::dumpStates(FILE* out, bool verbose) const { Loading
cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.h +5 −0 Original line number Original line Diff line number Diff line Loading @@ -60,6 +60,9 @@ public: void dumpStates(FILE* out, bool verbose) const override; void dumpStates(FILE* out, bool verbose) const override; private: private: // Returns true if at least one of the mInfos is started. bool anyStarted(); std::unordered_map<HashableDimensionKey, DurationInfo> mInfos; std::unordered_map<HashableDimensionKey, DurationInfo> mInfos; void noteConditionChanged(const HashableDimensionKey& key, bool conditionMet, void noteConditionChanged(const HashableDimensionKey& key, bool conditionMet, Loading @@ -72,6 +75,8 @@ private: FRIEND_TEST(MaxDurationTrackerTest, TestCrossBucketBoundary); FRIEND_TEST(MaxDurationTrackerTest, TestCrossBucketBoundary); FRIEND_TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition); FRIEND_TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition); FRIEND_TEST(MaxDurationTrackerTest, TestStopAll); FRIEND_TEST(MaxDurationTrackerTest, TestStopAll); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyDetection); FRIEND_TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp); }; }; } // namespace statsd } // namespace statsd Loading
cmds/statsd/tests/metrics/MaxDurationTracker_test.cpp +173 −13 Original line number Original line Diff line number Diff line Loading @@ -204,8 +204,53 @@ TEST(MaxDurationTrackerTest, TestCrossBucketBoundary_nested) { } } TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { const std::vector<HashableDimensionKey> conditionKey = {key1}; vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); ConditionKey conditionKey1; MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 1, "1"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; /** Start in first bucket, stop in second bucket. Condition turns on and off in the first bucket and again turns on and off in the second bucket. */ uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t eventStartTimeNs = bucketStartTimeNs + 1 * NS_PER_SEC; uint64_t conditionStarts1 = bucketStartTimeNs + 11 * NS_PER_SEC; uint64_t conditionStops1 = bucketStartTimeNs + 14 * NS_PER_SEC; uint64_t conditionStarts2 = bucketStartTimeNs + bucketSizeNs + 5 * NS_PER_SEC; uint64_t conditionStops2 = conditionStarts2 + 10 * NS_PER_SEC; uint64_t eventStopTimeNs = conditionStops2 + 8 * NS_PER_SEC; int64_t metricId = 1; MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, 0, bucketStartTimeNs, bucketSizeNs, true, {}); EXPECT_TRUE(tracker.mAnomalyTrackers.empty()); tracker.noteStart(key1, false, eventStartTimeNs, conditionKey1); tracker.noteConditionChanged(key1, true, conditionStarts1); tracker.noteConditionChanged(key1, false, conditionStops1); unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1, &buckets); EXPECT_EQ(0U, buckets.size()); tracker.noteConditionChanged(key1, true, conditionStarts2); tracker.noteConditionChanged(key1, false, conditionStops2); tracker.noteStop(key1, eventStopTimeNs, false); tracker.flushIfNeeded(bucketStartTimeNs + 2 * bucketSizeNs + 1, &buckets); EXPECT_EQ(1U, buckets.size()); vector<DurationBucket> item = buckets.begin()->second; EXPECT_EQ(1UL, item.size()); EXPECT_EQ(13ULL * NS_PER_SEC, item[0].mDuration); } TEST(MaxDurationTrackerTest, TestAnomalyDetection) { const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; const HashableDimensionKey key1 = getMockedDimensionKey(TagId, 1, "1"); vector<Matcher> dimensionInCondition; vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); Loading @@ -214,34 +259,149 @@ TEST(MaxDurationTrackerTest, TestMaxDurationWithCondition) { MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; EXPECT_CALL(*wizard, query(_, conditionKey1, _, _)) // #4 .WillOnce(Return(ConditionState::kFalse)); unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketNum = 0; uint64_t bucketNum = 0; uint64_t eventStartTimeNs = bucketStartTimeNs + 1; uint64_t eventStartTimeNs = 13000000000; int64_t durationTimeNs = 2 * 1000; int64_t durationTimeNs = 2 * 1000; int64_t metricId = 1; int64_t metricId = 1; Alert alert; alert.set_id(101); alert.set_metric_id(1); alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); alert.set_num_buckets(2); const int32_t refPeriodSec = 45; alert.set_refractory_period_secs(refPeriodSec); sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey); MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, true, {}); true, {anomalyTracker}); EXPECT_TRUE(tracker.mAnomalyTrackers.empty()); tracker.noteStart(key1, true, eventStartTimeNs, conditionKey1); tracker.noteStart(key1, true, eventStartTimeNs, conditionKey1); sp<const AnomalyAlarm> alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ((long long)(53ULL * NS_PER_SEC), (long long)(alarm->timestampSec * NS_PER_SEC)); // Remove the anomaly alarm when the duration is no longer fully met. tracker.noteConditionChanged(key1, false, eventStartTimeNs + 15 * NS_PER_SEC); EXPECT_EQ(0U, anomalyTracker->mAlarms.size()); // Since the condition was off for 10 seconds, the anomaly should trigger 10 sec later. tracker.noteConditionChanged(key1, true, eventStartTimeNs + 25 * NS_PER_SEC); EXPECT_EQ(1U, anomalyTracker->mAlarms.size()); alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ((long long)(63ULL * NS_PER_SEC), (long long)(alarm->timestampSec * NS_PER_SEC)); } // This tests that we correctly compute the predicted time of an anomaly assuming that the current // state continues forward as-is. TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp) { const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; tracker.onSlicedConditionMayChange(eventStartTimeNs + 5); vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); tracker.noteStop(key1, eventStartTimeNs + durationTimeNs, false); ConditionKey conditionKey1; MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; ConditionKey conditionKey2; conditionKey2[StringToId("APP_BACKGROUND")] = {getMockedDimensionKey(TagId, 4, "2")}; tracker.flushIfNeeded(bucketStartTimeNs + bucketSizeNs + 1, &buckets); unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; EXPECT_TRUE(buckets.find(eventKey) != buckets.end()); EXPECT_EQ(1u, buckets[eventKey].size()); /** EXPECT_EQ(5ULL, buckets[eventKey][0].mDuration); * Suppose we have two sub-dimensions that we're taking the MAX over. In the first of these * nested dimensions, we enter the pause state after 3 seconds. When we resume, the second * dimension has already been running for 4 seconds. Thus, we have 40-4=36 seconds remaining * before we trigger the anomaly. */ uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketNum = 0; uint64_t eventStartTimeNs = bucketStartTimeNs + 5 * NS_PER_SEC; // Condition is off at start. uint64_t conditionStarts1 = bucketStartTimeNs + 11 * NS_PER_SEC; uint64_t conditionStops1 = bucketStartTimeNs + 14 * NS_PER_SEC; uint64_t conditionStarts2 = bucketStartTimeNs + 20 * NS_PER_SEC; uint64_t eventStartTimeNs2 = conditionStarts2 - 4 * NS_PER_SEC; int64_t metricId = 1; Alert alert; alert.set_id(101); alert.set_metric_id(1); alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); alert.set_num_buckets(2); const int32_t refPeriodSec = 45; alert.set_refractory_period_secs(refPeriodSec); sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey); MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, true, {anomalyTracker}); tracker.noteStart(key1, false, eventStartTimeNs, conditionKey1); tracker.noteConditionChanged(key1, true, conditionStarts1); tracker.noteConditionChanged(key1, false, conditionStops1); tracker.noteStart(key2, true, eventStartTimeNs2, conditionKey2); // Condition is on already. tracker.noteConditionChanged(key1, true, conditionStarts2); EXPECT_EQ(1U, anomalyTracker->mAlarms.size()); auto alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ(conditionStarts2 + 36 * NS_PER_SEC, (unsigned long long)(alarm->timestampSec * NS_PER_SEC)); } // Suppose that within one tracker there are two dimensions A and B. // Suppose A starts, then B starts, and then A stops. We still need to set an anomaly based on the // elapsed duration of B. TEST(MaxDurationTrackerTest, TestAnomalyPredictedTimestamp_UpdatedOnStop) { const std::vector<HashableDimensionKey> conditionKey = {getMockedDimensionKey(TagId, 4, "1")}; vector<Matcher> dimensionInCondition; sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>(); ConditionKey conditionKey1; MetricDimensionKey eventKey = getMockedMetricDimensionKey(TagId, 2, "maps"); conditionKey1[StringToId("APP_BACKGROUND")] = conditionKey; ConditionKey conditionKey2; conditionKey2[StringToId("APP_BACKGROUND")] = {getMockedDimensionKey(TagId, 4, "2")}; unordered_map<MetricDimensionKey, vector<DurationBucket>> buckets; /** * Suppose we have two sub-dimensions that we're taking the MAX over. In the first of these * nested dimensions, are started for 8 seconds. When we stop, the other nested dimension has * been started for 5 seconds. So we can only allow 35 more seconds from now. */ uint64_t bucketSizeNs = 30 * 1000 * 1000 * 1000LL; uint64_t bucketStartTimeNs = 10000000000; uint64_t bucketEndTimeNs = bucketStartTimeNs + bucketSizeNs; uint64_t bucketNum = 0; uint64_t eventStartTimeNs1 = bucketStartTimeNs + 5 * NS_PER_SEC; // Condition is off at start. uint64_t eventStopTimeNs1 = bucketStartTimeNs + 13 * NS_PER_SEC; uint64_t eventStartTimeNs2 = bucketStartTimeNs + 8 * NS_PER_SEC; int64_t metricId = 1; Alert alert; alert.set_id(101); alert.set_metric_id(1); alert.set_trigger_if_sum_gt(40 * NS_PER_SEC); alert.set_num_buckets(2); const int32_t refPeriodSec = 45; alert.set_refractory_period_secs(refPeriodSec); sp<DurationAnomalyTracker> anomalyTracker = new DurationAnomalyTracker(alert, kConfigKey); MaxDurationTracker tracker(kConfigKey, metricId, eventKey, wizard, 1, dimensionInCondition, false, bucketStartTimeNs, bucketNum, bucketStartTimeNs, bucketSizeNs, true, {anomalyTracker}); tracker.noteStart(key1, true, eventStartTimeNs1, conditionKey1); tracker.noteStart(key2, true, eventStartTimeNs2, conditionKey2); tracker.noteStop(key1, eventStopTimeNs1, false); EXPECT_EQ(1U, anomalyTracker->mAlarms.size()); auto alarm = anomalyTracker->mAlarms.begin()->second; EXPECT_EQ(eventStopTimeNs1 + 35 * NS_PER_SEC, (unsigned long long)(alarm->timestampSec * NS_PER_SEC)); } } } // namespace statsd } // namespace statsd Loading