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