/* * Copyright (C) 2021 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. */ package com.android.internal.telephony; import android.annotation.IntRange; import android.annotation.NonNull; import android.os.SystemClock; import android.util.LongArrayQueue; import com.android.internal.annotations.VisibleForTesting; /** * Tracks whether an event occurs mNumOccurrences times within the time span mWindowSizeMillis. *

* Stores all events in memory, use a small number of required occurrences when using. */ public class SlidingWindowEventCounter { private final long mWindowSizeMillis; private final int mNumOccurrences; private final LongArrayQueue mTimestampQueueMillis; public SlidingWindowEventCounter(@IntRange(from = 0) final long windowSizeMillis, @IntRange(from = 2) final int numOccurrences) { if (windowSizeMillis < 0) { throw new IllegalArgumentException("windowSizeMillis must be greater or equal to 0"); } if (numOccurrences <= 1) { throw new IllegalArgumentException("numOccurrences must be greater than 1"); } mWindowSizeMillis = windowSizeMillis; mNumOccurrences = numOccurrences; mTimestampQueueMillis = new LongArrayQueue(numOccurrences); } /** * Increments the occurrence counter. * * Returns true if an event has occurred at least mNumOccurrences times within the * time span mWindowSizeMillis. */ public synchronized boolean addOccurrence() { return addOccurrence(SystemClock.elapsedRealtime()); } /** * Increments the occurrence counter. * * Returns true if an event has occurred at least mNumOccurrences times within the * time span mWindowSizeMillis. * @param timestampMillis */ public synchronized boolean addOccurrence(long timestampMillis) { mTimestampQueueMillis.addLast(timestampMillis); if (mTimestampQueueMillis.size() > mNumOccurrences) { mTimestampQueueMillis.removeFirst(); } return isInWindow(); } /** * Returns true if the conditions is satisfied. */ public synchronized boolean isInWindow() { return (mTimestampQueueMillis.size() == mNumOccurrences) && mTimestampQueueMillis.peekFirst() + mWindowSizeMillis > mTimestampQueueMillis.peekLast(); } @VisibleForTesting int getQueuedNumOccurrences() { return mTimestampQueueMillis.size(); } /** * @return the time span in ms of the sliding window. */ public synchronized long getWindowSizeMillis() { return mWindowSizeMillis; } /** * @return the least number of occurrences for {@link #isInWindow} to be true. */ public synchronized int getNumOccurrences() { return mNumOccurrences; } /** * Get the event frequency description. * * @return A string describing the anomaly event */ public @NonNull String getFrequencyString() { if (mWindowSizeMillis >= 1000L) { return mNumOccurrences + " times within " + mWindowSizeMillis / 1000L + " seconds"; } else { return mNumOccurrences + " times within " + mWindowSizeMillis + "ms"; } } @Override public String toString() { return String.format("SlidingWindowEventCounter=[windowSizeMillis=" + mWindowSizeMillis + ", numOccurrences=" + mNumOccurrences + ", timestampQueueMillis=" + mTimestampQueueMillis + "]"); } }