PageRenderTime 91ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/src/com/android/email/provider/ContentCache.java

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