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