1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app;
18 
19 import android.annotation.NonNull;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.Message;
23 import android.os.SystemClock;
24 import android.os.SystemProperties;
25 import android.util.Log;
26 
27 import com.android.internal.annotations.GuardedBy;
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.util.FastPrintWriter;
30 
31 import java.io.FileDescriptor;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.PrintWriter;
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.LinkedHashMap;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Random;
41 import java.util.Set;
42 import java.util.WeakHashMap;
43 import java.util.concurrent.atomic.AtomicLong;
44 
45 /**
46  * LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
47  * but doesn't hold a lock across data fetches on query misses.
48  *
49  * The intended use case is caching frequently-read, seldom-changed information normally
50  * retrieved across interprocess communication. Imagine that you've written a user birthday
51  * information daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface
52  * over binder. That binder interface looks something like this:
53  *
54  * <pre>
55  * parcelable Birthday {
56  *   int month;
57  *   int day;
58  * }
59  * interface IUserBirthdayService {
60  *   Birthday getUserBirthday(int userId);
61  * }
62  * </pre>
63  *
64  * Suppose the service implementation itself looks like this...
65  *
66  * <pre>
67  * public class UserBirthdayServiceImpl implements IUserBirthdayService {
68  *   private final HashMap<Integer, Birthday> mUidToBirthday;
69  *   @Override
70  *   public synchronized Birthday getUserBirthday(int userId) {
71  *     return mUidToBirthday.get(userId);
72  *   }
73  *   private synchronized void updateBirthdays(Map<Integer, Birthday> uidToBirthday) {
74  *     mUidToBirthday.clear();
75  *     mUidToBirthday.putAll(uidToBirthday);
76  *   }
77  * }
78  * </pre>
79  *
80  * ... and we have a client in frameworks (loaded into every app process) that looks
81  * like this:
82  *
83  * <pre>
84  * public class ActivityThread {
85  *   ...
86  *   public Birthday getUserBirthday(int userId) {
87  *     return GetService("birthdayd").getUserBirthday(userId);
88  *   }
89  *   ...
90  * }
91  * </pre>
92  *
93  * With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call
94  * to the birthdayd process and consult its database of birthdays. If we query user birthdays
95  * frequently, we do a lot of work that we don't have to do, since user birthdays
96  * change infrequently.
97  *
98  * PropertyInvalidatedCache is part of a pattern for optimizing this kind of
99  * information-querying code. Using {@code PropertyInvalidatedCache}, you'd write the client
100  * this way:
101  *
102  * <pre>
103  * public class ActivityThread {
104  *   ...
105  *   private static final int BDAY_CACHE_MAX = 8;  // Maximum birthdays to cache
106  *   private static final String BDAY_CACHE_KEY = "cache_key.birthdayd";
107  *   private final PropertyInvalidatedCache<Integer, Birthday> mBirthdayCache = new
108  *     PropertyInvalidatedCache<Integer, Birthday>(BDAY_CACHE_MAX, BDAY_CACHE_KEY) {
109  *       @Override
110  *       protected Birthday recompute(Integer userId) {
111  *         return GetService("birthdayd").getUserBirthday(userId);
112  *       }
113  *     };
114  *   public void disableUserBirthdayCache() {
115  *     mBirthdayCache.disableLocal();
116  *   }
117  *   public void invalidateUserBirthdayCache() {
118  *     mBirthdayCache.invalidateCache();
119  *   }
120  *   public Birthday getUserBirthday(int userId) {
121  *     return mBirthdayCache.query(userId);
122  *   }
123  *   ...
124  * }
125  * </pre>
126  *
127  * With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
128  * for the first time; on subsequent queries, we return the already-known Birthday object.
129  *
130  * User birthdays do occasionally change, so we have to modify the server to invalidate this
131  * cache when necessary. That invalidation code looks like this:
132  *
133  * <pre>
134  * public class UserBirthdayServiceImpl {
135  *   ...
136  *   public UserBirthdayServiceImpl() {
137  *     ...
138  *     ActivityThread.currentActivityThread().disableUserBirthdayCache();
139  *     ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
140  *   }
141  *
142  *   private synchronized void updateBirthdays(Map<Integer, Birthday> uidToBirthday) {
143  *     mUidToBirthday.clear();
144  *     mUidToBirthday.putAll(uidToBirthday);
145  *     ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
146  *   }
147  *   ...
148  * }
149  * </pre>
150  *
151  * The call to {@code PropertyInvalidatedCache.invalidateCache()} guarantees that all clients
152  * will re-fetch birthdays from binder during consequent calls to
153  * {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock
154  * held, we maintain consistency between different client views of the birthday state. The use
155  * of PropertyInvalidatedCache in this idiomatic way introduces no new race conditions.
156  *
157  * PropertyInvalidatedCache has a few other features for doing things like incremental
158  * enhancement of cached values and invalidation of multiple caches (that all share the same
159  * property key) at once.
160  *
161  * {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each
162  * time we update the cache. SELinux configuration must allow everyone to read this property
163  * and it must allow any process that needs to invalidate the cache (here, birthdayd) to write
164  * the property. (These properties conventionally begin with the "cache_key." prefix.)
165  *
166  * The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so
167  * that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In
168  * this local case, there's no IPC, so use of the cache is (depending on exact
169  * circumstance) unnecessary.
170  *
171  * For security, there is a whitelist of processes that are allowed to invalidate a cache.
172  * The whitelist includes normal runtime processes but does not include test processes.
173  * Test processes must call {@code PropertyInvalidatedCache.disableForTestMode()} to disable
174  * all cache activity in that process.
175  *
176  * Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding.
177  *
178  * @param <Query> The class used to index cache entries: must be hashable and comparable
179  * @param <Result> The class holding cache entries; use a boxed primitive if possible
180  *
181  * {@hide}
182  */
183 public abstract class PropertyInvalidatedCache<Query, Result> {
184     private static final long NONCE_UNSET = 0;
185     private static final long NONCE_DISABLED = -1;
186 
187     private static final String TAG = "PropertyInvalidatedCache";
188     private static final boolean DEBUG = false;
189     private static final boolean VERIFY = false;
190 
191     // Per-Cache performance counters. As some cache instances are declared static,
192     @GuardedBy("mLock")
193     private long mHits = 0;
194 
195     @GuardedBy("mLock")
196     private long mMisses = 0;
197 
198     // Most invalidation is done in a static context, so the counters need to be accessible.
199     @GuardedBy("sCorkLock")
200     private static final HashMap<String, Long> sInvalidates = new HashMap<>();
201 
202     /**
203      * If sEnabled is false then all cache operations are stubbed out.  Set
204      * it to false inside test processes.
205      */
206     private static boolean sEnabled = true;
207 
208     private static final Object sCorkLock = new Object();
209 
210     /**
211      * A map of cache keys that we've "corked". (The values are counts.)  When a cache key is
212      * corked, we skip the cache invalidate when the cache key is in the unset state --- that
213      * is, when a cache key is corked, an invalidation does not enable the cache if somebody
214      * else hasn't disabled it.
215      */
216     @GuardedBy("sCorkLock")
217     private static final HashMap<String, Integer> sCorks = new HashMap<>();
218 
219     /**
220      * Weakly references all cache objects in the current process, allowing us to iterate over
221      * them all for purposes like issuing debug dumps and reacting to memory pressure.
222      */
223     @GuardedBy("sCorkLock")
224     private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches =
225             new WeakHashMap<>();
226 
227     private final Object mLock = new Object();
228 
229     /**
230      * Name of the property that holds the unique value that we use to invalidate the cache.
231      */
232     private final String mPropertyName;
233 
234     /**
235      * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the
236      * property exists on the system.
237      */
238     private volatile SystemProperties.Handle mPropertyHandle;
239 
240     @GuardedBy("mLock")
241     private final LinkedHashMap<Query, Result> mCache;
242 
243     /**
244      * The last value of the {@code mPropertyHandle} that we observed.
245      */
246     @GuardedBy("mLock")
247     private long mLastSeenNonce = NONCE_UNSET;
248 
249     /**
250      * Whether we've disabled the cache in this process.
251      */
252     private boolean mDisabled = false;
253 
254     /**
255      * Maximum number of entries the cache will maintain.
256      */
257     private final int mMaxEntries;
258 
259     /**
260      * Make a new property invalidated cache.
261      *
262      * @param maxEntries Maximum number of entries to cache; LRU discard
263      * @param propertyName Name of the system property holding the cache invalidation nonce
264      */
PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName)265     public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) {
266         mPropertyName = propertyName;
267         mMaxEntries = maxEntries;
268         mCache = new LinkedHashMap<Query, Result>(
269             2 /* start small */,
270             0.75f /* default load factor */,
271             true /* LRU access order */) {
272                 @Override
273                 protected boolean removeEldestEntry(Map.Entry eldest) {
274                     return size() > maxEntries;
275                 }
276             };
277         synchronized (sCorkLock) {
278             sCaches.put(this, null);
279             sInvalidates.put(propertyName, (long) 0);
280         }
281     }
282 
283     /**
284      * Forget all cached values.
285      */
clear()286     public final void clear() {
287         synchronized (mLock) {
288             if (DEBUG) {
289                 Log.d(TAG, "clearing cache for " + mPropertyName);
290             }
291             mCache.clear();
292         }
293     }
294 
295     /**
296      * Fetch a result from scratch in case it's not in the cache at all.  Called unlocked: may
297      * block. If this function returns null, the result of the cache query is null. There is no
298      * "negative cache" in the query: we don't cache null results at all.
299      */
recompute(Query query)300     protected abstract Result recompute(Query query);
301 
302     /**
303      * Determines if a pair of responses are considered equal. Used to determine whether
304      * a cache is inadvertently returning stale results when VERIFY is set to true.
305      */
debugCompareQueryResults(Result cachedResult, Result fetchedResult)306     protected boolean debugCompareQueryResults(Result cachedResult, Result fetchedResult) {
307         // If a service crashes and returns a null result, the cached value remains valid.
308         if (fetchedResult != null) {
309             return Objects.equals(cachedResult, fetchedResult);
310         }
311         return true;
312     }
313 
314     /**
315      * Make result up-to-date on a cache hit.  Called unlocked;
316      * may block.
317      *
318      * Return either 1) oldResult itself (the same object, by reference equality), in which
319      * case we just return oldResult as the result of the cache query, 2) a new object, which
320      * replaces oldResult in the cache and which we return as the result of the cache query
321      * after performing another property read to make sure that the result hasn't changed in
322      * the meantime (if the nonce has changed in the meantime, we drop the cache and try the
323      * whole query again), or 3) null, which causes the old value to be removed from the cache
324      * and null to be returned as the result of the cache query.
325      */
refresh(Result oldResult, Query query)326     protected Result refresh(Result oldResult, Query query) {
327         return oldResult;
328     }
329 
getCurrentNonce()330     private long getCurrentNonce() {
331         SystemProperties.Handle handle = mPropertyHandle;
332         if (handle == null) {
333             handle = SystemProperties.find(mPropertyName);
334             if (handle == null) {
335                 return NONCE_UNSET;
336             }
337             mPropertyHandle = handle;
338         }
339         return handle.getLong(NONCE_UNSET);
340     }
341 
342     /**
343      * Disable the use of this cache in this process.
344      */
disableLocal()345     public final void disableLocal() {
346         synchronized (mLock) {
347             mDisabled = true;
348             mCache.clear();
349         }
350     }
351 
352     /**
353      * Return whether the cache is disabled in this process.
354      */
isDisabledLocal()355     public final boolean isDisabledLocal() {
356         return mDisabled || !sEnabled;
357     }
358 
359     /**
360      * Get a value from the cache or recompute it.
361      */
query(Query query)362     public Result query(Query query) {
363         // Let access to mDisabled race: it's atomic anyway.
364         long currentNonce = (!isDisabledLocal()) ? getCurrentNonce() : NONCE_DISABLED;
365         for (;;) {
366             if (currentNonce == NONCE_DISABLED || currentNonce == NONCE_UNSET) {
367                 if (DEBUG) {
368                     Log.d(TAG,
369                             String.format("cache %s %s for %s",
370                                 cacheName(),
371                                 currentNonce == NONCE_DISABLED ? "disabled" : "unset",
372                                 queryToString(query)));
373                 }
374                 return recompute(query);
375             }
376             final Result cachedResult;
377             synchronized (mLock) {
378                 if (currentNonce == mLastSeenNonce) {
379                     cachedResult = mCache.get(query);
380 
381                     if (cachedResult != null) mHits++;
382                 } else {
383                     if (DEBUG) {
384                         Log.d(TAG,
385                                 String.format("clearing cache %s because nonce changed [%s] -> [%s]",
386                                         cacheName(),
387                                         mLastSeenNonce, currentNonce));
388                     }
389                     mCache.clear();
390                     mLastSeenNonce = currentNonce;
391                     cachedResult = null;
392                 }
393             }
394             // Cache hit --- but we're not quite done yet.  A value in the cache might need to
395             // be augmented in a "refresh" operation.  The refresh operation can combine the
396             // old and the new nonce values.  In order to make sure the new parts of the value
397             // are consistent with the old, possibly-reused parts, we check the property value
398             // again after the refresh and do the whole fetch again if the property invalidated
399             // us while we were refreshing.
400             if (cachedResult != null) {
401                 final Result refreshedResult = refresh(cachedResult, query);
402                 if (refreshedResult != cachedResult) {
403                     if (DEBUG) {
404                         Log.d(TAG, "cache refresh for " + cacheName() + " " + queryToString(query));
405                     }
406                     final long afterRefreshNonce = getCurrentNonce();
407                     if (currentNonce != afterRefreshNonce) {
408                         currentNonce = afterRefreshNonce;
409                         if (DEBUG) {
410                             Log.d(TAG, String.format("restarting %s %s because nonce changed in refresh",
411                                                      cacheName(),
412                                                      queryToString(query)));
413                         }
414                         continue;
415                     }
416                     synchronized (mLock) {
417                         if (currentNonce != mLastSeenNonce) {
418                             // Do nothing: cache is already out of date. Just return the value
419                             // we already have: there's no guarantee that the contents of mCache
420                             // won't become invalid as soon as we return.
421                         } else if (refreshedResult == null) {
422                             mCache.remove(query);
423                         } else {
424                             mCache.put(query, refreshedResult);
425                         }
426                     }
427                     return maybeCheckConsistency(query, refreshedResult);
428                 }
429                 if (DEBUG) {
430                     Log.d(TAG, "cache hit for " + cacheName() + " " + queryToString(query));
431                 }
432                 return maybeCheckConsistency(query, cachedResult);
433             }
434             // Cache miss: make the value from scratch.
435             if (DEBUG) {
436                 Log.d(TAG, "cache miss for " + cacheName() + " " + queryToString(query));
437             }
438             final Result result = recompute(query);
439             synchronized (mLock) {
440                 // If someone else invalidated the cache while we did the recomputation, don't
441                 // update the cache with a potentially stale result.
442                 if (mLastSeenNonce == currentNonce && result != null) {
443                     mCache.put(query, result);
444                 }
445                 mMisses++;
446             }
447             return maybeCheckConsistency(query, result);
448         }
449     }
450 
451     // Inner class avoids initialization in processes that don't do any invalidation
452     private static final class NoPreloadHolder {
453         private static final AtomicLong sNextNonce = new AtomicLong((new Random()).nextLong());
next()454         public static long next() {
455             return sNextNonce.getAndIncrement();
456         }
457     }
458 
459     /**
460      * Non-static convenience version of disableSystemWide() for situations in which only a
461      * single PropertyInvalidatedCache is keyed on a particular property value.
462      *
463      * When multiple caches share a single property value, using an instance method on one of
464      * the cache objects to invalidate all of the cache objects becomes confusing and you should
465      * just use the static version of this function.
466      */
disableSystemWide()467     public final void disableSystemWide() {
468         disableSystemWide(mPropertyName);
469     }
470 
471     /**
472      * Disable all caches system-wide that are keyed on {@var name}. This
473      * function is synchronous: caches are invalidated and disabled upon return.
474      *
475      * @param name Name of the cache-key property to invalidate
476      */
disableSystemWide(@onNull String name)477     public static void disableSystemWide(@NonNull String name) {
478         if (!sEnabled) {
479             return;
480         }
481         SystemProperties.set(name, Long.toString(NONCE_DISABLED));
482     }
483 
484     /**
485      * Non-static convenience version of invalidateCache() for situations in which only a single
486      * PropertyInvalidatedCache is keyed on a particular property value.
487      */
invalidateCache()488     public final void invalidateCache() {
489         invalidateCache(mPropertyName);
490     }
491 
492     /**
493      * Invalidate PropertyInvalidatedCache caches in all processes that are keyed on
494      * {@var name}. This function is synchronous: caches are invalidated upon return.
495      *
496      * @param name Name of the cache-key property to invalidate
497      */
invalidateCache(@onNull String name)498     public static void invalidateCache(@NonNull String name) {
499         if (!sEnabled) {
500             if (DEBUG) {
501                 Log.w(TAG, String.format(
502                     "cache invalidate %s suppressed", name));
503             }
504             return;
505         }
506 
507         // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't
508         // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork.
509         // The property service is single-threaded anyway, so we don't lose any concurrency by
510         // taking the cork lock around cache invalidations.  If we see contention on this lock,
511         // we're invalidating too often.
512         synchronized (sCorkLock) {
513             Integer numberCorks = sCorks.get(name);
514             if (numberCorks != null && numberCorks > 0) {
515                 if (DEBUG) {
516                     Log.d(TAG, "ignoring invalidation due to cork: " + name);
517                 }
518                 return;
519             }
520             invalidateCacheLocked(name);
521         }
522     }
523 
524     @GuardedBy("sCorkLock")
invalidateCacheLocked(@onNull String name)525     private static void invalidateCacheLocked(@NonNull String name) {
526         // There's no race here: we don't require that values strictly increase, but instead
527         // only that each is unique in a single runtime-restart session.
528         final long nonce = SystemProperties.getLong(name, NONCE_UNSET);
529         if (nonce == NONCE_DISABLED) {
530             if (DEBUG) {
531                 Log.d(TAG, "refusing to invalidate disabled cache: " + name);
532             }
533             return;
534         }
535 
536         long newValue;
537         do {
538             newValue = NoPreloadHolder.next();
539         } while (newValue == NONCE_UNSET || newValue == NONCE_DISABLED);
540         final String newValueString = Long.toString(newValue);
541         if (DEBUG) {
542             Log.d(TAG,
543                     String.format("invalidating cache [%s]: [%s] -> [%s]",
544                             name,
545                             nonce,
546                             newValueString));
547         }
548         SystemProperties.set(name, newValueString);
549         long invalidateCount = sInvalidates.getOrDefault(name, (long) 0);
550         sInvalidates.put(name, ++invalidateCount);
551     }
552 
553     /**
554      * Temporarily put the cache in the uninitialized state and prevent invalidations from
555      * moving it out of that state: useful in cases where we want to avoid the overhead of a
556      * large number of cache invalidations in a short time.  While the cache is corked, clients
557      * bypass the cache and talk to backing services directly.  This property makes corking
558      * correctness-preserving even if corked outside the lock that controls access to the
559      * cache's backing service.
560      *
561      * corkInvalidations() and uncorkInvalidations() must be called in pairs.
562      *
563      * @param name Name of the cache-key property to cork
564      */
corkInvalidations(@onNull String name)565     public static void corkInvalidations(@NonNull String name) {
566         synchronized (sCorkLock) {
567             int numberCorks = sCorks.getOrDefault(name, 0);
568             if (DEBUG) {
569                 Log.d(TAG, String.format("corking %s: numberCorks=%s", name, numberCorks));
570             }
571 
572             // If we're the first ones to cork this cache, set the cache to the unset state so
573             // existing caches talk directly to their services while we've corked updates.
574             // Make sure we don't clobber a disabled cache value.
575 
576             // TODO(dancol): we can skip this property write and leave the cache enabled if the
577             // caller promises not to make observable changes to the cache backing state before
578             // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
579             // Implement this more dangerous mode of operation if necessary.
580             if (numberCorks == 0) {
581                 final long nonce = SystemProperties.getLong(name, NONCE_UNSET);
582                 if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
583                     SystemProperties.set(name, Long.toString(NONCE_UNSET));
584                 }
585             }
586             sCorks.put(name, numberCorks + 1);
587             if (DEBUG) {
588                 Log.d(TAG, "corked: " + name);
589             }
590         }
591     }
592 
593     /**
594      * Undo the effect of a cork, allowing cache invalidations to proceed normally.
595      * Removing the last cork on a cache name invalidates the cache by side effect,
596      * transitioning it to normal operation (unless explicitly disabled system-wide).
597      *
598      * @param name Name of the cache-key property to uncork
599      */
uncorkInvalidations(@onNull String name)600     public static void uncorkInvalidations(@NonNull String name) {
601         synchronized (sCorkLock) {
602             int numberCorks = sCorks.getOrDefault(name, 0);
603             if (DEBUG) {
604                 Log.d(TAG, String.format("uncorking %s: numberCorks=%s", name, numberCorks));
605             }
606 
607             if (numberCorks < 1) {
608                 throw new AssertionError("cork underflow: " + name);
609             }
610             if (numberCorks == 1) {
611                 sCorks.remove(name);
612                 invalidateCacheLocked(name);
613                 if (DEBUG) {
614                     Log.d(TAG, "uncorked: " + name);
615                 }
616             } else {
617                 sCorks.put(name, numberCorks - 1);
618             }
619         }
620     }
621 
622     /**
623      * Time-based automatic corking helper. This class allows providers of cached data to
624      * amortize the cost of cache invalidations by corking the cache immediately after a
625      * modification (instructing clients to bypass the cache temporarily) and automatically
626      * uncork after some period of time has elapsed.
627      *
628      * It's better to use explicit cork and uncork pairs that tighly surround big batches of
629      * invalidations, but it's not always practical to tell where these invalidation batches
630      * might occur. AutoCorker's time-based corking is a decent alternative.
631      */
632     public static final class AutoCorker {
633         public static final int DEFAULT_AUTO_CORK_DELAY_MS = 2000;
634 
635         private final String mPropertyName;
636         private final int mAutoCorkDelayMs;
637         private final Object mLock = new Object();
638         @GuardedBy("mLock")
639         private long mUncorkDeadlineMs = -1;  // SystemClock.uptimeMillis()
640         @GuardedBy("mLock")
641         private Handler mHandler;
642 
AutoCorker(@onNull String propertyName)643         public AutoCorker(@NonNull String propertyName) {
644             this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS);
645         }
646 
AutoCorker(@onNull String propertyName, int autoCorkDelayMs)647         public AutoCorker(@NonNull String propertyName, int autoCorkDelayMs) {
648             mPropertyName = propertyName;
649             mAutoCorkDelayMs = autoCorkDelayMs;
650             // We can't initialize mHandler here: when we're created, the main loop might not
651             // be set up yet! Wait until we have a main loop to initialize our
652             // corking callback.
653         }
654 
autoCork()655         public void autoCork() {
656             if (Looper.getMainLooper() == null) {
657                 // We're not ready to auto-cork yet, so just invalidate the cache immediately.
658                 if (DEBUG) {
659                     Log.w(TAG, "invalidating instead of autocorking early in init: "
660                             + mPropertyName);
661                 }
662                 PropertyInvalidatedCache.invalidateCache(mPropertyName);
663                 return;
664             }
665             synchronized (mLock) {
666                 boolean alreadyQueued = mUncorkDeadlineMs >= 0;
667                 if (DEBUG) {
668                     Log.w(TAG, String.format(
669                                     "autoCork mUncorkDeadlineMs=%s", mUncorkDeadlineMs));
670                 }
671                 mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs;
672                 if (!alreadyQueued) {
673                     getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
674                     PropertyInvalidatedCache.corkInvalidations(mPropertyName);
675                 }
676             }
677         }
678 
handleMessage(Message msg)679         private void handleMessage(Message msg) {
680             synchronized (mLock) {
681                 if (DEBUG) {
682                     Log.w(TAG, String.format(
683                                     "handleMsesage mUncorkDeadlineMs=%s", mUncorkDeadlineMs));
684                 }
685 
686                 if (mUncorkDeadlineMs < 0) {
687                     return;  // ???
688                 }
689                 long nowMs = SystemClock.uptimeMillis();
690                 if (mUncorkDeadlineMs > nowMs) {
691                     mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs;
692                     if (DEBUG) {
693                         Log.w(TAG, String.format(
694                                         "scheduling uncork at %s",
695                                         mUncorkDeadlineMs));
696                     }
697                     getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
698                     return;
699                 }
700                 if (DEBUG) {
701                     Log.w(TAG, "automatic uncorking " + mPropertyName);
702                 }
703                 mUncorkDeadlineMs = -1;
704                 PropertyInvalidatedCache.uncorkInvalidations(mPropertyName);
705             }
706         }
707 
708         @GuardedBy("mLock")
getHandlerLocked()709         private Handler getHandlerLocked() {
710             if (mHandler == null) {
711                 mHandler = new Handler(Looper.getMainLooper()) {
712                         @Override
713                         public void handleMessage(Message msg) {
714                             AutoCorker.this.handleMessage(msg);
715                         }
716                     };
717             }
718             return mHandler;
719         }
720     }
721 
maybeCheckConsistency(Query query, Result proposedResult)722     protected Result maybeCheckConsistency(Query query, Result proposedResult) {
723         if (VERIFY) {
724             Result resultToCompare = recompute(query);
725             boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce);
726             if (!nonceChanged && !debugCompareQueryResults(proposedResult, resultToCompare)) {
727                 throw new AssertionError("cache returned out of date response for " + query);
728             }
729         }
730         return proposedResult;
731     }
732 
733     /**
734      * Return the name of the cache, to be used in debug messages.  The
735      * method is public so clients can use it.
736      */
cacheName()737     public String cacheName() {
738         return mPropertyName;
739     }
740 
741     /**
742      * Return the query as a string, to be used in debug messages.  The
743      * method is public so clients can use it in external debug messages.
744      */
queryToString(Query query)745     public String queryToString(Query query) {
746         return Objects.toString(query);
747     }
748 
749     /**
750      * Disable all caches in the local process.  Once disabled it is not
751      * possible to re-enable caching in the current process.
752      */
753     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
disableForTestMode()754     public static void disableForTestMode() {
755         Log.d(TAG, "disabling all caches in the process");
756         sEnabled = false;
757     }
758 
759     /**
760      * Returns a list of caches alive at the current time.
761      */
getActiveCaches()762     public static ArrayList<PropertyInvalidatedCache> getActiveCaches() {
763         synchronized (sCorkLock) {
764             return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet());
765         }
766     }
767 
768     /**
769      * Returns a list of the active corks in a process.
770      */
getActiveCorks()771     public static ArrayList<Map.Entry<String, Integer>> getActiveCorks() {
772         synchronized (sCorkLock) {
773             return new ArrayList<Map.Entry<String, Integer>>(sCorks.entrySet());
774         }
775     }
776 
dumpContents(PrintWriter pw, String[] args)777     private void dumpContents(PrintWriter pw, String[] args) {
778         long invalidateCount;
779 
780         synchronized (sCorkLock) {
781             invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0);
782         }
783 
784         synchronized (mLock) {
785             pw.println(String.format("  Cache Property Name: %s", cacheName()));
786             pw.println(String.format("    Hits: %d, Misses: %d, Invalidates: %d",
787                     mHits, mMisses, invalidateCount));
788             pw.println(String.format("    Last Observed Nonce: %d", mLastSeenNonce));
789             pw.println(String.format("    Current Size: %d, Max Size: %d",
790                     mCache.entrySet().size(), mMaxEntries));
791             pw.println(String.format("    Enabled: %s", mDisabled ? "false" : "true"));
792 
793             Set<Map.Entry<Query, Result>> cacheEntries = mCache.entrySet();
794             if (cacheEntries.size() == 0) {
795                 pw.println("");
796                 return;
797             }
798 
799             pw.println("");
800             pw.println("    Contents:");
801             for (Map.Entry<Query, Result> entry : cacheEntries) {
802                 String key = Objects.toString(entry.getKey());
803                 String value = Objects.toString(entry.getValue());
804 
805                 pw.println(String.format("      Key: %s\n      Value: %s\n", key, value));
806             }
807         }
808     }
809 
810     /**
811      * Dumps contents of every cache in the process to the provided FileDescriptor.
812      */
dumpCacheInfo(FileDescriptor fd, String[] args)813     public static void dumpCacheInfo(FileDescriptor fd, String[] args) {
814         ArrayList<PropertyInvalidatedCache> activeCaches;
815         ArrayList<Map.Entry<String, Integer>> activeCorks;
816 
817         try  (
818             FileOutputStream fout = new FileOutputStream(fd);
819             PrintWriter pw = new FastPrintWriter(fout);
820         ) {
821             if (!sEnabled) {
822                 pw.println("  Caching is disabled in this process.");
823                 return;
824             }
825 
826             synchronized (sCorkLock) {
827                 activeCaches = getActiveCaches();
828                 activeCorks = getActiveCorks();
829 
830                 if (activeCorks.size() > 0) {
831                     pw.println("  Corking Status:");
832                     for (int i = 0; i < activeCorks.size(); i++) {
833                         Map.Entry<String, Integer> entry = activeCorks.get(i);
834                         pw.println(String.format("    Property Name: %s Count: %d",
835                                 entry.getKey(), entry.getValue()));
836                     }
837                 }
838             }
839 
840             for (int i = 0; i < activeCaches.size(); i++) {
841                 PropertyInvalidatedCache currentCache = activeCaches.get(i);
842                 currentCache.dumpContents(pw, args);
843                 pw.flush();
844             }
845         } catch (IOException e) {
846             Log.e(TAG, "Failed to dump PropertyInvalidatedCache instances");
847         }
848     }
849 }
850