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