/* * Copyright (C) 2019 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 android.app; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicLong; /** * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing, * but doesn't hold a lock across data fetches on query misses. * * The intended use case is caching frequently-read, seldom-changed information normally * retrieved across interprocess communication. Imagine that you've written a user birthday * information daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface * over binder. That binder interface looks something like this: * *
* parcelable Birthday { * int month; * int day; * } * interface IUserBirthdayService { * Birthday getUserBirthday(int userId); * } ** * Suppose the service implementation itself looks like this... * *
* public class UserBirthdayServiceImpl implements IUserBirthdayService { * 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) { * mUidToBirthday.clear(); * mUidToBirthday.putAll(uidToBirthday); * } * } ** * ... and we have a client in frameworks (loaded into every app process) that looks * like this: * *
* public class ActivityThread { * ... * public Birthday getUserBirthday(int userId) { * return GetService("birthdayd").getUserBirthday(userId); * } * ... * } ** * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call * to the birthdayd process and consult its database of birthdays. If we query user birthdays * frequently, we do a lot of work that we don't have to do, since user birthdays * change infrequently. * * PropertyInvalidatedCache is part of a pattern for optimizing this kind of * information-querying code. Using {@code PropertyInvalidatedCache}, you'd write the client * this way: * *
* public class ActivityThread { * ... * private final PropertyInvalidatedCache.QueryHandler<Integer, Birthday> mBirthdayQuery = * new PropertyInvalidatedCache.QueryHandler<Integer, Birthday>() { * {@literal @}Override * public Birthday apply(Integer) { * return GetService("birthdayd").getUserBirthday(userId); * } * }; * 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, MODULE_SYSTEM, "getUserBirthday", mBirthdayQuery); * * public void disableUserBirthdayCache() { * mBirthdayCache.disableForCurrentProcess(); * } * public void invalidateUserBirthdayCache() { * mBirthdayCache.invalidateCache(); * } * public Birthday getUserBirthday(int userId) { * return mBirthdayCache.query(userId); * } * ... * } ** * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday * for the first time; on subsequent queries, we return the already-known Birthday object. * * The second parameter to the IpcDataCache constructor is a string that identifies the "module" * that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any * string is permitted. The third parameters is the name of the API being cached; this, too, can * any value. The fourth is the name of the cache. The cache is usually named after th API. * Some things you must know about the three strings: *
* public class UserBirthdayServiceImpl { * ... * public UserBirthdayServiceImpl() { * ... * ActivityThread.currentActivityThread().disableUserBirthdayCache(); * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); * } * * private synchronized void updateBirthdays(Map<Integer, Birthday%> uidToBirthday) { * mUidToBirthday.clear(); * mUidToBirthday.putAll(uidToBirthday); * ActivityThread.currentActivityThread().invalidateUserBirthdayCache(); * } * ... * } ** * The call to {@code PropertyInvalidatedCache.invalidateCache()} guarantees that all clients * will re-fetch birthdays from binder during consequent calls to * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock * held, we maintain consistency between different client views of the birthday state. The use * of PropertyInvalidatedCache in this idiomatic way introduces no new race conditions. * * PropertyInvalidatedCache has a few other features for doing things like incremental * enhancement of cached values and invalidation of multiple caches (that all share the same * property key) at once. * * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each * time we update the cache. SELinux configuration must allow everyone to read this property * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write * the property. (These properties conventionally begin with the "cache_key." prefix.) * * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In * 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. *
* public class ActivityThread { * ... * private final IpcDataCache.QueryHandler<Integer, Birthday> mBirthdayQuery = * new IpcDataCache.QueryHandler<Integer, Birthday>() { * {@literal @}Override * public Birthday apply(Integer) { * return GetService("birthdayd").getUserBirthday(userId); * } * {@literal @}Override * public boolean shouldBypassQuery(Integer userId) { * return userId == NEXT_BIRTHDAY; * } * }; * ... * } ** * If the {@code shouldBypassQuery()} method returns true then the cache is not used for that * particular query. The {@code shouldBypassQuery()} 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 * all cache activity in that process. * * Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding. * * To test a binder cache, create one or more tests that exercise the binder method. This * should be done twice: once with production code and once with a special image that sets * {@code DEBUG} and {@code VERIFY} true. In the latter case, verify that no cache * inconsistencies are reported. If a cache inconsistency is reported, however, it might be a * false positive. This happens if the server side data can be read and written non-atomically * with respect to cache invalidation. * * @param
{ /** * Compute a result given a query. The semantics are those of Functor. */ public abstract @Nullable R apply(@NonNull Q query); /** * Return true if a query should not use the cache. The default implementation * always uses the cache. */ public boolean shouldBypassCache(@NonNull Q query) { return false; } }; /** * The system properties used by caches should be of the form. . , * where the prefix is "cache_key", the module is one of the constants below, and the * api is any string. The ability to write the property (which happens during * invalidation) depends on SELinux rules; these rules are defined against * . . Therefore, the module chosen for a cache property must match * the permissions granted to the processes that contain the corresponding caches. * @hide */ /** * The module used for unit tests and cts tests. It is expected that no process in * the system has permissions to write properties with this module. * @hide */ @TestApi public static final String MODULE_TEST = "test"; /** * The module used for system server/framework caches. This is not visible outside * the system processes. * @hide */ @TestApi public static final String MODULE_SYSTEM = "system_server"; /** * The module used for bluetooth caches. * @hide */ @TestApi public static final String MODULE_BLUETOOTH = "bluetooth"; /** * The module used for telephony caches. */ public static final String MODULE_TELEPHONY = "telephony"; /** * Constants that affect retries when the process is unable to write the property. * The first constant is the number of times the process will attempt to set the * property. The second constant is the delay between attempts. */ /** * Wait 200ms between retry attempts and the retry limit is 5. That gives a total possible * delay of 1s, which should be less than ANR timeouts. The goal is to have the system crash * because the property could not be set (which is a condition that is easily recognized) and * not crash because of an ANR (which can be confusing to debug). */ private static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200; private static final int PROPERTY_FAILURE_RETRY_LIMIT = 5; /** * Construct a system property that matches the rules described above. The module is * one of the permitted values above. The API is a string that is a legal Java simple * identifier. The api is modified to conform to the system property style guide by * replacing every upper case letter with an underscore and the lower case equivalent. * (An initial upper case letter is not prefixed with an underscore). * There is no requirement that the apiName be the name of an actual API. * * Be aware that SystemProperties has a maximum length which is private to the * implementation. The current maximum is 92 characters. If this method creates a * property name that is too long, SystemProperties.set() will fail without a good * error message. * @hide */ @TestApi public static @NonNull String createPropertyName(@NonNull String module, @NonNull String apiName) { char[] api = apiName.toCharArray(); int upper = 0; for (int i = 1; i < api.length; i++) { if (Character.isUpperCase(api[i])) { upper++; } } char[] suffix = new char[api.length + upper]; int j = 0; for (int i = 0; i < api.length; i++) { if (Character.isJavaIdentifierPart(api[i])) { if (Character.isUpperCase(api[i])) { if (i > 0) { suffix[j++] = '_'; } suffix[j++] = Character.toLowerCase(api[i]); } else { suffix[j++] = api[i]; } } else { throw new IllegalArgumentException("invalid api name"); } } return "cache_key." + module + "." + new String(suffix); } /** * 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_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", "bypass" }; private static final String TAG = "PropertyInvalidatedCache"; private static final boolean DEBUG = false; private static final boolean VERIFY = false; /** * The object-private lock. */ private final Object mLock = new Object(); // Per-Cache performance counters. @GuardedBy("mLock") private long mHits = 0; @GuardedBy("mLock") private long mMisses = 0; @GuardedBy("mLock") private long[] mSkips = new long[]{ 0, 0, 0, 0 }; @GuardedBy("mLock") private long mMissOverflow = 0; @GuardedBy("mLock") private long mHighWaterMark = 0; @GuardedBy("mLock") private long mClears = 0; /** * Protect objects that support corking. mLock and sGlobalLock must never be taken while this * is held. */ private static final Object sCorkLock = new Object(); /** * Record the number of invalidate or cork calls that were nops because the cache was already * corked. This is static because invalidation is done in a static context. Entries are * indexed by the cache property. */ @GuardedBy("sCorkLock") private static final HashMap sCorkedInvalidates = new HashMap<>(); /** * A map of cache keys that we've "corked". (The values are counts.) When a cache key is * corked, we skip the cache invalidate when the cache key is in the unset state --- that * is, when a cache key is corked, an invalidation does not enable the cache if somebody * else hasn't disabled it. */ @GuardedBy("sCorkLock") private static final HashMap sCorks = new HashMap<>(); /** * A lock for the global list of caches and cache keys. This must never be taken inside mLock * or sCorkLock. */ private static final Object sGlobalLock = new Object(); /** * A map of cache keys that have been disabled in the local process. When a key is * disabled locally, existing caches are disabled and the key is saved in this map. * Future cache instances that use the same key will be disabled in their constructor. */ @GuardedBy("sGlobalLock") private static final HashSet sDisabledKeys = new HashSet<>(); /** * Weakly references all cache objects in the current process, allowing us to iterate over * them all for purposes like issuing debug dumps and reacting to memory pressure. */ @GuardedBy("sGlobalLock") private static final WeakHashMap sCaches = new WeakHashMap<>(); /** * Counts of the number of times a cache key was invalidated. Invalidation occurs in a static * context with no cache object available, so this is a static map. Entries are indexed by * the cache property. */ @GuardedBy("sGlobalLock") private static final HashMap sInvalidates = new HashMap<>(); /** * If sEnabled is false then all cache operations are stubbed out. Set * it to false inside test processes. */ private static boolean sEnabled = true; /** * Name of the property that holds the unique value that we use to invalidate the cache. */ private final String mPropertyName; /** * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the * property exists on the system. */ private volatile SystemProperties.Handle mPropertyHandle; /** * The name by which this cache is known. This should normally be the * binder call that is being cached, but the constructors default it to * the property name. */ private final String mCacheName; /** * The function that computes a Result, given a Query. This function is called on a * cache miss. */ private QueryHandler mComputer; /** * A default function that delegates to the deprecated recompute() method. */ private static class DefaultComputer extends QueryHandler { final PropertyInvalidatedCache mCache; DefaultComputer(PropertyInvalidatedCache cache) { mCache = cache; } public Result apply(Query query) { return mCache.recompute(query); } } @GuardedBy("mLock") private final LinkedHashMap mCache; /** * The last value of the {@code mPropertyHandle} that we observed. */ @GuardedBy("mLock") private long mLastSeenNonce = NONCE_UNSET; /** * Whether we've disabled the cache in this process. */ private boolean mDisabled = false; /** * Maximum number of entries the cache will maintain. */ private final int mMaxEntries; /** * Make a new property invalidated cache. This constructor names the cache after the * property name. New clients should prefer the constructor that takes an explicit * cache name. * * TODO(216112648): deprecate this as a public interface, in favor of the four-argument * constructor. * * @param maxEntries Maximum number of entries to cache; LRU discard * @param propertyName Name of the system property holding the cache invalidation nonce. * * @hide */ public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) { this(maxEntries, propertyName, propertyName); } /** * Make a new property invalidated cache. * * TODO(216112648): deprecate this as a public interface, in favor of the four-argument * constructor. * * @param maxEntries Maximum number of entries to cache; LRU discard * @param propertyName Name of the system property holding the cache invalidation nonce * @param cacheName Name of this cache in debug and dumpsys * @hide */ public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName, @NonNull String cacheName) { mPropertyName = propertyName; mCacheName = cacheName; mMaxEntries = maxEntries; mComputer = new DefaultComputer<>(this); mCache = createMap(); registerCache(); } /** * Make a new property invalidated cache. The key is computed from the module and api * parameters. * * @param maxEntries Maximum number of entries to cache; LRU discard * @param module The module under which the cache key should be placed. * @param api The api this cache front-ends. The api must be a Java identifier but * need not be an actual api. * @param cacheName Name of this cache in debug and dumpsys * @param computer The code to compute values that are not in the cache. * @hide */ @TestApi public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api, @NonNull String cacheName, @NonNull QueryHandler computer) { mPropertyName = createPropertyName(module, api); mCacheName = cacheName; mMaxEntries = maxEntries; mComputer = computer; mCache = createMap(); registerCache(); } // Create a map. This should be called only from the constructor. private LinkedHashMap createMap() { return new LinkedHashMap ( 2 /* start small */, 0.75f /* default load factor */, true /* LRU access order */) { @GuardedBy("mLock") @Override protected boolean removeEldestEntry(Map.Entry eldest) { final int size = size(); if (size > mHighWaterMark) { mHighWaterMark = size; } if (size > mMaxEntries) { mMissOverflow++; return true; } return false; } }; } /** * Register the map in the global list. If the cache is disabled globally, disable it * now. This method is only ever called from the constructor, which means no other thread has * access to the object yet. It can safely be modified outside any lock. */ private void registerCache() { synchronized (sGlobalLock) { if (sDisabledKeys.contains(mCacheName)) { disableInstance(); } sCaches.put(this, null); } } /** * 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 sTestingPropertyMap = new HashMap<>(); /** * Enable or disable testing. The testing property map is cleared every time this * method is called. * @hide */ @TestApi 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. */ private static void testPropertyName(@NonNull String name) { synchronized (sTestingPropertyMap) { sTestingPropertyMap.put(name, (long) NONCE_UNSET); } } /** * 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. * @hide */ @TestApi public void testPropertyName() { testPropertyName(mPropertyName); } // 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; } } } RuntimeException failure = null; for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) { try { SystemProperties.set(name, Long.toString(val)); if (attempt > 0) { // This log is not guarded. Based on known bug reports, it should // occur once a week or less. The purpose of the log message is to // identify the retries as a source of delay that might be otherwise // be attributed to the cache itself. Log.w(TAG, "Nonce set after " + attempt + " tries"); } return; } catch (RuntimeException e) { if (failure == null) { failure = e; } try { Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS); } catch (InterruptedException x) { // Ignore this exception. The desired delay is only approximate and // there is no issue if the sleep sometimes terminates early. } } } // This point is reached only if SystemProperties.set() fails at least once. // Rethrow the first exception that was received. throw failure; } // 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. * TODO(216112648) remove this as a public API. Clients should invalidate caches, not clear * them. * @hide */ public final void clear() { synchronized (mLock) { if (DEBUG) { Log.d(TAG, "clearing cache for " + mPropertyName); } mCache.clear(); mClears++; } } /** * Fetch a result from scratch in case it's not in the cache at all. Called unlocked: may * block. If this function returns null, the result of the cache query is null. There is no * "negative cache" in the query: we don't cache null results at all. * TODO(216112648): deprecate this as a public interface, in favor of an instance of * QueryHandler. * @hide */ public Result recompute(@NonNull Query query) { return mComputer.apply(query); } /** * Return true if the query should bypass the cache. The default behavior is to * always use the cache but the method can be overridden for a specific class. * TODO(216112648): deprecate this as a public interface, in favor of an instance of * QueryHandler. * @hide */ public boolean bypass(@NonNull Query query) { return mComputer.shouldBypassCache(query); } /** * Determines if a pair of responses are considered equal. Used to determine whether * a cache is inadvertently returning stale results when VERIFY is set to true. * @hide */ public boolean resultEquals(Result cachedResult, Result fetchedResult) { // If a service crashes and returns a null result, the cached value remains valid. if (fetchedResult != null) { return Objects.equals(cachedResult, fetchedResult); } return true; } /** * Make result up-to-date on a cache hit. Called unlocked; * may block. * * Return either 1) oldResult itself (the same object, by reference equality), in which * case we just return oldResult as the result of the cache query, 2) a new object, which * replaces oldResult in the cache and which we return as the result of the cache query * after performing another property read to make sure that the result hasn't changed in * the meantime (if the nonce has changed in the meantime, we drop the cache and try the * whole query again), or 3) null, which causes the old value to be removed from the cache * and null to be returned as the result of the cache query. * @hide */ protected Result refresh(Result oldResult, Query query) { return oldResult; } /** * Disable the use of this cache in this process. This method is using internally and during * testing. To disable a cache in normal code, use disableLocal(). A disabled cache cannot * be re-enabled. * @hide */ @TestApi public final void disableInstance() { synchronized (mLock) { mDisabled = true; clear(); } } /** * Disable the local use of all caches with the same name. All currently registered caches * with the name will be disabled now, and all future cache instances that use the name will * be disabled in their constructor. */ private static final void disableLocal(@NonNull String name) { synchronized (sGlobalLock) { if (sDisabledKeys.contains(name)) { // The key is already in recorded so there is no further work to be done. return; } for (PropertyInvalidatedCache cache : sCaches.keySet()) { if (name.equals(cache.mCacheName)) { cache.disableInstance(); } } // Record the disabled key after the iteration. If an exception occurs during the // iteration above, and the code is retried, the function should not exit early. sDisabledKeys.add(name); } } /** * Stop disabling local caches with a particular name. Any caches that are currently * disabled remain disabled (the "disabled" setting is sticky). However, new caches * with this name will not be disabled. It is not an error if the cache name is not * found in the list of disabled caches. * @hide */ @TestApi public final void forgetDisableLocal() { synchronized (sGlobalLock) { sDisabledKeys.remove(mCacheName); } } /** * Disable this cache in the current process, and all other caches that use the same * name. This does not affect caches that have a different name but use the same * property. * TODO(216112648) Remove this in favor of disableForCurrentProcess(). * @hide */ public void disableLocal() { disableForCurrentProcess(); } /** * Disable this cache in the current process, and all other present and future caches that use * the same name. This does not affect caches that have a different name but use the same * property. Once disabled, a cache cannot be reenabled. * @hide */ @TestApi public void disableForCurrentProcess() { disableLocal(mCacheName); } /** @hide */ @TestApi public static void disableForCurrentProcess(@NonNull String cacheName) { disableLocal(cacheName); } /** * Return whether a cache instance is disabled. * @hide */ @TestApi public final boolean isDisabled() { return mDisabled || !sEnabled; } /** * Get a value from the cache or recompute it. * @hide */ @TestApi public @Nullable Result query(@NonNull Query query) { // Let access to mDisabled race: it's atomic anyway. long currentNonce = (!isDisabled()) ? getCurrentNonce() : NONCE_DISABLED; if (bypass(query)) { currentNonce = NONCE_BYPASS; } for (;;) { if (isReservedNonce(currentNonce)) { if (!mDisabled) { // Do not bother collecting statistics if the cache is // locally disabled. synchronized (mLock) { mSkips[(int) currentNonce]++; } } if (DEBUG) { if (!mDisabled) { Log.d(TAG, TextUtils.formatSimple( "cache %s %s for %s", cacheName(), sNonceName[(int) currentNonce], queryToString(query))); } } return recompute(query); } final Result cachedResult; synchronized (mLock) { if (currentNonce == mLastSeenNonce) { cachedResult = mCache.get(query); if (cachedResult != null) mHits++; } else { if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "clearing cache %s of %d entries because nonce changed [%s] -> [%s]", cacheName(), mCache.size(), mLastSeenNonce, currentNonce)); } clear(); mLastSeenNonce = currentNonce; cachedResult = null; } } // Cache hit --- but we're not quite done yet. A value in the cache might need to // be augmented in a "refresh" operation. The refresh operation can combine the // old and the new nonce values. In order to make sure the new parts of the value // are consistent with the old, possibly-reused parts, we check the property value // again after the refresh and do the whole fetch again if the property invalidated // us while we were refreshing. if (cachedResult != null) { final Result refreshedResult = refresh(cachedResult, query); if (refreshedResult != cachedResult) { if (DEBUG) { Log.d(TAG, "cache refresh for " + cacheName() + " " + queryToString(query)); } final long afterRefreshNonce = getCurrentNonce(); if (currentNonce != afterRefreshNonce) { currentNonce = afterRefreshNonce; if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "restarting %s %s because nonce changed in refresh", cacheName(), queryToString(query))); } continue; } synchronized (mLock) { if (currentNonce != mLastSeenNonce) { // Do nothing: cache is already out of date. Just return the value // we already have: there's no guarantee that the contents of mCache // won't become invalid as soon as we return. } else if (refreshedResult == null) { mCache.remove(query); } else { mCache.put(query, refreshedResult); } } return maybeCheckConsistency(query, refreshedResult); } if (DEBUG) { Log.d(TAG, "cache hit for " + cacheName() + " " + queryToString(query)); } return maybeCheckConsistency(query, cachedResult); } // Cache miss: make the value from scratch. if (DEBUG) { Log.d(TAG, "cache miss for " + cacheName() + " " + queryToString(query)); } final Result result = recompute(query); synchronized (mLock) { // If someone else invalidated the cache while we did the recomputation, don't // update the cache with a potentially stale result. if (mLastSeenNonce == currentNonce && result != null) { mCache.put(query, result); } mMisses++; } return maybeCheckConsistency(query, result); } } // Inner class avoids initialization in processes that don't do any invalidation private static final class NoPreloadHolder { private static final AtomicLong sNextNonce = new AtomicLong((new Random()).nextLong()); public static long next() { return sNextNonce.getAndIncrement(); } } /** * Non-static convenience version of disableSystemWide() for situations in which only a * single PropertyInvalidatedCache is keyed on a particular property value. * * When multiple caches share a single property value, using an instance method on one of * the cache objects to invalidate all of the cache objects becomes confusing and you should * just use the static version of this function. * @hide */ @TestApi public final void disableSystemWide() { disableSystemWide(mPropertyName); } /** * Disable all caches system-wide that are keyed on {@var name}. This * function is synchronous: caches are invalidated and disabled upon return. * * @param name Name of the cache-key property to invalidate */ private static void disableSystemWide(@NonNull String name) { if (!sEnabled) { return; } setNonce(name, NONCE_DISABLED); } /** * Non-static convenience version of invalidateCache() for situations in which only a single * PropertyInvalidatedCache is keyed on a particular property value. * @hide */ @TestApi public void invalidateCache() { invalidateCache(mPropertyName); } /** * Invalidate caches in all processes that are keyed for the module and api. * @hide */ @TestApi public static void invalidateCache(@NonNull String module, @NonNull String api) { invalidateCache(createPropertyName(module, api)); } /** * Invalidate PropertyInvalidatedCache caches in all processes that are keyed on * {@var name}. This function is synchronous: caches are invalidated upon return. * * TODO(216112648) make this method private in favor of the two-argument (module, api) * override. * * @param name Name of the cache-key property to invalidate * @hide */ public static void invalidateCache(@NonNull String name) { if (!sEnabled) { if (DEBUG) { Log.w(TAG, TextUtils.formatSimple( "cache invalidate %s suppressed", name)); } return; } // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork. // The property service is single-threaded anyway, so we don't lose any concurrency by // taking the cork lock around cache invalidations. If we see contention on this lock, // we're invalidating too often. synchronized (sCorkLock) { Integer numberCorks = sCorks.get(name); if (numberCorks != null && numberCorks > 0) { if (DEBUG) { Log.d(TAG, "ignoring invalidation due to cork: " + name); } final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); sCorkedInvalidates.put(name, count + 1); return; } invalidateCacheLocked(name); } } @GuardedBy("sCorkLock") 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 = getNonce(name); if (nonce == NONCE_DISABLED) { if (DEBUG) { Log.d(TAG, "refusing to invalidate disabled cache: " + name); } return; } long newValue; do { newValue = NoPreloadHolder.next(); } while (isReservedNonce(newValue)); if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "invalidating cache [%s]: [%s] -> [%s]", name, nonce, Long.toString(newValue))); } // There is a small race with concurrent disables here. A compare-and-exchange // property operation would be required to eliminate the race condition. setNonce(name, newValue); long invalidateCount = sInvalidates.getOrDefault(name, (long) 0); sInvalidates.put(name, ++invalidateCount); } /** * Temporarily put the cache in the uninitialized state and prevent invalidations from * moving it out of that state: useful in cases where we want to avoid the overhead of a * large number of cache invalidations in a short time. While the cache is corked, clients * bypass the cache and talk to backing services directly. This property makes corking * correctness-preserving even if corked outside the lock that controls access to the * cache's backing service. * * corkInvalidations() and uncorkInvalidations() must be called in pairs. * * @param name Name of the cache-key property to cork * @hide */ public static void corkInvalidations(@NonNull String name) { if (!sEnabled) { if (DEBUG) { Log.w(TAG, TextUtils.formatSimple( "cache cork %s suppressed", name)); } return; } synchronized (sCorkLock) { int numberCorks = sCorks.getOrDefault(name, 0); if (DEBUG) { 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 // existing caches talk directly to their services while we've corked updates. // Make sure we don't clobber a disabled cache value. // TODO(dancol): we can skip this property write and leave the cache enabled if the // caller promises not to make observable changes to the cache backing state before // 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 = getNonce(name); if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) { setNonce(name, NONCE_CORKED); } } else { final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); sCorkedInvalidates.put(name, count + 1); } sCorks.put(name, numberCorks + 1); if (DEBUG) { Log.d(TAG, "corked: " + name); } } } /** * Undo the effect of a cork, allowing cache invalidations to proceed normally. * Removing the last cork on a cache name invalidates the cache by side effect, * transitioning it to normal operation (unless explicitly disabled system-wide). * * @param name Name of the cache-key property to uncork * @hide */ public static void uncorkInvalidations(@NonNull String name) { if (!sEnabled) { if (DEBUG) { Log.w(TAG, TextUtils.formatSimple( "cache uncork %s suppressed", name)); } return; } synchronized (sCorkLock) { int numberCorks = sCorks.getOrDefault(name, 0); if (DEBUG) { Log.d(TAG, TextUtils.formatSimple( "uncorking %s: numberCorks=%s", name, numberCorks)); } if (numberCorks < 1) { throw new AssertionError("cork underflow: " + name); } if (numberCorks == 1) { sCorks.remove(name); invalidateCacheLocked(name); if (DEBUG) { Log.d(TAG, "uncorked: " + name); } } else { sCorks.put(name, numberCorks - 1); } } } /** * Time-based automatic corking helper. This class allows providers of cached data to * amortize the cost of cache invalidations by corking the cache immediately after a * modification (instructing clients to bypass the cache temporarily) and automatically * uncork after some period of time has elapsed. * * It's better to use explicit cork and uncork pairs that tighly surround big batches of * invalidations, but it's not always practical to tell where these invalidation batches * might occur. AutoCorker's time-based corking is a decent alternative. * * The auto-cork delay is configurable but it should not be too long. The purpose of * the delay is to minimize the number of times a server writes to the system property * when invalidating the cache. One write every 50ms does not hurt system performance. * @hide */ public static final class AutoCorker { public static final int DEFAULT_AUTO_CORK_DELAY_MS = 50; private final String mPropertyName; private final int mAutoCorkDelayMs; private final Object mLock = new Object(); @GuardedBy("mLock") private long mUncorkDeadlineMs = -1; // SystemClock.uptimeMillis() @GuardedBy("mLock") private Handler mHandler; public AutoCorker(@NonNull String propertyName) { this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS); } public AutoCorker(@NonNull String propertyName, int autoCorkDelayMs) { mPropertyName = propertyName; mAutoCorkDelayMs = autoCorkDelayMs; // We can't initialize mHandler here: when we're created, the main loop might not // be set up yet! Wait until we have a main loop to initialize our // corking callback. } public void autoCork() { if (Looper.getMainLooper() == null) { // We're not ready to auto-cork yet, so just invalidate the cache immediately. if (DEBUG) { Log.w(TAG, "invalidating instead of autocorking early in init: " + mPropertyName); } PropertyInvalidatedCache.invalidateCache(mPropertyName); return; } synchronized (mLock) { boolean alreadyQueued = mUncorkDeadlineMs >= 0; if (DEBUG) { Log.w(TAG, TextUtils.formatSimple( "autoCork %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs; if (!alreadyQueued) { getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs); PropertyInvalidatedCache.corkInvalidations(mPropertyName); } else { synchronized (sCorkLock) { final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); sCorkedInvalidates.put(mPropertyName, count + 1); } } } } private void handleMessage(Message msg) { synchronized (mLock) { if (DEBUG) { Log.w(TAG, TextUtils.formatSimple( "handleMsesage %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } if (mUncorkDeadlineMs < 0) { return; // ??? } long nowMs = SystemClock.uptimeMillis(); if (mUncorkDeadlineMs > nowMs) { mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs; if (DEBUG) { Log.w(TAG, TextUtils.formatSimple( "scheduling uncork at %s", mUncorkDeadlineMs)); } getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs); return; } if (DEBUG) { Log.w(TAG, "automatic uncorking " + mPropertyName); } mUncorkDeadlineMs = -1; PropertyInvalidatedCache.uncorkInvalidations(mPropertyName); } } @GuardedBy("mLock") private Handler getHandlerLocked() { if (mHandler == null) { mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { AutoCorker.this.handleMessage(msg); } }; } return mHandler; } } /** * Return the result generated by a given query to the cache, performing debugging checks when * enabled. */ private Result maybeCheckConsistency(Query query, Result proposedResult) { if (VERIFY) { Result resultToCompare = recompute(query); boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce); if (!nonceChanged && !resultEquals(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; } return proposedResult; } /** * Return the name of the cache, to be used in debug messages. This is exposed * primarily for testing. * @hide */ public final @NonNull String cacheName() { return mCacheName; } /** * Return the property used by the cache. This is primarily for test purposes. * @hide */ public final @NonNull String propertyName() { return mPropertyName; } /** * Return the query as a string, to be used in debug messages. New clients should not * override this, but should instead add the necessary toString() method to the Query * class. * TODO(216112648) add a method in the QueryHandler and deprecate this API. * @hide */ protected @NonNull String queryToString(@NonNull Query query) { return Objects.toString(query); } /** * Disable all caches in the local process. This is primarily useful for testing when * the test needs to bypass the cache or when the test is for a server, and the test * process does not have privileges to write SystemProperties. Once disabled it is not * possible to re-enable caching in the current process. If a client wants to * temporarily disable caching, use the corking mechanism. * @hide */ @TestApi public static void disableForTestMode() { Log.d(TAG, "disabling all caches in the process"); sEnabled = false; } /** * Report the disabled status of this cache instance. The return value does not * reflect status of the property key. * @hide */ @TestApi public boolean getDisabledState() { return isDisabled(); } /** * Return the number of entries in the cache. This is used for testing and has package-only * visibility. * @hide */ public int size() { synchronized (mLock) { return mCache.size(); } } /** * Returns a list of caches alive at the current time. */ @GuardedBy("sGlobalLock") private static @NonNull ArrayList getActiveCaches() { return new ArrayList (sCaches.keySet()); } /** * Returns a list of the active corks in a process. */ private static @NonNull ArrayList > getActiveCorks() { synchronized (sCorkLock) { return new ArrayList >(sCorks.entrySet()); } } /** * Switches that can be used to control the detail emitted by a cache dump. The * "CONTAINS" switches match if the cache (property) name contains the switch * argument. The "LIKE" switches match if the cache (property) name matches the * switch argument as a regex. The regular expression must match the entire name, * which generally means it may need leading/trailing "." expressions. */ final static String NAME_CONTAINS = "-name-has="; final static String NAME_LIKE = "-name-like="; final static String PROPERTY_CONTAINS = "-property-has="; final static String PROPERTY_LIKE = "-property-like="; /** * Return true if any argument is a detailed specification switch. */ private static boolean anyDetailed(String[] args) { for (String a : args) { if (a.startsWith(NAME_CONTAINS) || a.startsWith(NAME_LIKE) || a.startsWith(PROPERTY_CONTAINS) || a.startsWith(PROPERTY_LIKE)) { return true; } } return false; } /** * A helper method to determine if a string matches a switch. */ private static boolean chooses(String arg, String key, String reference, boolean contains) { if (arg.startsWith(key)) { final String value = arg.substring(key.length()); if (contains) { return reference.contains(value); } else { return reference.matches(value); } } return false; } /** * Return true if this cache should be dumped in detail. This method is not called * unless it has already been determined that there is at least one match requested. */ private boolean showDetailed(String[] args) { for (String a : args) { if (chooses(a, NAME_CONTAINS, cacheName(), true) || chooses(a, NAME_LIKE, cacheName(), false) || chooses(a, PROPERTY_CONTAINS, mPropertyName, true) || chooses(a, PROPERTY_LIKE, mPropertyName, false)) { return true; } } return false; } private void dumpContents(PrintWriter pw, boolean detailed, String[] args) { // If the user has requested specific caches and this is not one of them, return // immediately. if (detailed && !showDetailed(args)) { return; } long invalidateCount; long corkedInvalidates; synchronized (sCorkLock) { invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0); corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); } synchronized (mLock) { 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(TextUtils.formatSimple( " Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d", mSkips[NONCE_CORKED], mSkips[NONCE_UNSET], mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED])); pw.println(TextUtils.formatSimple( " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", mLastSeenNonce, invalidateCount, corkedInvalidates)); pw.println(TextUtils.formatSimple( " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); pw.println(""); // No specific cache was requested. This is the default, and no details // should be dumped. if (!detailed) { return; } Set > cacheEntries = mCache.entrySet(); if (cacheEntries.size() == 0) { return; } pw.println(" Contents:"); for (Map.Entry entry : cacheEntries) { String key = Objects.toString(entry.getKey()); String value = Objects.toString(entry.getValue()); pw.println(TextUtils.formatSimple(" Key: %s\n Value: %s\n", key, value)); } } } /** * Dump the corking status. */ @GuardedBy("sCorkLock") private static void dumpCorkInfo(PrintWriter pw) { ArrayList > activeCorks = getActiveCorks(); if (activeCorks.size() > 0) { pw.println(" Corking Status:"); for (int i = 0; i < activeCorks.size(); i++) { Map.Entry entry = activeCorks.get(i); pw.println(TextUtils.formatSimple(" Property Name: %s Count: %d", entry.getKey(), entry.getValue())); } } } /** * Without arguments, this dumps statistics from every cache in the process to the * provided ParcelFileDescriptor. Optional switches allow the caller to choose * specific caches (selection is by cache name or property name); if these switches * are used then the output includes both cache statistics and cache entries. */ private static void dumpCacheInfo(@NonNull PrintWriter pw, @NonNull String[] args) { if (!sEnabled) { pw.println(" Caching is disabled in this process."); return; } // See if detailed is requested for any cache. If there is a specific detailed request, // then only that cache is reported. boolean detail = anyDetailed(args); ArrayList activeCaches; synchronized (sGlobalLock) { activeCaches = getActiveCaches(); if (!detail) { dumpCorkInfo(pw); } } for (int i = 0; i < activeCaches.size(); i++) { PropertyInvalidatedCache currentCache = activeCaches.get(i); currentCache.dumpContents(pw, detail, args); } } /** * Without arguments, this dumps statistics from every cache in the process to the * provided ParcelFileDescriptor. Optional switches allow the caller to choose * specific caches (selection is by cache name or property name); if these switches * are used then the output includes both cache statistics and cache entries. * @hide */ public static void dumpCacheInfo(@NonNull ParcelFileDescriptor pfd, @NonNull String[] args) { // Create a PrintWriter that uses a byte array. The code can safely write to // this array without fear of blocking. The completed byte array will be sent // to the caller after all the data has been collected and all locks have been // released. ByteArrayOutputStream barray = new ByteArrayOutputStream(); PrintWriter bout = new PrintWriter(barray); dumpCacheInfo(bout, args); bout.close(); try { // Send the final byte array to the output. This happens outside of all locks. var out = new FileOutputStream(pfd.getFileDescriptor()); barray.writeTo(out); out.close(); barray.close(); } catch (IOException e) { Log.e(TAG, "Failed to dump PropertyInvalidatedCache instances"); } } /** * Trim memory by clearing all the caches. * @hide */ public static void onTrimMemory() { ArrayList activeCaches; synchronized (sGlobalLock) { activeCaches = getActiveCaches(); } for (int i = 0; i < activeCaches.size(); i++) { activeCaches.get(i).clear(); } } }