ContentCache.java revision 03cd72805dab0379ed255d151f1c17cc60655fc3
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
17package com.android.email.provider;
18
19import android.content.ContentValues;
20import android.database.CrossProcessCursor;
21import android.database.Cursor;
22import android.database.CursorWindow;
23import android.database.CursorWrapper;
24import android.database.MatrixCursor;
25import android.net.Uri;
26import android.util.Log;
27import android.util.LruCache;
28
29import com.android.email.Email;
30import com.google.common.annotations.VisibleForTesting;
31
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.HashMap;
35import java.util.Map;
36import 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 */
76public 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
121        /*package*/ CounterMap(int maxSize) {
122            mMap = new HashMap<T, Integer>(maxSize);
123        }
124
125        /*package*/ CounterMap() {
126            mMap = new HashMap<T, Integer>();
127        }
128
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
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
154        /*package*/ synchronized boolean contains(T object) {
155            return mMap.containsKey(object);
156        }
157
158        /*package*/ synchronized int getCount(T object) {
159            Integer refCount = mMap.get(object);
160            return (refCount == null) ? 0 : refCount.intValue();
161        }
162
163        synchronized int size() {
164            return mMap.size();
165        }
166
167        /**
168         * For Debugging Only - not efficient
169         */
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
182        /*package*/ TokenList(String name) {
183            mLogTag = "TokenList-" + name;
184        }
185
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
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
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
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
246        /*package*/ CacheToken(String id) {
247            mId = id;
248        }
249
250        /*package*/ String getId() {
251            return mId;
252        }
253
254        /*package*/ boolean isValid() {
255            return mIsValid;
256        }
257
258        /*package*/ void invalidate() {
259            mIsValid = false;
260        }
261
262        @Override
263        public boolean equals(Object token) {
264            return ((token instanceof CacheToken) && ((CacheToken)token).mId.equals(mId));
265        }
266
267        @Override
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
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
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
318        public boolean isClosed() {
319            return isClosed;
320        }
321
322        @Override
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
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
343        public boolean moveToFirst() {
344            return moveToPosition(0);
345        }
346
347        @Override
348        public boolean moveToNext() {
349            return moveToPosition(mPosition + 1);
350        }
351
352        @Override
353        public boolean moveToPrevious() {
354            return moveToPosition(mPosition - 1);
355        }
356
357        @Override
358        public int getPosition() {
359            return mPosition;
360        }
361
362        @Override
363        public final boolean move(int offset) {
364            return moveToPosition(mPosition + offset);
365        }
366
367        @Override
368        public final boolean moveToLast() {
369            return moveToPosition(getCount() - 1);
370        }
371
372        @Override
373        public final boolean isLast() {
374            return mPosition == (getCount() - 1);
375        }
376
377        @Override
378        public final boolean isBeforeFirst() {
379            return mPosition == -1;
380        }
381
382        @Override
383        public final boolean isAfterLast() {
384            return mPosition == 1;
385        }
386
387        @Override
388        public CursorWindow getWindow() {
389           return ((CrossProcessCursor)mCursor).getWindow();
390        }
391
392        @Override
393        public void fillWindow(int pos, CursorWindow window) {
394            ((CrossProcessCursor)mCursor).fillWindow(pos, window);
395        }
396
397        @Override
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     */
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     */
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     */
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
453    public int size() {
454        return mLruCache.size();
455    }
456
457    @VisibleForTesting
458    Cursor get(String id) {
459        return mLruCache.get(id);
460    }
461
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     */
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    }
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     */
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
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
537    private MatrixCursor getMatrixCursor(String id, String[] projection) {
538        return getMatrixCursor(id, projection, null);
539    }
540
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     */
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     */
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     */
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     */
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     */
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     */
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
690    private void dumpOnCount(int num) {
691        mStats.mOpCount++;
692        if ((mStats.mOpCount % num) == 0) {
693            dumpStats();
694        }
695    }
696
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
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
721        CacheCounter(String _uri, Integer _count) {
722            uri = _uri;
723            count = _count;
724        }
725
726        @Override
727        public int compareTo(CacheCounter another) {
728            return another.count > count ? 1 : another.count == count ? 0 : -1;
729        }
730    }
731
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
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. */
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
789        Statistics(ContentCache cache) {
790            mCache = cache;
791            mName = mCache.mName;
792        }
793
794        Statistics(String name) {
795            mCache = null;
796            mName = name;
797        }
798
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
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
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
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