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