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