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