/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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.
*/
#define DEBUG false
#include "Log.h"
#include "MaxDurationTracker.h"
#include "guardrail/StatsdStats.h"
namespace android {
namespace os {
namespace statsd {
MaxDurationTracker::MaxDurationTracker(const ConfigKey& key, const int64_t& id,
const MetricDimensionKey& eventKey,
sp<ConditionWizard> wizard, int conditionIndex,
const vector<Matcher>& dimensionInCondition, bool nesting,
int64_t currentBucketStartNs, int64_t currentBucketNum,
int64_t startTimeNs, int64_t bucketSizeNs,
bool conditionSliced, bool fullLink,
const vector<sp<DurationAnomalyTracker>>& anomalyTrackers)
: DurationTracker(key, id, eventKey, wizard, conditionIndex, dimensionInCondition, nesting,
currentBucketStartNs, currentBucketNum, startTimeNs, bucketSizeNs,
conditionSliced, fullLink, anomalyTrackers) {
if (mWizard != nullptr) {
mSameConditionDimensionsInTracker =
mWizard->equalOutputDimensions(conditionIndex, mDimensionInCondition);
}
}
unique_ptr<DurationTracker> MaxDurationTracker::clone(const int64_t eventTime) {
auto clonedTracker = make_unique<MaxDurationTracker>(*this);
for (auto it = clonedTracker->mInfos.begin(); it != clonedTracker->mInfos.end();) {
if (it->second.state != kStopped) {
it->second.lastStartTime = eventTime;
it->second.lastDuration = 0;
it++;
} else {
it = clonedTracker->mInfos.erase(it);
}
}
if (clonedTracker->mInfos.empty()) {
return nullptr;
} else {
return clonedTracker;
}
}
bool MaxDurationTracker::hitGuardRail(const HashableDimensionKey& newKey) {
// ===========GuardRail==============
if (mInfos.find(newKey) != mInfos.end()) {
// if the key existed, we are good!
return false;
}
// 1. Report the tuple count if the tuple count > soft limit
if (mInfos.size() > StatsdStats::kDimensionKeySizeSoftLimit - 1) {
size_t newTupleCount = mInfos.size() + 1;
StatsdStats::getInstance().noteMetricDimensionSize(mConfigKey, mTrackerId, newTupleCount);
// 2. Don't add more tuples, we are above the allowed threshold. Drop the data.
if (newTupleCount > StatsdStats::kDimensionKeySizeHardLimit) {
ALOGE("MaxDurTracker %lld dropping data for dimension key %s",
(long long)mTrackerId, newKey.toString().c_str());
return true;
}
}
return false;
}
void MaxDurationTracker::noteStart(const HashableDimensionKey& key, bool condition,
const int64_t eventTime, const ConditionKey& conditionKey) {
// this will construct a new DurationInfo if this key didn't exist.
if (hitGuardRail(key)) {
return;
}
DurationInfo& duration = mInfos[key];
if (mConditionSliced) {
duration.conditionKeys = conditionKey;
}
VLOG("MaxDuration: key %s start condition %d", key.toString().c_str(), condition);
switch (duration.state) {
case kStarted:
duration.startCount++;
break;
case kPaused:
duration.startCount++;
break;
case kStopped:
if (!condition) {
// event started, but we need to wait for the condition to become true.
duration.state = DurationState::kPaused;
} else {
duration.state = DurationState::kStarted;
duration.lastStartTime = eventTime;
startAnomalyAlarm(eventTime);
}
duration.startCount = 1;
break;
}
}
void MaxDurationTracker::noteStop(const HashableDimensionKey& key, const int64_t eventTime,
bool forceStop) {
VLOG("MaxDuration: key %s stop", key.toString().c_str());
if (mInfos.find(key) == mInfos.end()) {
// we didn't see a start event before. do nothing.
return;
}
DurationInfo& duration = mInfos[key];
switch (duration.state) {
case DurationState::kStopped:
// already stopped, do nothing.
break;
case DurationState::kStarted: {
duration.startCount--;
if (forceStop || !mNested || duration.startCount <= 0) {
stopAnomalyAlarm(eventTime);
duration.state = DurationState::kStopped;
int64_t durationTime = eventTime - duration.lastStartTime;
VLOG("Max, key %s, Stop %lld %lld %lld", key.toString().c_str(),
(long long)duration.lastStartTime, (long long)eventTime,
(long long)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);
}
break;
}
case DurationState::kPaused: {
duration.startCount--;
if (forceStop || !mNested || duration.startCount <= 0) {
duration.state = DurationState::kStopped;
}
break;
}
}
if (duration.lastDuration > mDuration) {
mDuration = duration.lastDuration;
VLOG("Max: new max duration: %lld", (long long)mDuration);
}
// Once an atom duration ends, we erase it. Next time, if we see another atom event with the
// same name, they are still considered as different atom durations.
if (duration.state == DurationState::kStopped) {
mInfos.erase(key);
}
}
bool MaxDurationTracker::anyStarted() {
for (auto& pair : mInfos) {
if (pair.second.state == kStarted) {
return true;
}
}
return false;
}
void MaxDurationTracker::noteStopAll(const int64_t eventTime) {
std::set<HashableDimensionKey> keys;
for (const auto& pair : mInfos) {
keys.insert(pair.first);
}
for (auto& key : keys) {
noteStop(key, eventTime, true);
}
}
bool MaxDurationTracker::flushCurrentBucket(
const int64_t& eventTimeNs,
std::unordered_map<MetricDimensionKey, std::vector<DurationBucket>>* output) {
VLOG("MaxDurationTracker flushing.....");
// adjust the bucket start time
int numBucketsForward = 0;
int64_t fullBucketEnd = getCurrentBucketEndTimeNs();
int64_t currentBucketEndTimeNs;
if (eventTimeNs >= fullBucketEnd) {
numBucketsForward = 1 + (eventTimeNs - fullBucketEnd) / mBucketSizeNs;
currentBucketEndTimeNs = fullBucketEnd;
} else {
// This must be a partial bucket.
currentBucketEndTimeNs = eventTimeNs;
}
bool hasPendingEvent =
false; // has either a kStarted or kPaused event across bucket boundaries
// meaning we need to carry them over to the new bucket.
for (auto it = mInfos.begin(); it != mInfos.end();) {
if (it->second.state == DurationState::kStopped) {
// No need to keep buckets for events that were stopped before.
it = mInfos.erase(it);
} else {
++it;
hasPendingEvent = true;
}
}
// mDuration is updated in noteStop to the maximum duration that ended in the current bucket.
if (mDuration != 0) {
DurationBucket info;
info.mBucketStartNs = mCurrentBucketStartTimeNs;
info.mBucketEndNs = currentBucketEndTimeNs;
info.mDuration = mDuration;
(*output)[mEventKey].push_back(info);
VLOG(" final duration for last bucket: %lld", (long long)mDuration);
}
if (numBucketsForward > 0) {
mCurrentBucketStartTimeNs = fullBucketEnd + (numBucketsForward - 1) * mBucketSizeNs;
mCurrentBucketNum += numBucketsForward;
} else { // We must be forming a partial bucket.
mCurrentBucketStartTimeNs = eventTimeNs;
}
mDuration = 0;
// If this tracker has no pending events, tell owner to remove.
return !hasPendingEvent;
}
bool MaxDurationTracker::flushIfNeeded(
int64_t eventTimeNs, unordered_map<MetricDimensionKey, vector<DurationBucket>>* output) {
if (eventTimeNs < getCurrentBucketEndTimeNs()) {
return false;
}
return flushCurrentBucket(eventTimeNs, output);
}
void MaxDurationTracker::onSlicedConditionMayChange(bool overallCondition,
const int64_t timestamp) {
// Now for each of the on-going event, check if the condition has changed for them.
for (auto& pair : mInfos) {
if (pair.second.state == kStopped) {
continue;
}
std::unordered_set<HashableDimensionKey> conditionDimensionKeySet;
ConditionState conditionState = mWizard->query(
mConditionTrackerIndex, pair.second.conditionKeys, mDimensionInCondition,
!mSameConditionDimensionsInTracker,
!mHasLinksToAllConditionDimensionsInTracker,
&conditionDimensionKeySet);
bool conditionMet =
(conditionState == ConditionState::kTrue) &&
(mDimensionInCondition.size() == 0 ||
conditionDimensionKeySet.find(mEventKey.getDimensionKeyInCondition()) !=
conditionDimensionKeySet.end());
VLOG("key: %s, condition: %d", pair.first.toString().c_str(), conditionMet);
noteConditionChanged(pair.first, conditionMet, timestamp);
}
}
void MaxDurationTracker::onConditionChanged(bool condition, const int64_t timestamp) {
for (auto& pair : mInfos) {
noteConditionChanged(pair.first, condition, timestamp);
}
}
void MaxDurationTracker::noteConditionChanged(const HashableDimensionKey& key, bool conditionMet,
const int64_t timestamp) {
auto it = mInfos.find(key);
if (it == mInfos.end()) {
return;
}
switch (it->second.state) {
case kStarted:
// If condition becomes false, kStarted -> kPaused. Record the current duration and
// stop anomaly alarm.
if (!conditionMet) {
stopAnomalyAlarm(timestamp);
it->second.state = DurationState::kPaused;
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.toString().c_str());
}
break;
case kStopped:
// Nothing to do if it's stopped.
break;
case kPaused:
// If condition becomes true, kPaused -> kStarted. and the start time is the condition
// change time.
if (conditionMet) {
it->second.state = DurationState::kStarted;
it->second.lastStartTime = timestamp;
startAnomalyAlarm(timestamp);
VLOG("MaxDurationTracker Key: %s Paused->Started", key.toString().c_str());
}
break;
}
// Note that we don't update mDuration here since it's only updated during noteStop.
}
int64_t MaxDurationTracker::predictAnomalyTimestampNs(const DurationAnomalyTracker& anomalyTracker,
const int64_t currentTimestamp) const {
// The allowed time we can continue in the current state is the
// (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 anomalyTimeNs = currentTimestamp + anomalyTracker.getAnomalyThreshold() - maxElapsed;
int64_t refractoryEndNs = anomalyTracker.getRefractoryPeriodEndsSec(mEventKey) * NS_PER_SEC;
return std::max(anomalyTimeNs, refractoryEndNs);
}
void MaxDurationTracker::dumpStates(FILE* out, bool verbose) const {
fprintf(out, "\t\t sub-durations %lu\n", (unsigned long)mInfos.size());
fprintf(out, "\t\t current duration %lld\n", (long long)mDuration);
}
} // namespace statsd
} // namespace os
} // namespace android