Loading core/java/com/android/internal/util/RateLimitingCache.java 0 → 100644 +135 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.util; import android.os.SystemClock; /** * A speed/rate limiting cache that's used to cache a value to be returned as long as period hasn't * elapsed and then fetches a new value after period has elapsed. Use this when AIDL calls are * expensive but the value returned by those APIs don't change often enough (or the recency doesn't * matter as much), to incur the cost every time. This class maintains the last fetch time and * fetches a new value when period has passed. Do not use this for API calls that have side-effects. * <p> * By passing in an optional <code>count</code> during creation, this can be used as a rate * limiter that allows up to <code>count</code> calls per period to be passed on to the query * and then the cached value is returned for the remainder of the period. It uses a simple fixed * window method to track rate. Use a window and count appropriate for bursts of calls and for * high latency/cost of the AIDL call. * * @param <Value> The type of the return value * @hide */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public class RateLimitingCache<Value> { private volatile Value mCurrentValue; private volatile long mLastTimestamp; // Can be last fetch time or window start of fetch time private final long mPeriodMillis; // window size private final int mLimit; // max per window private int mCount = 0; // current count within window private long mRandomOffset; // random offset to avoid batching of AIDL calls at window boundary /** * The interface to fetch the actual value, if the cache is null or expired. * @hide * @param <V> The return value type */ public interface ValueFetcher<V> { /** Called when the cache needs to be updated. * @return the latest value fetched from the source */ V fetchValue(); } /** * Create a speed limiting cache that returns the same value until periodMillis has passed * and then fetches a new value via the {@link ValueFetcher}. * * @param periodMillis time to wait before fetching a new value. Use a negative period to * indicate the value never changes and is fetched only once and * cached. A value of 0 will mean always fetch a new value. */ public RateLimitingCache(long periodMillis) { this(periodMillis, 1); } /** * Create a rate-limiting cache that allows up to <code>count</code> number of AIDL calls per * period before it starts returning a cached value. The count resets when the next period * begins. * * @param periodMillis the window of time in which <code>count</code> calls will fetch the * newest value from the AIDL call. * @param count how many times during the period it's ok to forward the request to the fetcher * in the {@link #get(ValueFetcher)} method. */ public RateLimitingCache(long periodMillis, int count) { mPeriodMillis = periodMillis; mLimit = count; if (mLimit > 1 && periodMillis > 1) { mRandomOffset = (long) (Math.random() * (periodMillis / 2)); } } /** * Returns the current time in <code>elapsedRealtime</code>. Can be overridden to use * a different timebase that is monotonically increasing; for example, uptimeMillis() * @return a monotonically increasing time in milliseconds */ protected long getTime() { return SystemClock.elapsedRealtime(); } /** * Returns either the cached value, if called more frequently than the specific rate, or * a new value is fetched and cached. Warning: if the caller is likely to mutate the returned * object, override this method and make a clone before returning it. * @return the cached or latest value */ public Value get(ValueFetcher<Value> query) { // If the value never changes if (mPeriodMillis < 0 && mLastTimestamp != 0) { return mCurrentValue; } synchronized (this) { // Get the current time and add a random offset to avoid colliding with other // caches with similar harmonic window boundaries final long now = getTime() + mRandomOffset; final boolean newWindow = now - mLastTimestamp >= mPeriodMillis; if (newWindow || mCount < mLimit) { // Fetch a new value mCurrentValue = query.fetchValue(); // If rate limiting, set timestamp to start of this window if (mLimit > 1) { mLastTimestamp = now - (now % mPeriodMillis); } else { mLastTimestamp = now; } if (newWindow) { mCount = 1; } else { mCount++; } } return mCurrentValue; } } } core/tests/coretests/src/com/android/internal/util/RateLimitingCacheTest.java 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.os.SystemClock; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Test the RateLimitingCache class. */ @RunWith(AndroidJUnit4.class) public class RateLimitingCacheTest { private int mCounter = 0; @Before public void before() { mCounter = -1; } RateLimitingCache.ValueFetcher<Integer> mFetcher = () -> { return ++mCounter; }; /** * Test zero period passed into RateLimitingCache. A new value should be returned for each * time the cache's get() is invoked. */ @Test public void testTtl_Zero() { RateLimitingCache<Integer> s = new RateLimitingCache<>(0); int first = s.get(mFetcher); assertEquals(first, 0); int second = s.get(mFetcher); assertEquals(second, 1); SystemClock.sleep(20); int third = s.get(mFetcher); assertEquals(third, 2); } /** * Test a period of 100ms passed into RateLimitingCache. A new value should not be fetched * any more frequently than every 100ms. */ @Test public void testTtl_100() { RateLimitingCache<Integer> s = new RateLimitingCache<>(100); int first = s.get(mFetcher); assertEquals(first, 0); int second = s.get(mFetcher); // Too early to change assertEquals(second, 0); SystemClock.sleep(150); int third = s.get(mFetcher); // Changed by now assertEquals(third, 1); int fourth = s.get(mFetcher); // Too early to change again assertEquals(fourth, 1); } /** * Test a negative period passed into RateLimitingCache. A new value should only be fetched the * first call to get(). */ @Test public void testTtl_Negative() { RateLimitingCache<Integer> s = new RateLimitingCache<>(-1); int first = s.get(mFetcher); assertEquals(first, 0); SystemClock.sleep(200); // Should return the original value every time int second = s.get(mFetcher); assertEquals(second, 0); } /** * Test making tons of calls to the speed-limiter and make sure number of fetches does not * exceed expected number of fetches. */ @Test public void testTtl_Spam() { RateLimitingCache<Integer> s = new RateLimitingCache<>(100); assertCount(s, 1000, 7, 15); } /** * Test rate-limiting across multiple periods and make sure the expected number of fetches is * within the specified rate. */ @Test public void testRate_10hz() { RateLimitingCache<Integer> s = new RateLimitingCache<>(1000, 10); // At 10 per second, 2 seconds should not exceed about 30, assuming overlap into left and // right windows that allow 10 each assertCount(s, 2000, 20, 33); } /** * Test that using a different timebase works correctly. */ @Test public void testTimebase() { RateLimitingCache<Integer> s = new RateLimitingCache<>(1000, 10) { @Override protected long getTime() { return SystemClock.elapsedRealtime() / 2; } }; // Timebase is moving at half the speed, so only allows for 1 second worth in 2 seconds. assertCount(s, 2000, 10, 22); } /** * Helper to make repeated calls every 5 millis to verify the number of expected fetches for * the given parameters. * @param cache the cache object * @param period the period for which to make get() calls * @param minCount the lower end of the expected number of fetches, with a margin for error * @param maxCount the higher end of the expected number of fetches, with a margin for error */ private void assertCount(RateLimitingCache<Integer> cache, long period, int minCount, int maxCount) { long startTime = SystemClock.elapsedRealtime(); while (SystemClock.elapsedRealtime() < startTime + period) { int value = cache.get(mFetcher); SystemClock.sleep(5); } int latest = cache.get(mFetcher); assertTrue("Latest should be between " + minCount + " and " + maxCount + " but is " + latest, latest <= maxCount && latest >= minCount); } } ravenwood/texts/ravenwood-annotation-allowed-classes.txt +1 −0 Original line number Diff line number Diff line Loading @@ -311,6 +311,7 @@ com.android.internal.util.ProcFileReader com.android.internal.util.QuickSelect com.android.internal.util.RingBuffer com.android.internal.util.SizedInputStream com.android.internal.util.RateLimitingCache com.android.internal.util.StringPool com.android.internal.util.TokenBucket com.android.internal.util.XmlPullParserWrapper Loading Loading
core/java/com/android/internal/util/RateLimitingCache.java 0 → 100644 +135 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.util; import android.os.SystemClock; /** * A speed/rate limiting cache that's used to cache a value to be returned as long as period hasn't * elapsed and then fetches a new value after period has elapsed. Use this when AIDL calls are * expensive but the value returned by those APIs don't change often enough (or the recency doesn't * matter as much), to incur the cost every time. This class maintains the last fetch time and * fetches a new value when period has passed. Do not use this for API calls that have side-effects. * <p> * By passing in an optional <code>count</code> during creation, this can be used as a rate * limiter that allows up to <code>count</code> calls per period to be passed on to the query * and then the cached value is returned for the remainder of the period. It uses a simple fixed * window method to track rate. Use a window and count appropriate for bursts of calls and for * high latency/cost of the AIDL call. * * @param <Value> The type of the return value * @hide */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public class RateLimitingCache<Value> { private volatile Value mCurrentValue; private volatile long mLastTimestamp; // Can be last fetch time or window start of fetch time private final long mPeriodMillis; // window size private final int mLimit; // max per window private int mCount = 0; // current count within window private long mRandomOffset; // random offset to avoid batching of AIDL calls at window boundary /** * The interface to fetch the actual value, if the cache is null or expired. * @hide * @param <V> The return value type */ public interface ValueFetcher<V> { /** Called when the cache needs to be updated. * @return the latest value fetched from the source */ V fetchValue(); } /** * Create a speed limiting cache that returns the same value until periodMillis has passed * and then fetches a new value via the {@link ValueFetcher}. * * @param periodMillis time to wait before fetching a new value. Use a negative period to * indicate the value never changes and is fetched only once and * cached. A value of 0 will mean always fetch a new value. */ public RateLimitingCache(long periodMillis) { this(periodMillis, 1); } /** * Create a rate-limiting cache that allows up to <code>count</code> number of AIDL calls per * period before it starts returning a cached value. The count resets when the next period * begins. * * @param periodMillis the window of time in which <code>count</code> calls will fetch the * newest value from the AIDL call. * @param count how many times during the period it's ok to forward the request to the fetcher * in the {@link #get(ValueFetcher)} method. */ public RateLimitingCache(long periodMillis, int count) { mPeriodMillis = periodMillis; mLimit = count; if (mLimit > 1 && periodMillis > 1) { mRandomOffset = (long) (Math.random() * (periodMillis / 2)); } } /** * Returns the current time in <code>elapsedRealtime</code>. Can be overridden to use * a different timebase that is monotonically increasing; for example, uptimeMillis() * @return a monotonically increasing time in milliseconds */ protected long getTime() { return SystemClock.elapsedRealtime(); } /** * Returns either the cached value, if called more frequently than the specific rate, or * a new value is fetched and cached. Warning: if the caller is likely to mutate the returned * object, override this method and make a clone before returning it. * @return the cached or latest value */ public Value get(ValueFetcher<Value> query) { // If the value never changes if (mPeriodMillis < 0 && mLastTimestamp != 0) { return mCurrentValue; } synchronized (this) { // Get the current time and add a random offset to avoid colliding with other // caches with similar harmonic window boundaries final long now = getTime() + mRandomOffset; final boolean newWindow = now - mLastTimestamp >= mPeriodMillis; if (newWindow || mCount < mLimit) { // Fetch a new value mCurrentValue = query.fetchValue(); // If rate limiting, set timestamp to start of this window if (mLimit > 1) { mLastTimestamp = now - (now % mPeriodMillis); } else { mLastTimestamp = now; } if (newWindow) { mCount = 1; } else { mCount++; } } return mCurrentValue; } } }
core/tests/coretests/src/com/android/internal/util/RateLimitingCacheTest.java 0 → 100644 +158 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.util; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.os.SystemClock; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; /** * Test the RateLimitingCache class. */ @RunWith(AndroidJUnit4.class) public class RateLimitingCacheTest { private int mCounter = 0; @Before public void before() { mCounter = -1; } RateLimitingCache.ValueFetcher<Integer> mFetcher = () -> { return ++mCounter; }; /** * Test zero period passed into RateLimitingCache. A new value should be returned for each * time the cache's get() is invoked. */ @Test public void testTtl_Zero() { RateLimitingCache<Integer> s = new RateLimitingCache<>(0); int first = s.get(mFetcher); assertEquals(first, 0); int second = s.get(mFetcher); assertEquals(second, 1); SystemClock.sleep(20); int third = s.get(mFetcher); assertEquals(third, 2); } /** * Test a period of 100ms passed into RateLimitingCache. A new value should not be fetched * any more frequently than every 100ms. */ @Test public void testTtl_100() { RateLimitingCache<Integer> s = new RateLimitingCache<>(100); int first = s.get(mFetcher); assertEquals(first, 0); int second = s.get(mFetcher); // Too early to change assertEquals(second, 0); SystemClock.sleep(150); int third = s.get(mFetcher); // Changed by now assertEquals(third, 1); int fourth = s.get(mFetcher); // Too early to change again assertEquals(fourth, 1); } /** * Test a negative period passed into RateLimitingCache. A new value should only be fetched the * first call to get(). */ @Test public void testTtl_Negative() { RateLimitingCache<Integer> s = new RateLimitingCache<>(-1); int first = s.get(mFetcher); assertEquals(first, 0); SystemClock.sleep(200); // Should return the original value every time int second = s.get(mFetcher); assertEquals(second, 0); } /** * Test making tons of calls to the speed-limiter and make sure number of fetches does not * exceed expected number of fetches. */ @Test public void testTtl_Spam() { RateLimitingCache<Integer> s = new RateLimitingCache<>(100); assertCount(s, 1000, 7, 15); } /** * Test rate-limiting across multiple periods and make sure the expected number of fetches is * within the specified rate. */ @Test public void testRate_10hz() { RateLimitingCache<Integer> s = new RateLimitingCache<>(1000, 10); // At 10 per second, 2 seconds should not exceed about 30, assuming overlap into left and // right windows that allow 10 each assertCount(s, 2000, 20, 33); } /** * Test that using a different timebase works correctly. */ @Test public void testTimebase() { RateLimitingCache<Integer> s = new RateLimitingCache<>(1000, 10) { @Override protected long getTime() { return SystemClock.elapsedRealtime() / 2; } }; // Timebase is moving at half the speed, so only allows for 1 second worth in 2 seconds. assertCount(s, 2000, 10, 22); } /** * Helper to make repeated calls every 5 millis to verify the number of expected fetches for * the given parameters. * @param cache the cache object * @param period the period for which to make get() calls * @param minCount the lower end of the expected number of fetches, with a margin for error * @param maxCount the higher end of the expected number of fetches, with a margin for error */ private void assertCount(RateLimitingCache<Integer> cache, long period, int minCount, int maxCount) { long startTime = SystemClock.elapsedRealtime(); while (SystemClock.elapsedRealtime() < startTime + period) { int value = cache.get(mFetcher); SystemClock.sleep(5); } int latest = cache.get(mFetcher); assertTrue("Latest should be between " + minCount + " and " + maxCount + " but is " + latest, latest <= maxCount && latest >= minCount); } }
ravenwood/texts/ravenwood-annotation-allowed-classes.txt +1 −0 Original line number Diff line number Diff line Loading @@ -311,6 +311,7 @@ com.android.internal.util.ProcFileReader com.android.internal.util.QuickSelect com.android.internal.util.RingBuffer com.android.internal.util.SizedInputStream com.android.internal.util.RateLimitingCache com.android.internal.util.StringPool com.android.internal.util.TokenBucket com.android.internal.util.XmlPullParserWrapper Loading