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