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