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