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