• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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 com.android.email.provider;
18 
19 import android.content.ContentValues;
20 import android.database.CrossProcessCursor;
21 import android.database.Cursor;
22 import android.database.CursorWindow;
23 import android.database.CursorWrapper;
24 import android.database.MatrixCursor;
25 import android.net.Uri;
26 import android.util.Log;
27 import android.util.LruCache;
28 
29 import com.android.email.Email;
30 import com.google.common.annotations.VisibleForTesting;
31 
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.HashMap;
35 import java.util.Map;
36 import java.util.Set;
37 
38 /**
39  * An LRU cache for EmailContent (Account, HostAuth, Mailbox, and Message, thus far).  The intended
40  * user of this cache is EmailProvider itself; caching is entirely transparent to users of the
41  * provider.
42  *
43  * Usage examples; id is a String representation of a row id (_id), as it might be retrieved from
44  * a uri via getPathSegment
45  *
46  * To create a cache:
47  *    ContentCache cache = new ContentCache(name, projection, max);
48  *
49  * To (try to) get a cursor from a cache:
50  *    Cursor cursor = cache.getCursor(id, projection);
51  *
52  * To read from a table and cache the resulting cursor:
53  * 1. Get a CacheToken: CacheToken token = cache.getToken(id);
54  * 2. Get a cursor from the database: Cursor cursor = db.query(....);
55  * 3. Put the cursor in the cache: cache.putCursor(cursor, id, token);
56  * Only cursors with the projection given in the definition of the cache can be cached
57  *
58  * To delete one or more rows or update multiple rows from a table that uses cached data:
59  * 1. Lock the row in the cache: cache.lock(id);
60  * 2. Delete/update the row(s): db.delete(...);
61  * 3. Invalidate any other caches that might be affected by the delete/update:
62  *      The entire cache: affectedCache.invalidate()*
63  *      A specific row in a cache: affectedCache.invalidate(rowId)
64  * 4. Unlock the row in the cache: cache.unlock(id);
65  *
66  * To update a single row from a table that uses cached data:
67  * 1. Lock the row in the cache: cache.lock(id);
68  * 2. Update the row: db.update(...);
69  * 3. Unlock the row in the cache, passing in the new values: cache.unlock(id, values);
70  *
71  * Synchronization note: All of the public methods in ContentCache are synchronized (i.e. on the
72  * cache itself) except for methods that are solely used for debugging and do not modify the cache.
73  * All references to ContentCache that are external to the ContentCache class MUST synchronize on
74  * the ContentCache instance (e.g. CachedCursor.close())
75  */
76 public final class ContentCache {
77     private static final boolean DEBUG_CACHE = false;  // DO NOT CHECK IN TRUE
78     private static final boolean DEBUG_TOKENS = false;  // DO NOT CHECK IN TRUE
79     private static final boolean DEBUG_NOT_CACHEABLE = false;  // DO NOT CHECK IN TRUE
80     private static final boolean DEBUG_STATISTICS = false; // DO NOT CHECK THIS IN TRUE
81 
82     // If false, reads will not use the cache; this is intended for debugging only
83     private static final boolean READ_CACHE_ENABLED = true;  // DO NOT CHECK IN FALSE
84 
85     // Count of non-cacheable queries (debug only)
86     private static int sNotCacheable = 0;
87     // A map of queries that aren't cacheable (debug only)
88     private static final CounterMap<String> sNotCacheableMap = new CounterMap<String>();
89 
90     private final LruCache<String, Cursor> mLruCache;
91 
92     // All defined caches
93     private static final ArrayList<ContentCache> sContentCaches = new ArrayList<ContentCache>();
94     // A set of all unclosed, cached cursors; this will typically be a very small set, as cursors
95     // tend to be closed quickly after use.  The value, for each cursor, is its reference count
96     /*package*/ static final CounterMap<Cursor> sActiveCursors = new CounterMap<Cursor>(24);
97 
98     // A set of locked content id's
99     private final CounterMap<String> mLockMap = new CounterMap<String>(4);
100     // A set of active tokens
101     /*package*/ TokenList mTokenList;
102 
103     // The name of the cache (used for logging)
104     private final String mName;
105     // The base projection (only queries in which all columns exist in this projection will be
106     // able to avoid a cache miss)
107     private final String[] mBaseProjection;
108     // The tag used for logging
109     private final String mLogTag;
110     // Cache statistics
111     private final Statistics mStats;
112     /** If {@code true}, lock the cache for all writes */
113     private static boolean sLockCache;
114 
115     /**
116      * A synchronized reference counter for arbitrary objects
117      */
118     /*package*/ static class CounterMap<T> {
119         private HashMap<T, Integer> mMap;
120 
CounterMap(int maxSize)121         /*package*/ CounterMap(int maxSize) {
122             mMap = new HashMap<T, Integer>(maxSize);
123         }
124 
CounterMap()125         /*package*/ CounterMap() {
126             mMap = new HashMap<T, Integer>();
127         }
128 
subtract(T object)129         /*package*/ synchronized int subtract(T object) {
130             Integer refCount = mMap.get(object);
131             int newCount;
132             if (refCount == null || refCount.intValue() == 0) {
133                 throw new IllegalStateException();
134             }
135             if (refCount > 1) {
136                 newCount = refCount - 1;
137                 mMap.put(object, newCount);
138             } else {
139                 newCount = 0;
140                 mMap.remove(object);
141             }
142             return newCount;
143         }
144 
add(T object)145         /*package*/ synchronized void add(T object) {
146             Integer refCount = mMap.get(object);
147             if (refCount == null) {
148                 mMap.put(object, 1);
149             } else {
150                 mMap.put(object, refCount + 1);
151             }
152         }
153 
contains(T object)154         /*package*/ synchronized boolean contains(T object) {
155             return mMap.containsKey(object);
156         }
157 
getCount(T object)158         /*package*/ synchronized int getCount(T object) {
159             Integer refCount = mMap.get(object);
160             return (refCount == null) ? 0 : refCount.intValue();
161         }
162 
size()163         synchronized int size() {
164             return mMap.size();
165         }
166 
167         /**
168          * For Debugging Only - not efficient
169          */
entrySet()170         synchronized Set<HashMap.Entry<T, Integer>> entrySet() {
171             return mMap.entrySet();
172         }
173     }
174 
175     /**
176      * A list of tokens that are in use at any moment; there can be more than one token for an id
177      */
178     /*package*/ static class TokenList extends ArrayList<CacheToken> {
179         private static final long serialVersionUID = 1L;
180         private final String mLogTag;
181 
TokenList(String name)182         /*package*/ TokenList(String name) {
183             mLogTag = "TokenList-" + name;
184         }
185 
invalidateTokens(String id)186         /*package*/ int invalidateTokens(String id) {
187             if (Email.DEBUG && DEBUG_TOKENS) {
188                 Log.d(mLogTag, "============ Invalidate tokens for: " + id);
189             }
190             ArrayList<CacheToken> removeList = new ArrayList<CacheToken>();
191             int count = 0;
192             for (CacheToken token: this) {
193                 if (token.getId().equals(id)) {
194                     token.invalidate();
195                     removeList.add(token);
196                     count++;
197                 }
198             }
199             for (CacheToken token: removeList) {
200                 remove(token);
201             }
202             return count;
203         }
204 
invalidate()205         /*package*/ void invalidate() {
206             if (Email.DEBUG && DEBUG_TOKENS) {
207                 Log.d(mLogTag, "============ List invalidated");
208             }
209             for (CacheToken token: this) {
210                 token.invalidate();
211             }
212             clear();
213         }
214 
remove(CacheToken token)215         /*package*/ boolean remove(CacheToken token) {
216             boolean result = super.remove(token);
217             if (Email.DEBUG && DEBUG_TOKENS) {
218                 if (result) {
219                     Log.d(mLogTag, "============ Removing token for: " + token.mId);
220                 } else {
221                     Log.d(mLogTag, "============ No token found for: " + token.mId);
222                 }
223             }
224             return result;
225         }
226 
add(String id)227         public CacheToken add(String id) {
228             CacheToken token = new CacheToken(id);
229             super.add(token);
230             if (Email.DEBUG && DEBUG_TOKENS) {
231                 Log.d(mLogTag, "============ Taking token for: " + token.mId);
232             }
233             return token;
234         }
235     }
236 
237     /**
238      * A CacheToken is an opaque object that must be passed into putCursor in order to attempt to
239      * write into the cache.  The token becomes invalidated by any intervening write to the cached
240      * record.
241      */
242     public static final class CacheToken {
243         private final String mId;
244         private boolean mIsValid = READ_CACHE_ENABLED;
245 
CacheToken(String id)246         /*package*/ CacheToken(String id) {
247             mId = id;
248         }
249 
getId()250         /*package*/ String getId() {
251             return mId;
252         }
253 
isValid()254         /*package*/ boolean isValid() {
255             return mIsValid;
256         }
257 
invalidate()258         /*package*/ void invalidate() {
259             mIsValid = false;
260         }
261 
262         @Override
equals(Object token)263         public boolean equals(Object token) {
264             return ((token instanceof CacheToken) && ((CacheToken)token).mId.equals(mId));
265         }
266 
267         @Override
hashCode()268         public int hashCode() {
269             return mId.hashCode();
270         }
271     }
272 
273     /**
274      * The cached cursor is simply a CursorWrapper whose underlying cursor contains zero or one
275      * rows.  We handle simple movement (moveToFirst(), moveToNext(), etc.), and override close()
276      * to keep the underlying cursor alive (unless it's no longer cached due to an invalidation).
277      * Multiple CachedCursor's can use the same underlying cursor, so we override the various
278      * moveX methods such that each CachedCursor can have its own position information
279      */
280     public static final class CachedCursor extends CursorWrapper implements CrossProcessCursor {
281         // The cursor we're wrapping
282         private final Cursor mCursor;
283         // The cache which generated this cursor
284         private final ContentCache mCache;
285         private final String mId;
286         // The current position of the cursor (can only be 0 or 1)
287         private int mPosition = -1;
288         // The number of rows in this cursor (-1 = not determined)
289         private int mCount = -1;
290         private boolean isClosed = false;
291 
CachedCursor(Cursor cursor, ContentCache cache, String id)292         public CachedCursor(Cursor cursor, ContentCache cache, String id) {
293             super(cursor);
294             mCursor = cursor;
295             mCache = cache;
296             mId = id;
297             // Add this to our set of active cursors
298             sActiveCursors.add(cursor);
299         }
300 
301         /**
302          * Close this cursor; if the cursor's cache no longer contains the underlying cursor, and
303          * there are no other users of that cursor, we'll close it here. In any event,
304          * we'll remove the cursor from our set of active cursors.
305          */
306         @Override
close()307         public void close() {
308             synchronized(mCache) {
309                 int count = sActiveCursors.subtract(mCursor);
310                 if ((count == 0) && mCache.mLruCache.get(mId) != (mCursor)) {
311                     super.close();
312                 }
313             }
314             isClosed = true;
315         }
316 
317         @Override
isClosed()318         public boolean isClosed() {
319             return isClosed;
320         }
321 
322         @Override
getCount()323         public int getCount() {
324             if (mCount < 0) {
325                 mCount = super.getCount();
326             }
327             return mCount;
328         }
329 
330         /**
331          * We'll be happy to move to position 0 or -1
332          */
333         @Override
moveToPosition(int pos)334         public boolean moveToPosition(int pos) {
335             if (pos >= getCount() || pos < -1) {
336                 return false;
337             }
338             mPosition = pos;
339             return true;
340         }
341 
342         @Override
moveToFirst()343         public boolean moveToFirst() {
344             return moveToPosition(0);
345         }
346 
347         @Override
moveToNext()348         public boolean moveToNext() {
349             return moveToPosition(mPosition + 1);
350         }
351 
352         @Override
moveToPrevious()353         public boolean moveToPrevious() {
354             return moveToPosition(mPosition - 1);
355         }
356 
357         @Override
getPosition()358         public int getPosition() {
359             return mPosition;
360         }
361 
362         @Override
move(int offset)363         public final boolean move(int offset) {
364             return moveToPosition(mPosition + offset);
365         }
366 
367         @Override
moveToLast()368         public final boolean moveToLast() {
369             return moveToPosition(getCount() - 1);
370         }
371 
372         @Override
isLast()373         public final boolean isLast() {
374             return mPosition == (getCount() - 1);
375         }
376 
377         @Override
isBeforeFirst()378         public final boolean isBeforeFirst() {
379             return mPosition == -1;
380         }
381 
382         @Override
isAfterLast()383         public final boolean isAfterLast() {
384             return mPosition == 1;
385         }
386 
387         @Override
getWindow()388         public CursorWindow getWindow() {
389            return ((CrossProcessCursor)mCursor).getWindow();
390         }
391 
392         @Override
fillWindow(int pos, CursorWindow window)393         public void fillWindow(int pos, CursorWindow window) {
394             ((CrossProcessCursor)mCursor).fillWindow(pos, window);
395         }
396 
397         @Override
onMove(int oldPosition, int newPosition)398         public boolean onMove(int oldPosition, int newPosition) {
399             return true;
400         }
401     }
402 
403     /**
404      * Public constructor
405      * @param name the name of the cache (used for logging)
406      * @param baseProjection the projection used for cached cursors; queries whose columns are not
407      *  included in baseProjection will always generate a cache miss
408      * @param maxSize the maximum number of content cursors to cache
409      */
ContentCache(String name, String[] baseProjection, int maxSize)410     public ContentCache(String name, String[] baseProjection, int maxSize) {
411         mName = name;
412         mLruCache = new LruCache<String, Cursor>(maxSize) {
413             @Override
414             protected void entryRemoved(
415                     boolean evicted, String key, Cursor oldValue, Cursor newValue) {
416                 // Close this cursor if it's no longer being used
417                 if (evicted && !sActiveCursors.contains(oldValue)) {
418                     oldValue.close();
419                 }
420             }
421         };
422         mBaseProjection = baseProjection;
423         mLogTag = "ContentCache-" + name;
424         sContentCaches.add(this);
425         mTokenList = new TokenList(mName);
426         mStats = new Statistics(this);
427     }
428 
429     /**
430      * Return the base projection for cached rows
431      * Get the projection used for cached rows (typically, the largest possible projection)
432      * @return
433      */
getProjection()434     public String[] getProjection() {
435         return mBaseProjection;
436     }
437 
438 
439     /**
440      * Get a CacheToken for a row as specified by its id (_id column)
441      * @param id the id of the record
442      * @return a CacheToken needed in order to write data for the record back to the cache
443      */
getCacheToken(String id)444     public synchronized CacheToken getCacheToken(String id) {
445         // If another thread is already writing the data, return an invalid token
446         CacheToken token = mTokenList.add(id);
447         if (mLockMap.contains(id)) {
448             token.invalidate();
449         }
450         return token;
451     }
452 
size()453     public int size() {
454         return mLruCache.size();
455     }
456 
457     @VisibleForTesting
get(String id)458     Cursor get(String id) {
459         return mLruCache.get(id);
460     }
461 
getSnapshot()462     protected Map<String, Cursor> getSnapshot() {
463         return mLruCache.snapshot();
464     }
465     /**
466      * Try to cache a cursor for the given id and projection; returns a valid cursor, either a
467      * cached cursor (if caching was successful) or the original cursor
468      *
469      * @param c the cursor to be cached
470      * @param id the record id (_id) of the content
471      * @param projection the projection represented by the cursor
472      * @return whether or not the cursor was cached
473      */
putCursor(Cursor c, String id, String[] projection, CacheToken token)474     public Cursor putCursor(Cursor c, String id, String[] projection, CacheToken token) {
475         // Make sure the underlying cursor is at the first row, and do this without synchronizing,
476         // to prevent deadlock with a writing thread (which might, for example, be calling into
477         // CachedCursor.invalidate)
478         c.moveToPosition(0);
479         return putCursorImpl(c, id, projection, token);
480     }
putCursorImpl(Cursor c, String id, String[] projection, CacheToken token)481     public synchronized Cursor putCursorImpl(Cursor c, String id, String[] projection,
482             CacheToken token) {
483         try {
484             if (!token.isValid()) {
485                 if (Email.DEBUG && DEBUG_CACHE) {
486                     Log.d(mLogTag, "============ Stale token for " + id);
487                 }
488                 mStats.mStaleCount++;
489                 return c;
490             }
491             if (c != null && Arrays.equals(projection, mBaseProjection) && !sLockCache) {
492                 if (Email.DEBUG && DEBUG_CACHE) {
493                     Log.d(mLogTag, "============ Caching cursor for: " + id);
494                 }
495                 // If we've already cached this cursor, invalidate the older one
496                 Cursor existingCursor = get(id);
497                 if (existingCursor != null) {
498                    unlockImpl(id, null, false);
499                 }
500                 mLruCache.put(id, c);
501                 return new CachedCursor(c, this, id);
502             }
503             return c;
504         } finally {
505             mTokenList.remove(token);
506         }
507     }
508 
509     /**
510      * Find and, if found, return a cursor, based on cached values, for the supplied id
511      * @param id the _id column of the desired row
512      * @param projection the requested projection for a query
513      * @return a cursor based on cached values, or null if the row is not cached
514      */
getCachedCursor(String id, String[] projection)515     public synchronized Cursor getCachedCursor(String id, String[] projection) {
516         if (Email.DEBUG && DEBUG_STATISTICS) {
517             // Every 200 calls to getCursor, report cache statistics
518             dumpOnCount(200);
519         }
520         if (projection == mBaseProjection) {
521             return getCachedCursorImpl(id);
522         } else {
523             return getMatrixCursor(id, projection);
524         }
525     }
526 
getCachedCursorImpl(String id)527     private CachedCursor getCachedCursorImpl(String id) {
528         Cursor c = get(id);
529         if (c != null) {
530             mStats.mHitCount++;
531             return new CachedCursor(c, this, id);
532         }
533         mStats.mMissCount++;
534         return null;
535     }
536 
getMatrixCursor(String id, String[] projection)537     private MatrixCursor getMatrixCursor(String id, String[] projection) {
538         return getMatrixCursor(id, projection, null);
539     }
540 
getMatrixCursor(String id, String[] projection, ContentValues values)541     private MatrixCursor getMatrixCursor(String id, String[] projection,
542             ContentValues values) {
543         Cursor c = get(id);
544         if (c != null) {
545             // Make a new MatrixCursor with the requested columns
546             MatrixCursor mc = new MatrixCursor(projection, 1);
547             if (c.getCount() == 0) {
548                 return mc;
549             }
550             Object[] row = new Object[projection.length];
551             if (values != null) {
552                 // Make a copy; we don't want to change the original
553                 values = new ContentValues(values);
554             }
555             int i = 0;
556             for (String column: projection) {
557                 int columnIndex = c.getColumnIndex(column);
558                 if (columnIndex < 0) {
559                     mStats.mProjectionMissCount++;
560                     return null;
561                 } else {
562                     String value;
563                     if (values != null && values.containsKey(column)) {
564                         Object val = values.get(column);
565                         if (val instanceof Boolean) {
566                             value = (val == Boolean.TRUE) ? "1" : "0";
567                         } else {
568                             value = values.getAsString(column);
569                         }
570                         values.remove(column);
571                     } else {
572                         value = c.getString(columnIndex);
573                     }
574                     row[i++] = value;
575                 }
576             }
577             if (values != null && values.size() != 0) {
578                 return null;
579             }
580             mc.addRow(row);
581             mStats.mHitCount++;
582             return mc;
583         }
584         mStats.mMissCount++;
585         return null;
586     }
587 
588     /**
589      * Lock a given row, such that no new valid CacheTokens can be created for the passed-in id.
590      * @param id the id of the row to lock
591      */
lock(String id)592     public synchronized void lock(String id) {
593         // Prevent new valid tokens from being created
594         mLockMap.add(id);
595         // Invalidate current tokens
596         int count = mTokenList.invalidateTokens(id);
597         if (Email.DEBUG && DEBUG_TOKENS) {
598             Log.d(mTokenList.mLogTag, "============ Lock invalidated " + count +
599                     " tokens for: " + id);
600         }
601     }
602 
603     /**
604      * Unlock a given row, allowing new valid CacheTokens to be created for the passed-in id.
605      * @param id the id of the item whose cursor is cached
606      */
unlock(String id)607     public synchronized void unlock(String id) {
608         unlockImpl(id, null, true);
609     }
610 
611     /**
612      * If the row with id is currently cached, replaces the cached values with the supplied
613      * ContentValues.  Then, unlock the row, so that new valid CacheTokens can be created.
614      *
615      * @param id the id of the item whose cursor is cached
616      * @param values updated values for this row
617      */
unlock(String id, ContentValues values)618     public synchronized void unlock(String id, ContentValues values) {
619         unlockImpl(id, values, true);
620     }
621 
622     /**
623      * If values are passed in, replaces any cached cursor with one containing new values, and
624      * then closes the previously cached one (if any, and if not in use)
625      * If values are not passed in, removes the row from cache
626      * If the row was locked, unlock it
627      * @param id the id of the row
628      * @param values new ContentValues for the row (or null if row should simply be removed)
629      * @param wasLocked whether or not the row was locked; if so, the lock will be removed
630      */
unlockImpl(String id, ContentValues values, boolean wasLocked)631     private void unlockImpl(String id, ContentValues values, boolean wasLocked) {
632         Cursor c = get(id);
633         if (c != null) {
634             if (Email.DEBUG && DEBUG_CACHE) {
635                 Log.d(mLogTag, "=========== Unlocking cache for: " + id);
636             }
637             if (values != null && !sLockCache) {
638                 MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values);
639                 if (cursor != null) {
640                     if (Email.DEBUG && DEBUG_CACHE) {
641                         Log.d(mLogTag, "=========== Recaching with new values: " + id);
642                     }
643                     cursor.moveToFirst();
644                     mLruCache.put(id, cursor);
645                 } else {
646                     mLruCache.remove(id);
647                 }
648             } else {
649                 mLruCache.remove(id);
650             }
651             // If there are no cursors using the old cached cursor, close it
652             if (!sActiveCursors.contains(c)) {
653                 c.close();
654             }
655         }
656         if (wasLocked) {
657             mLockMap.subtract(id);
658         }
659     }
660 
661     /**
662      * Invalidate the entire cache, without logging
663      */
invalidate()664     public synchronized void invalidate() {
665         invalidate(null, null, null);
666     }
667 
668     /**
669      * Invalidate the entire cache; the arguments are used for logging only, and indicate the
670      * write operation that caused the invalidation
671      *
672      * @param operation a string describing the operation causing the invalidate (or null)
673      * @param uri the uri causing the invalidate (or null)
674      * @param selection the selection used with the uri (or null)
675      */
invalidate(String operation, Uri uri, String selection)676     public synchronized void invalidate(String operation, Uri uri, String selection) {
677         if (DEBUG_CACHE && (operation != null)) {
678             Log.d(mLogTag, "============ INVALIDATED BY " + operation + ": " + uri +
679                     ", SELECTION: " + selection);
680         }
681         mStats.mInvalidateCount++;
682         // Close all cached cursors that are no longer in use
683         mLruCache.evictAll();
684         // Invalidate all current tokens
685         mTokenList.invalidate();
686     }
687 
688     // Debugging code below
689 
dumpOnCount(int num)690     private void dumpOnCount(int num) {
691         mStats.mOpCount++;
692         if ((mStats.mOpCount % num) == 0) {
693             dumpStats();
694         }
695     }
696 
recordQueryTime(Cursor c, long nanoTime)697     /*package*/ void recordQueryTime(Cursor c, long nanoTime) {
698         if (c instanceof CachedCursor) {
699             mStats.hitTimes += nanoTime;
700             mStats.hits++;
701         } else {
702             if (c.getCount() == 1) {
703                 mStats.missTimes += nanoTime;
704                 mStats.miss++;
705             }
706         }
707     }
708 
notCacheable(Uri uri, String selection)709     public static synchronized void notCacheable(Uri uri, String selection) {
710         if (DEBUG_NOT_CACHEABLE) {
711             sNotCacheable++;
712             String str = uri.toString() + "$" + selection;
713             sNotCacheableMap.add(str);
714         }
715     }
716 
717     private static class CacheCounter implements Comparable<CacheCounter> {
718         String uri;
719         Integer count;
720 
CacheCounter(String _uri, Integer _count)721         CacheCounter(String _uri, Integer _count) {
722             uri = _uri;
723             count = _count;
724         }
725 
726         @Override
compareTo(CacheCounter another)727         public int compareTo(CacheCounter another) {
728             return another.count > count ? 1 : another.count == count ? 0 : -1;
729         }
730     }
731 
dumpNotCacheableQueries()732     private static void dumpNotCacheableQueries() {
733         int size = sNotCacheableMap.size();
734         CacheCounter[] array = new CacheCounter[size];
735 
736         int i = 0;
737         for (Map.Entry<String, Integer> entry: sNotCacheableMap.entrySet()) {
738             array[i++] = new CacheCounter(entry.getKey(), entry.getValue());
739         }
740         Arrays.sort(array);
741         for (CacheCounter cc: array) {
742             Log.d("NotCacheable", cc.count + ": " + cc.uri);
743         }
744     }
745 
746     // For use with unit tests
invalidateAllCaches()747     public static void invalidateAllCaches() {
748         for (ContentCache cache: sContentCaches) {
749             cache.invalidate();
750         }
751     }
752 
753     /** Sets the cache lock. If the lock is {@code true}, also invalidates all cached items. */
setLockCacheForTest(boolean lock)754     public static void setLockCacheForTest(boolean lock) {
755         sLockCache = lock;
756         if (sLockCache) {
757             invalidateAllCaches();
758         }
759     }
760 
761     static class Statistics {
762         private final ContentCache mCache;
763         private final String mName;
764 
765         // Cache statistics
766         // The item is in the cache AND is used to create a cursor
767         private int mHitCount = 0;
768         // Basic cache miss (the item is not cached)
769         private int mMissCount = 0;
770         // Incremented when a cachePut is invalid due to an intervening write
771         private int mStaleCount = 0;
772         // A projection miss occurs when the item is cached, but not all requested columns are
773         // available in the base projection
774         private int mProjectionMissCount = 0;
775         // Incremented whenever the entire cache is invalidated
776         private int mInvalidateCount = 0;
777         // Count of operations put/get
778         private int mOpCount = 0;
779         // The following are for timing statistics
780         private long hits = 0;
781         private long hitTimes = 0;
782         private long miss = 0;
783         private long missTimes = 0;
784 
785         // Used in toString() and addCacheStatistics()
786         private int mCursorCount = 0;
787         private int mTokenCount = 0;
788 
Statistics(ContentCache cache)789         Statistics(ContentCache cache) {
790             mCache = cache;
791             mName = mCache.mName;
792         }
793 
Statistics(String name)794         Statistics(String name) {
795             mCache = null;
796             mName = name;
797         }
798 
addCacheStatistics(ContentCache cache)799         private void addCacheStatistics(ContentCache cache) {
800             if (cache != null) {
801                 mHitCount += cache.mStats.mHitCount;
802                 mMissCount += cache.mStats.mMissCount;
803                 mProjectionMissCount += cache.mStats.mProjectionMissCount;
804                 mStaleCount += cache.mStats.mStaleCount;
805                 hitTimes += cache.mStats.hitTimes;
806                 missTimes += cache.mStats.missTimes;
807                 hits += cache.mStats.hits;
808                 miss += cache.mStats.miss;
809                 mCursorCount += cache.size();
810                 mTokenCount += cache.mTokenList.size();
811             }
812         }
813 
append(StringBuilder sb, String name, Object value)814         private void append(StringBuilder sb, String name, Object value) {
815             sb.append(", ");
816             sb.append(name);
817             sb.append(": ");
818             sb.append(value);
819         }
820 
821         @Override
toString()822         public String toString() {
823             if (mHitCount + mMissCount == 0) return "No cache";
824             int totalTries = mMissCount + mProjectionMissCount + mHitCount;
825             StringBuilder sb = new StringBuilder();
826             sb.append("Cache " + mName);
827             append(sb, "Cursors", mCache == null ? mCursorCount : mCache.size());
828             append(sb, "Hits", mHitCount);
829             append(sb, "Misses", mMissCount + mProjectionMissCount);
830             append(sb, "Inval", mInvalidateCount);
831             append(sb, "Tokens", mCache == null ? mTokenCount : mCache.mTokenList.size());
832             append(sb, "Hit%", mHitCount * 100 / totalTries);
833             append(sb, "\nHit time", hitTimes / 1000000.0 / hits);
834             append(sb, "Miss time", missTimes / 1000000.0 / miss);
835             return sb.toString();
836         }
837     }
838 
dumpStats()839     public static void dumpStats() {
840         Statistics totals = new Statistics("Totals");
841 
842         for (ContentCache cache: sContentCaches) {
843             if (cache != null) {
844                 Log.d(cache.mName, cache.mStats.toString());
845                 totals.addCacheStatistics(cache);
846             }
847         }
848         Log.d(totals.mName, totals.toString());
849     }
850 }
851