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