diff options
Diffstat (limited to 'core/java/android/app/PropertyInvalidatedCache.java')
| -rw-r--r-- | core/java/android/app/PropertyInvalidatedCache.java | 252 |
1 files changed, 185 insertions, 67 deletions
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 6ca7dfbe2284..ef4d7b1f42e2 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -22,6 +22,7 @@ import android.os.Looper; import android.os.Message; import android.os.SystemClock; import android.os.SystemProperties; +import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -66,12 +67,12 @@ import java.util.concurrent.atomic.AtomicLong; * * <pre> * public class UserBirthdayServiceImpl implements IUserBirthdayService { - * private final HashMap<Integer, Birthday> mUidToBirthday; - * @Override + * private final HashMap<Integer, Birthday%> mUidToBirthday; + * {@literal @}Override * public synchronized Birthday getUserBirthday(int userId) { * return mUidToBirthday.get(userId); * } - * private synchronized void updateBirthdays(Map<Integer, Birthday> uidToBirthday) { + * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { * mUidToBirthday.clear(); * mUidToBirthday.putAll(uidToBirthday); * } @@ -105,9 +106,9 @@ import java.util.concurrent.atomic.AtomicLong; * ... * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache * private static final String BDAY_CACHE_KEY = "cache_key.birthdayd"; - * private final PropertyInvalidatedCache<Integer, Birthday> mBirthdayCache = new - * PropertyInvalidatedCache<Integer, Birthday>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) { - * @Override + * private final PropertyInvalidatedCache<Integer, Birthday%> mBirthdayCache = new + * PropertyInvalidatedCache<Integer, Birthday%>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) { + * {@literal @}Override * protected Birthday recompute(Integer userId) { * return GetService("birthdayd").getUserBirthday(userId); * } @@ -140,7 +141,7 @@ import java.util.concurrent.atomic.AtomicLong; * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); * } * - * private synchronized void updateBirthdays(Map<Integer, Birthday> uidToBirthday) { + * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { * mUidToBirthday.clear(); * mUidToBirthday.putAll(uidToBirthday); * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); @@ -169,6 +170,41 @@ import java.util.concurrent.atomic.AtomicLong; * this local case, there's no IPC, so use of the cache is (depending on exact * circumstance) unnecessary. * + * There may be queries for which it is more efficient to bypass the cache than to cache + * the result. This would be true, for example, if some queries would require frequent + * cache invalidation while other queries require infrequent invalidation. To expand on + * the birthday example, suppose that there is a userId that signifies "the next + * birthday". When passed this userId, the server returns the next birthday among all + * users - this value changes as time advances. The userId value can be cached, but the + * cache must be invalidated whenever a birthday occurs, and this invalidates all + * birthdays. If there is a large number of users, invalidation will happen so often that + * the cache provides no value. + * + * The class provides a bypass mechanism to handle this situation. + * <pre> + * public class ActivityThread { + * ... + * private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache + * private static final String BDAY_CACHE_KEY = "cache_key.birthdayd"; + * private final PropertyInvalidatedCache<Integer, Birthday%> mBirthdayCache = new + * PropertyInvalidatedCache<Integer, Birthday%>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) { + * {@literal @}Override + * protected Birthday recompute(Integer userId) { + * return GetService("birthdayd").getUserBirthday(userId); + * } + * {@literal @}Override + * protected boolean bypass(Integer userId) { + * return userId == NEXT_BIRTHDAY; + * } + * }; + * ... + * } + * </pre> + * + * If the {@code bypass()} method returns true then the cache is not used for that + * particular query. The {@code bypass()} method is not abstract and the default + * implementation returns false. + * * For security, there is a allowlist of processes that are allowed to invalidate a cache. * The allowlist includes normal runtime processes but does not include test processes. * Test processes must call {@code PropertyInvalidatedCache.disableForTestMode()} to disable @@ -190,19 +226,23 @@ import java.util.concurrent.atomic.AtomicLong; */ public abstract class PropertyInvalidatedCache<Query, Result> { /** - * Reserved nonce values. The code is written assuming that these - * values are contiguous. + * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note + * that all values cause the cache to be skipped. */ private static final int NONCE_UNSET = 0; private static final int NONCE_DISABLED = 1; private static final int NONCE_CORKED = 2; - private static final int NONCE_RESERVED = NONCE_CORKED + 1; + private static final int NONCE_BYPASS = 3; + + private static boolean isReservedNonce(long n) { + return n >= NONCE_UNSET && n <= NONCE_BYPASS; + } /** * The names of the nonces */ private static final String[] sNonceName = - new String[]{ "unset", "disabled", "corked" }; + new String[]{ "unset", "disabled", "corked", "bypass" }; private static final String TAG = "PropertyInvalidatedCache"; private static final boolean DEBUG = false; @@ -220,7 +260,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { private long mMisses = 0; @GuardedBy("mLock") - private long mSkips[] = new long[]{ 0, 0, 0 }; + private long[] mSkips = new long[]{ 0, 0, 0, 0 }; @GuardedBy("mLock") private long mMissOverflow = 0; @@ -363,6 +403,91 @@ public abstract class PropertyInvalidatedCache<Query, Result> { } /** + * SystemProperties are protected and cannot be written (or read, usually) by random + * processes. So, for testing purposes, the methods have a bypass mode that reads and + * writes to a HashMap and does not go out to the SystemProperties at all. + */ + + // If true, the cache might be under test. If false, there is no testing in progress. + private static volatile boolean sTesting = false; + + // If sTesting is true then keys that are under test are in this map. + private static final HashMap<String, Long> sTestingPropertyMap = new HashMap<>(); + + /** + * Enable or disable testing. The testing property map is cleared every time this + * method is called. + */ + @VisibleForTesting + public static void setTestMode(boolean mode) { + sTesting = mode; + synchronized (sTestingPropertyMap) { + sTestingPropertyMap.clear(); + } + } + + /** + * Enable testing the specific cache key. Only keys in the map are subject to testing. + * There is no method to stop testing a property name. Just disable the test mode. + */ + @VisibleForTesting + public static void testPropertyName(String name) { + synchronized (sTestingPropertyMap) { + sTestingPropertyMap.put(name, (long) NONCE_UNSET); + } + } + + // Read the system property associated with the current cache. This method uses the + // handle for faster reading. + private long getCurrentNonce() { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(mPropertyName); + if (n != null) { + return n; + } + } + } + + SystemProperties.Handle handle = mPropertyHandle; + if (handle == null) { + handle = SystemProperties.find(mPropertyName); + if (handle == null) { + return NONCE_UNSET; + } + mPropertyHandle = handle; + } + return handle.getLong(NONCE_UNSET); + } + + // Write the nonce in a static context. No handle is available. + private static void setNonce(String name, long val) { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(name); + if (n != null) { + sTestingPropertyMap.put(name, val); + return; + } + } + } + SystemProperties.set(name, Long.toString(val)); + } + + // Set the nonce in a static context. No handle is available. + private static long getNonce(String name) { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(name); + if (n != null) { + return n; + } + } + } + return SystemProperties.getLong(name, NONCE_UNSET); + } + + /** * Forget all cached values. */ public final void clear() { @@ -418,18 +543,6 @@ public abstract class PropertyInvalidatedCache<Query, Result> { return oldResult; } - private long getCurrentNonce() { - SystemProperties.Handle handle = mPropertyHandle; - if (handle == null) { - handle = SystemProperties.find(mPropertyName); - if (handle == null) { - return NONCE_UNSET; - } - mPropertyHandle = handle; - } - return handle.getLong(NONCE_UNSET); - } - /** * Disable the use of this cache in this process. */ @@ -477,9 +590,11 @@ public abstract class PropertyInvalidatedCache<Query, Result> { public Result query(Query query) { // Let access to mDisabled race: it's atomic anyway. long currentNonce = (!isDisabledLocal()) ? getCurrentNonce() : NONCE_DISABLED; + if (bypass(query)) { + currentNonce = NONCE_BYPASS; + } for (;;) { - if (currentNonce == NONCE_DISABLED || currentNonce == NONCE_UNSET - || currentNonce == NONCE_CORKED || bypass(query)) { + if (isReservedNonce(currentNonce)) { if (!mDisabled) { // Do not bother collecting statistics if the cache is // locally disabled. @@ -490,7 +605,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { if (DEBUG) { if (!mDisabled) { - Log.d(TAG, String.format( + Log.d(TAG, TextUtils.formatSimple( "cache %s %s for %s", cacheName(), sNonceName[(int) currentNonce], queryToString(query))); } @@ -505,7 +620,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { if (cachedResult != null) mHits++; } else { if (DEBUG) { - Log.d(TAG, String.format( + Log.d(TAG, TextUtils.formatSimple( "clearing cache %s of %d entries because nonce changed [%s] -> [%s]", cacheName(), mCache.size(), mLastSeenNonce, currentNonce)); @@ -531,9 +646,10 @@ public abstract class PropertyInvalidatedCache<Query, Result> { if (currentNonce != afterRefreshNonce) { currentNonce = afterRefreshNonce; if (DEBUG) { - Log.d(TAG, String.format("restarting %s %s because nonce changed in refresh", - cacheName(), - queryToString(query))); + Log.d(TAG, TextUtils.formatSimple( + "restarting %s %s because nonce changed in refresh", + cacheName(), + queryToString(query))); } continue; } @@ -602,7 +718,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { if (!sEnabled) { return; } - SystemProperties.set(name, Long.toString(NONCE_DISABLED)); + setNonce(name, NONCE_DISABLED); } /** @@ -622,7 +738,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { public static void invalidateCache(@NonNull String name) { if (!sEnabled) { if (DEBUG) { - Log.w(TAG, String.format( + Log.w(TAG, TextUtils.formatSimple( "cache invalidate %s suppressed", name)); } return; @@ -651,7 +767,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { private static void invalidateCacheLocked(@NonNull String name) { // There's no race here: we don't require that values strictly increase, but instead // only that each is unique in a single runtime-restart session. - final long nonce = SystemProperties.getLong(name, NONCE_UNSET); + final long nonce = getNonce(name); if (nonce == NONCE_DISABLED) { if (DEBUG) { Log.d(TAG, "refusing to invalidate disabled cache: " + name); @@ -662,18 +778,15 @@ public abstract class PropertyInvalidatedCache<Query, Result> { long newValue; do { newValue = NoPreloadHolder.next(); - } while (newValue >= 0 && newValue < NONCE_RESERVED); - final String newValueString = Long.toString(newValue); + } while (isReservedNonce(newValue)); if (DEBUG) { - Log.d(TAG, - String.format("invalidating cache [%s]: [%s] -> [%s]", - name, - nonce, - newValueString)); + Log.d(TAG, TextUtils.formatSimple( + "invalidating cache [%s]: [%s] -> [%s]", + name, nonce, Long.toString(newValue))); } // TODO(dancol): add an atomic compare and exchange property set operation to avoid a // small race with concurrent disable here. - SystemProperties.set(name, newValueString); + setNonce(name, newValue); long invalidateCount = sInvalidates.getOrDefault(name, (long) 0); sInvalidates.put(name, ++invalidateCount); } @@ -693,7 +806,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { public static void corkInvalidations(@NonNull String name) { if (!sEnabled) { if (DEBUG) { - Log.w(TAG, String.format( + Log.w(TAG, TextUtils.formatSimple( "cache cork %s suppressed", name)); } return; @@ -702,7 +815,8 @@ public abstract class PropertyInvalidatedCache<Query, Result> { synchronized (sCorkLock) { int numberCorks = sCorks.getOrDefault(name, 0); if (DEBUG) { - Log.d(TAG, String.format("corking %s: numberCorks=%s", name, numberCorks)); + Log.d(TAG, TextUtils.formatSimple( + "corking %s: numberCorks=%s", name, numberCorks)); } // If we're the first ones to cork this cache, set the cache to the corked state so @@ -714,9 +828,9 @@ public abstract class PropertyInvalidatedCache<Query, Result> { // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair. // Implement this more dangerous mode of operation if necessary. if (numberCorks == 0) { - final long nonce = SystemProperties.getLong(name, NONCE_UNSET); + final long nonce = getNonce(name); if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) { - SystemProperties.set(name, Long.toString(NONCE_CORKED)); + setNonce(name, NONCE_CORKED); } } else { final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); @@ -739,8 +853,8 @@ public abstract class PropertyInvalidatedCache<Query, Result> { public static void uncorkInvalidations(@NonNull String name) { if (!sEnabled) { if (DEBUG) { - Log.w(TAG, String.format( - "cache uncork %s suppressed", name)); + Log.w(TAG, TextUtils.formatSimple( + "cache uncork %s suppressed", name)); } return; } @@ -748,7 +862,8 @@ public abstract class PropertyInvalidatedCache<Query, Result> { synchronized (sCorkLock) { int numberCorks = sCorks.getOrDefault(name, 0); if (DEBUG) { - Log.d(TAG, String.format("uncorking %s: numberCorks=%s", name, numberCorks)); + Log.d(TAG, TextUtils.formatSimple( + "uncorking %s: numberCorks=%s", name, numberCorks)); } if (numberCorks < 1) { @@ -816,7 +931,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { synchronized (mLock) { boolean alreadyQueued = mUncorkDeadlineMs >= 0; if (DEBUG) { - Log.w(TAG, String.format( + Log.w(TAG, TextUtils.formatSimple( "autoCork %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } @@ -834,7 +949,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { private void handleMessage(Message msg) { synchronized (mLock) { if (DEBUG) { - Log.w(TAG, String.format( + Log.w(TAG, TextUtils.formatSimple( "handleMsesage %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } @@ -846,7 +961,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { if (mUncorkDeadlineMs > nowMs) { mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs; if (DEBUG) { - Log.w(TAG, String.format( + Log.w(TAG, TextUtils.formatSimple( "scheduling uncork at %s", mUncorkDeadlineMs)); } @@ -880,10 +995,10 @@ public abstract class PropertyInvalidatedCache<Query, Result> { Result resultToCompare = recompute(query); boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce); if (!nonceChanged && !debugCompareQueryResults(proposedResult, resultToCompare)) { - Log.e(TAG, String.format( - "cache %s inconsistent for %s is %s should be %s", - cacheName(), queryToString(query), - proposedResult, resultToCompare)); + Log.e(TAG, TextUtils.formatSimple( + "cache %s inconsistent for %s is %s should be %s", + cacheName(), queryToString(query), + proposedResult, resultToCompare)); } // Always return the "true" result in verification mode. return resultToCompare; @@ -953,21 +1068,24 @@ public abstract class PropertyInvalidatedCache<Query, Result> { } synchronized (mLock) { - pw.println(String.format(" Cache Name: %s", cacheName())); - pw.println(String.format(" Property: %s", mPropertyName)); - final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED]; - pw.println(String.format(" Hits: %d, Misses: %d, Skips: %d, Clears: %d", + pw.println(TextUtils.formatSimple(" Cache Name: %s", cacheName())); + pw.println(TextUtils.formatSimple(" Property: %s", mPropertyName)); + final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED] + + mSkips[NONCE_BYPASS]; + pw.println(TextUtils.formatSimple( + " Hits: %d, Misses: %d, Skips: %d, Clears: %d", mHits, mMisses, skips, mClears)); - pw.println(String.format(" Skip-corked: %d, Skip-unset: %d, Skip-other: %d", + pw.println(TextUtils.formatSimple( + " Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d", mSkips[NONCE_CORKED], mSkips[NONCE_UNSET], - mSkips[NONCE_DISABLED])); - pw.println(String.format( + mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED])); + pw.println(TextUtils.formatSimple( " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", mLastSeenNonce, invalidateCount, corkedInvalidates)); - pw.println(String.format( + pw.println(TextUtils.formatSimple( " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); - pw.println(String.format(" Enabled: %s", mDisabled ? "false" : "true")); + pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); pw.println(""); Set<Map.Entry<Query, Result>> cacheEntries = mCache.entrySet(); @@ -980,7 +1098,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { String key = Objects.toString(entry.getKey()); String value = Objects.toString(entry.getValue()); - pw.println(String.format(" Key: %s\n Value: %s\n", key, value)); + pw.println(TextUtils.formatSimple(" Key: %s\n Value: %s\n", key, value)); } } } @@ -1009,7 +1127,7 @@ public abstract class PropertyInvalidatedCache<Query, Result> { pw.println(" Corking Status:"); for (int i = 0; i < activeCorks.size(); i++) { Map.Entry<String, Integer> entry = activeCorks.get(i); - pw.println(String.format(" Property Name: %s Count: %d", + pw.println(TextUtils.formatSimple(" Property Name: %s Count: %d", entry.getKey(), entry.getValue())); } } |
