PageRenderTime 60ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

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

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