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