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 17 package com.android.email.provider; 18 19 import android.content.ContentValues; 20 import android.database.CrossProcessCursor; 21 import android.database.Cursor; 22 import android.database.CursorWindow; 23 import android.database.CursorWrapper; 24 import android.database.MatrixCursor; 25 import android.net.Uri; 26 import android.util.LruCache; 27 28 import com.android.email.DebugUtils; 29 import com.android.mail.utils.LogUtils; 30 import com.android.mail.utils.MatrixCursorWithCachedColumns; 31 import com.google.common.annotations.VisibleForTesting; 32 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.HashMap; 36 import java.util.Map; 37 import 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 */ 77 public 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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 (DebugUtils.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 // For use with unit tests 719 public static void invalidateAllCaches() { 720 for (ContentCache cache: sContentCaches) { 721 cache.invalidate(); 722 } 723 } 724 725 /** Sets the cache lock. If the lock is {@code true}, also invalidates all cached items. */ 726 public static void setLockCacheForTest(boolean lock) { 727 sLockCache = lock; 728 if (sLockCache) { 729 invalidateAllCaches(); 730 } 731 } 732 733 static class Statistics { 734 private final ContentCache mCache; 735 private final String mName; 736 737 // Cache statistics 738 // The item is in the cache AND is used to create a cursor 739 private int mHitCount = 0; 740 // Basic cache miss (the item is not cached) 741 private int mMissCount = 0; 742 // Incremented when a cachePut is invalid due to an intervening write 743 private int mStaleCount = 0; 744 // A projection miss occurs when the item is cached, but not all requested columns are 745 // available in the base projection 746 private int mProjectionMissCount = 0; 747 // Incremented whenever the entire cache is invalidated 748 private int mInvalidateCount = 0; 749 // Count of operations put/get 750 private int mOpCount = 0; 751 // The following are for timing statistics 752 private long hits = 0; 753 private long hitTimes = 0; 754 private long miss = 0; 755 private long missTimes = 0; 756 757 // Used in toString() and addCacheStatistics() 758 private int mCursorCount = 0; 759 private int mTokenCount = 0; 760 761 Statistics(ContentCache cache) { 762 mCache = cache; 763 mName = mCache.mName; 764 } 765 766 Statistics(String name) { 767 mCache = null; 768 mName = name; 769 } 770 771 private void addCacheStatistics(ContentCache cache) { 772 if (cache != null) { 773 mHitCount += cache.mStats.mHitCount; 774 mMissCount += cache.mStats.mMissCount; 775 mProjectionMissCount += cache.mStats.mProjectionMissCount; 776 mStaleCount += cache.mStats.mStaleCount; 777 hitTimes += cache.mStats.hitTimes; 778 missTimes += cache.mStats.missTimes; 779 hits += cache.mStats.hits; 780 miss += cache.mStats.miss; 781 mCursorCount += cache.size(); 782 mTokenCount += cache.mTokenList.size(); 783 } 784 } 785 786 private static void append(StringBuilder sb, String name, Object value) { 787 sb.append(", "); 788 sb.append(name); 789 sb.append(": "); 790 sb.append(value); 791 } 792 793 @Override 794 public String toString() { 795 if (mHitCount + mMissCount == 0) return "No cache"; 796 int totalTries = mMissCount + mProjectionMissCount + mHitCount; 797 StringBuilder sb = new StringBuilder(); 798 sb.append("Cache " + mName); 799 append(sb, "Cursors", mCache == null ? mCursorCount : mCache.size()); 800 append(sb, "Hits", mHitCount); 801 append(sb, "Misses", mMissCount + mProjectionMissCount); 802 append(sb, "Inval", mInvalidateCount); 803 append(sb, "Tokens", mCache == null ? mTokenCount : mCache.mTokenList.size()); 804 append(sb, "Hit%", mHitCount * 100 / totalTries); 805 append(sb, "\nHit time", hitTimes / 1000000.0 / hits); 806 append(sb, "Miss time", missTimes / 1000000.0 / miss); 807 return sb.toString(); 808 } 809 } 810 811 public static void dumpStats() { 812 Statistics totals = new Statistics("Totals"); 813 814 for (ContentCache cache: sContentCaches) { 815 if (cache != null) { 816 LogUtils.d(cache.mName, cache.mStats.toString()); 817 totals.addCacheStatistics(cache); 818 } 819 } 820 LogUtils.d(totals.mName, totals.toString()); 821 } 822 } 823