2010-10-27 23:50:54 +00:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2010 The Android Open Source Project
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
package com.android.email.provider;
|
2010-11-17 06:49:04 +00:00
|
|
|
|
2010-10-27 23:50:54 +00:00
|
|
|
import android.content.ContentValues;
|
2011-06-22 17:59:06 +00:00
|
|
|
import android.database.CrossProcessCursor;
|
2010-10-27 23:50:54 +00:00
|
|
|
import android.database.Cursor;
|
2011-06-22 17:59:06 +00:00
|
|
|
import android.database.CursorWindow;
|
2010-10-27 23:50:54 +00:00
|
|
|
import android.database.CursorWrapper;
|
|
|
|
import android.database.MatrixCursor;
|
|
|
|
import android.net.Uri;
|
2011-02-11 21:34:23 +00:00
|
|
|
import android.util.LruCache;
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
|
2014-08-28 18:00:08 +00:00
|
|
|
import com.android.email.DebugUtils;
|
2013-05-26 04:32:32 +00:00
|
|
|
import com.android.mail.utils.LogUtils;
|
2013-02-06 23:21:15 +00:00
|
|
|
import com.android.mail.utils.MatrixCursorWithCachedColumns;
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
import com.google.common.annotations.VisibleForTesting;
|
|
|
|
|
2010-10-27 23:50:54 +00:00
|
|
|
import java.util.ArrayList;
|
|
|
|
import java.util.Arrays;
|
|
|
|
import java.util.HashMap;
|
|
|
|
import java.util.Map;
|
2010-11-17 06:49:04 +00:00
|
|
|
import java.util.Set;
|
2010-10-27 23:50:54 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* An LRU cache for EmailContent (Account, HostAuth, Mailbox, and Message, thus far). The intended
|
|
|
|
* user of this cache is EmailProvider itself; caching is entirely transparent to users of the
|
|
|
|
* provider.
|
|
|
|
*
|
|
|
|
* Usage examples; id is a String representation of a row id (_id), as it might be retrieved from
|
|
|
|
* a uri via getPathSegment
|
|
|
|
*
|
|
|
|
* To create a cache:
|
|
|
|
* ContentCache cache = new ContentCache(name, projection, max);
|
|
|
|
*
|
|
|
|
* To (try to) get a cursor from a cache:
|
|
|
|
* Cursor cursor = cache.getCursor(id, projection);
|
|
|
|
*
|
|
|
|
* To read from a table and cache the resulting cursor:
|
|
|
|
* 1. Get a CacheToken: CacheToken token = cache.getToken(id);
|
|
|
|
* 2. Get a cursor from the database: Cursor cursor = db.query(....);
|
|
|
|
* 3. Put the cursor in the cache: cache.putCursor(cursor, id, token);
|
|
|
|
* Only cursors with the projection given in the definition of the cache can be cached
|
|
|
|
*
|
|
|
|
* To delete one or more rows or update multiple rows from a table that uses cached data:
|
|
|
|
* 1. Lock the row in the cache: cache.lock(id);
|
|
|
|
* 2. Delete/update the row(s): db.delete(...);
|
|
|
|
* 3. Invalidate any other caches that might be affected by the delete/update:
|
|
|
|
* The entire cache: affectedCache.invalidate()*
|
|
|
|
* A specific row in a cache: affectedCache.invalidate(rowId)
|
|
|
|
* 4. Unlock the row in the cache: cache.unlock(id);
|
|
|
|
*
|
|
|
|
* To update a single row from a table that uses cached data:
|
|
|
|
* 1. Lock the row in the cache: cache.lock(id);
|
|
|
|
* 2. Update the row: db.update(...);
|
|
|
|
* 3. Unlock the row in the cache, passing in the new values: cache.unlock(id, values);
|
2010-12-16 18:39:08 +00:00
|
|
|
*
|
|
|
|
* Synchronization note: All of the public methods in ContentCache are synchronized (i.e. on the
|
|
|
|
* cache itself) except for methods that are solely used for debugging and do not modify the cache.
|
|
|
|
* All references to ContentCache that are external to the ContentCache class MUST synchronize on
|
|
|
|
* the ContentCache instance (e.g. CachedCursor.close())
|
2010-10-27 23:50:54 +00:00
|
|
|
*/
|
2011-01-26 01:13:22 +00:00
|
|
|
public final class ContentCache {
|
2010-12-26 18:43:02 +00:00
|
|
|
private static final boolean DEBUG_CACHE = false; // DO NOT CHECK IN TRUE
|
|
|
|
private static final boolean DEBUG_TOKENS = false; // DO NOT CHECK IN TRUE
|
2010-11-17 17:47:36 +00:00
|
|
|
private static final boolean DEBUG_NOT_CACHEABLE = false; // DO NOT CHECK IN TRUE
|
2011-01-28 22:54:52 +00:00
|
|
|
private static final boolean DEBUG_STATISTICS = false; // DO NOT CHECK THIS IN TRUE
|
2010-11-17 17:47:36 +00:00
|
|
|
|
|
|
|
// If false, reads will not use the cache; this is intended for debugging only
|
|
|
|
private static final boolean READ_CACHE_ENABLED = true; // DO NOT CHECK IN FALSE
|
2010-10-27 23:50:54 +00:00
|
|
|
|
|
|
|
// Count of non-cacheable queries (debug only)
|
|
|
|
private static int sNotCacheable = 0;
|
|
|
|
// A map of queries that aren't cacheable (debug only)
|
|
|
|
private static final CounterMap<String> sNotCacheableMap = new CounterMap<String>();
|
|
|
|
|
2011-02-11 21:34:23 +00:00
|
|
|
private final LruCache<String, Cursor> mLruCache;
|
2011-01-26 01:13:22 +00:00
|
|
|
|
2010-10-27 23:50:54 +00:00
|
|
|
// All defined caches
|
|
|
|
private static final ArrayList<ContentCache> sContentCaches = new ArrayList<ContentCache>();
|
|
|
|
// A set of all unclosed, cached cursors; this will typically be a very small set, as cursors
|
|
|
|
// tend to be closed quickly after use. The value, for each cursor, is its reference count
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
/*package*/ static final CounterMap<Cursor> sActiveCursors = new CounterMap<Cursor>(24);
|
2010-10-27 23:50:54 +00:00
|
|
|
|
|
|
|
// A set of locked content id's
|
|
|
|
private final CounterMap<String> mLockMap = new CounterMap<String>(4);
|
|
|
|
// A set of active tokens
|
|
|
|
/*package*/ TokenList mTokenList;
|
|
|
|
|
|
|
|
// The name of the cache (used for logging)
|
|
|
|
private final String mName;
|
|
|
|
// The base projection (only queries in which all columns exist in this projection will be
|
|
|
|
// able to avoid a cache miss)
|
|
|
|
private final String[] mBaseProjection;
|
|
|
|
// The tag used for logging
|
|
|
|
private final String mLogTag;
|
|
|
|
// Cache statistics
|
|
|
|
private final Statistics mStats;
|
2011-06-08 18:51:55 +00:00
|
|
|
/** If {@code true}, lock the cache for all writes */
|
|
|
|
private static boolean sLockCache;
|
2010-10-27 23:50:54 +00:00
|
|
|
|
|
|
|
/**
|
2010-11-17 06:49:04 +00:00
|
|
|
* A synchronized reference counter for arbitrary objects
|
2010-10-27 23:50:54 +00:00
|
|
|
*/
|
2010-11-17 06:49:04 +00:00
|
|
|
/*package*/ static class CounterMap<T> {
|
|
|
|
private HashMap<T, Integer> mMap;
|
2010-10-27 23:50:54 +00:00
|
|
|
|
|
|
|
/*package*/ CounterMap(int maxSize) {
|
2010-11-17 06:49:04 +00:00
|
|
|
mMap = new HashMap<T, Integer>(maxSize);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ CounterMap() {
|
2010-11-17 06:49:04 +00:00
|
|
|
mMap = new HashMap<T, Integer>();
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
2010-12-03 17:06:26 +00:00
|
|
|
/*package*/ synchronized int subtract(T object) {
|
2010-11-17 06:49:04 +00:00
|
|
|
Integer refCount = mMap.get(object);
|
2010-12-03 17:06:26 +00:00
|
|
|
int newCount;
|
2010-11-17 06:49:04 +00:00
|
|
|
if (refCount == null || refCount.intValue() == 0) {
|
|
|
|
throw new IllegalStateException();
|
|
|
|
}
|
|
|
|
if (refCount > 1) {
|
2010-12-03 17:06:26 +00:00
|
|
|
newCount = refCount - 1;
|
|
|
|
mMap.put(object, newCount);
|
2010-11-17 06:49:04 +00:00
|
|
|
} else {
|
2010-12-03 17:06:26 +00:00
|
|
|
newCount = 0;
|
2010-11-17 06:49:04 +00:00
|
|
|
mMap.remove(object);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
2010-12-03 17:06:26 +00:00
|
|
|
return newCount;
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
2010-11-17 06:49:04 +00:00
|
|
|
/*package*/ synchronized void add(T object) {
|
|
|
|
Integer refCount = mMap.get(object);
|
|
|
|
if (refCount == null) {
|
|
|
|
mMap.put(object, 1);
|
|
|
|
} else {
|
|
|
|
mMap.put(object, refCount + 1);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-17 06:49:04 +00:00
|
|
|
/*package*/ synchronized boolean contains(T object) {
|
|
|
|
return mMap.containsKey(object);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
2010-11-17 06:49:04 +00:00
|
|
|
/*package*/ synchronized int getCount(T object) {
|
|
|
|
Integer refCount = mMap.get(object);
|
|
|
|
return (refCount == null) ? 0 : refCount.intValue();
|
|
|
|
}
|
|
|
|
|
|
|
|
synchronized int size() {
|
|
|
|
return mMap.size();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For Debugging Only - not efficient
|
|
|
|
*/
|
2012-06-28 17:40:46 +00:00
|
|
|
synchronized Set<Map.Entry<T, Integer>> entrySet() {
|
2010-11-17 06:49:04 +00:00
|
|
|
return mMap.entrySet();
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A list of tokens that are in use at any moment; there can be more than one token for an id
|
|
|
|
*/
|
|
|
|
/*package*/ static class TokenList extends ArrayList<CacheToken> {
|
|
|
|
private static final long serialVersionUID = 1L;
|
|
|
|
private final String mLogTag;
|
|
|
|
|
|
|
|
/*package*/ TokenList(String name) {
|
|
|
|
mLogTag = "TokenList-" + name;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ int invalidateTokens(String id) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_TOKENS) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ Invalidate tokens for: " + id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
ArrayList<CacheToken> removeList = new ArrayList<CacheToken>();
|
|
|
|
int count = 0;
|
|
|
|
for (CacheToken token: this) {
|
|
|
|
if (token.getId().equals(id)) {
|
|
|
|
token.invalidate();
|
|
|
|
removeList.add(token);
|
|
|
|
count++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (CacheToken token: removeList) {
|
|
|
|
remove(token);
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ void invalidate() {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_TOKENS) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ List invalidated");
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
for (CacheToken token: this) {
|
|
|
|
token.invalidate();
|
|
|
|
}
|
|
|
|
clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ boolean remove(CacheToken token) {
|
|
|
|
boolean result = super.remove(token);
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_TOKENS) {
|
2010-10-27 23:50:54 +00:00
|
|
|
if (result) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ Removing token for: " + token.mId);
|
2010-10-27 23:50:54 +00:00
|
|
|
} else {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ No token found for: " + token.mId);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
public CacheToken add(String id) {
|
|
|
|
CacheToken token = new CacheToken(id);
|
|
|
|
super.add(token);
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_TOKENS) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ Taking token for: " + token.mId);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
return token;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A CacheToken is an opaque object that must be passed into putCursor in order to attempt to
|
|
|
|
* write into the cache. The token becomes invalidated by any intervening write to the cached
|
|
|
|
* record.
|
|
|
|
*/
|
|
|
|
public static final class CacheToken {
|
|
|
|
private final String mId;
|
2010-11-17 17:47:36 +00:00
|
|
|
private boolean mIsValid = READ_CACHE_ENABLED;
|
2010-10-27 23:50:54 +00:00
|
|
|
|
|
|
|
/*package*/ CacheToken(String id) {
|
|
|
|
mId = id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ String getId() {
|
|
|
|
return mId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ boolean isValid() {
|
|
|
|
return mIsValid;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ void invalidate() {
|
|
|
|
mIsValid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean equals(Object token) {
|
|
|
|
return ((token instanceof CacheToken) && ((CacheToken)token).mId.equals(mId));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int hashCode() {
|
|
|
|
return mId.hashCode();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The cached cursor is simply a CursorWrapper whose underlying cursor contains zero or one
|
|
|
|
* rows. We handle simple movement (moveToFirst(), moveToNext(), etc.), and override close()
|
2010-11-30 18:38:34 +00:00
|
|
|
* to keep the underlying cursor alive (unless it's no longer cached due to an invalidation).
|
|
|
|
* Multiple CachedCursor's can use the same underlying cursor, so we override the various
|
|
|
|
* moveX methods such that each CachedCursor can have its own position information
|
2010-10-27 23:50:54 +00:00
|
|
|
*/
|
2011-06-22 17:59:06 +00:00
|
|
|
public static final class CachedCursor extends CursorWrapper implements CrossProcessCursor {
|
2010-10-27 23:50:54 +00:00
|
|
|
// The cursor we're wrapping
|
|
|
|
private final Cursor mCursor;
|
|
|
|
// The cache which generated this cursor
|
|
|
|
private final ContentCache mCache;
|
2011-02-11 21:34:23 +00:00
|
|
|
private final String mId;
|
2010-10-27 23:50:54 +00:00
|
|
|
// The current position of the cursor (can only be 0 or 1)
|
|
|
|
private int mPosition = -1;
|
|
|
|
// The number of rows in this cursor (-1 = not determined)
|
|
|
|
private int mCount = -1;
|
|
|
|
private boolean isClosed = false;
|
|
|
|
|
2011-02-11 21:34:23 +00:00
|
|
|
public CachedCursor(Cursor cursor, ContentCache cache, String id) {
|
2010-10-27 23:50:54 +00:00
|
|
|
super(cursor);
|
|
|
|
mCursor = cursor;
|
|
|
|
mCache = cache;
|
2011-02-11 21:34:23 +00:00
|
|
|
mId = id;
|
2010-10-27 23:50:54 +00:00
|
|
|
// Add this to our set of active cursors
|
|
|
|
sActiveCursors.add(cursor);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2010-12-03 17:06:26 +00:00
|
|
|
* Close this cursor; if the cursor's cache no longer contains the underlying cursor, and
|
|
|
|
* there are no other users of that cursor, we'll close it here. In any event,
|
|
|
|
* we'll remove the cursor from our set of active cursors.
|
2010-10-27 23:50:54 +00:00
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
public void close() {
|
2010-12-16 18:39:08 +00:00
|
|
|
synchronized(mCache) {
|
2010-12-26 18:43:02 +00:00
|
|
|
int count = sActiveCursors.subtract(mCursor);
|
2011-02-11 21:34:23 +00:00
|
|
|
if ((count == 0) && mCache.mLruCache.get(mId) != (mCursor)) {
|
2010-12-16 18:39:08 +00:00
|
|
|
super.close();
|
|
|
|
}
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
isClosed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isClosed() {
|
|
|
|
return isClosed;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getCount() {
|
|
|
|
if (mCount < 0) {
|
|
|
|
mCount = super.getCount();
|
|
|
|
}
|
|
|
|
return mCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2010-11-30 18:38:34 +00:00
|
|
|
* We'll be happy to move to position 0 or -1
|
2010-10-27 23:50:54 +00:00
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
public boolean moveToPosition(int pos) {
|
2010-11-30 18:38:34 +00:00
|
|
|
if (pos >= getCount() || pos < -1) {
|
2010-10-27 23:50:54 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
mPosition = pos;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean moveToFirst() {
|
|
|
|
return moveToPosition(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean moveToNext() {
|
|
|
|
return moveToPosition(mPosition + 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean moveToPrevious() {
|
2010-11-30 18:38:34 +00:00
|
|
|
return moveToPosition(mPosition - 1);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getPosition() {
|
|
|
|
return mPosition;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public final boolean move(int offset) {
|
|
|
|
return moveToPosition(mPosition + offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public final boolean moveToLast() {
|
|
|
|
return moveToPosition(getCount() - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public final boolean isLast() {
|
|
|
|
return mPosition == (getCount() - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public final boolean isBeforeFirst() {
|
|
|
|
return mPosition == -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public final boolean isAfterLast() {
|
|
|
|
return mPosition == 1;
|
|
|
|
}
|
2011-06-22 17:59:06 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public CursorWindow getWindow() {
|
|
|
|
return ((CrossProcessCursor)mCursor).getWindow();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void fillWindow(int pos, CursorWindow window) {
|
|
|
|
((CrossProcessCursor)mCursor).fillWindow(pos, window);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean onMove(int oldPosition, int newPosition) {
|
2011-10-19 19:33:52 +00:00
|
|
|
return true;
|
2011-06-22 17:59:06 +00:00
|
|
|
}
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Public constructor
|
|
|
|
* @param name the name of the cache (used for logging)
|
|
|
|
* @param baseProjection the projection used for cached cursors; queries whose columns are not
|
|
|
|
* included in baseProjection will always generate a cache miss
|
|
|
|
* @param maxSize the maximum number of content cursors to cache
|
|
|
|
*/
|
|
|
|
public ContentCache(String name, String[] baseProjection, int maxSize) {
|
|
|
|
mName = name;
|
2011-02-11 21:34:23 +00:00
|
|
|
mLruCache = new LruCache<String, Cursor>(maxSize) {
|
|
|
|
@Override
|
2011-02-26 01:25:28 +00:00
|
|
|
protected void entryRemoved(
|
|
|
|
boolean evicted, String key, Cursor oldValue, Cursor newValue) {
|
2011-02-11 21:34:23 +00:00
|
|
|
// Close this cursor if it's no longer being used
|
2011-02-26 01:25:28 +00:00
|
|
|
if (evicted && !sActiveCursors.contains(oldValue)) {
|
|
|
|
oldValue.close();
|
2011-02-11 21:34:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2010-10-27 23:50:54 +00:00
|
|
|
mBaseProjection = baseProjection;
|
|
|
|
mLogTag = "ContentCache-" + name;
|
|
|
|
sContentCaches.add(this);
|
|
|
|
mTokenList = new TokenList(mName);
|
|
|
|
mStats = new Statistics(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the base projection for cached rows
|
|
|
|
* Get the projection used for cached rows (typically, the largest possible projection)
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
public String[] getProjection() {
|
|
|
|
return mBaseProjection;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a CacheToken for a row as specified by its id (_id column)
|
|
|
|
* @param id the id of the record
|
|
|
|
* @return a CacheToken needed in order to write data for the record back to the cache
|
|
|
|
*/
|
|
|
|
public synchronized CacheToken getCacheToken(String id) {
|
|
|
|
// If another thread is already writing the data, return an invalid token
|
|
|
|
CacheToken token = mTokenList.add(id);
|
|
|
|
if (mLockMap.contains(id)) {
|
|
|
|
token.invalidate();
|
|
|
|
}
|
|
|
|
return token;
|
|
|
|
}
|
|
|
|
|
2011-01-26 01:13:22 +00:00
|
|
|
public int size() {
|
2011-02-11 21:34:23 +00:00
|
|
|
return mLruCache.size();
|
2011-01-26 01:13:22 +00:00
|
|
|
}
|
|
|
|
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
@VisibleForTesting
|
|
|
|
Cursor get(String id) {
|
2011-02-11 21:34:23 +00:00
|
|
|
return mLruCache.get(id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
protected Map<String, Cursor> getSnapshot() {
|
|
|
|
return mLruCache.snapshot();
|
|
|
|
}
|
2010-10-27 23:50:54 +00:00
|
|
|
/**
|
|
|
|
* Try to cache a cursor for the given id and projection; returns a valid cursor, either a
|
|
|
|
* cached cursor (if caching was successful) or the original cursor
|
|
|
|
*
|
|
|
|
* @param c the cursor to be cached
|
|
|
|
* @param id the record id (_id) of the content
|
|
|
|
* @param projection the projection represented by the cursor
|
|
|
|
* @return whether or not the cursor was cached
|
|
|
|
*/
|
2010-11-23 19:19:26 +00:00
|
|
|
public Cursor putCursor(Cursor c, String id, String[] projection, CacheToken token) {
|
|
|
|
// Make sure the underlying cursor is at the first row, and do this without synchronizing,
|
|
|
|
// to prevent deadlock with a writing thread (which might, for example, be calling into
|
|
|
|
// CachedCursor.invalidate)
|
|
|
|
c.moveToPosition(0);
|
|
|
|
return putCursorImpl(c, id, projection, token);
|
|
|
|
}
|
|
|
|
public synchronized Cursor putCursorImpl(Cursor c, String id, String[] projection,
|
2010-10-27 23:50:54 +00:00
|
|
|
CacheToken token) {
|
|
|
|
try {
|
|
|
|
if (!token.isValid()) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_CACHE) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ Stale token for " + id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
mStats.mStaleCount++;
|
|
|
|
return c;
|
|
|
|
}
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
if (c != null && Arrays.equals(projection, mBaseProjection) && !sLockCache) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_CACHE) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ Caching cursor for: " + id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
// If we've already cached this cursor, invalidate the older one
|
|
|
|
Cursor existingCursor = get(id);
|
|
|
|
if (existingCursor != null) {
|
|
|
|
unlockImpl(id, null, false);
|
|
|
|
}
|
2011-02-11 21:34:23 +00:00
|
|
|
mLruCache.put(id, c);
|
2010-10-27 23:50:54 +00:00
|
|
|
return new CachedCursor(c, this, id);
|
|
|
|
}
|
|
|
|
return c;
|
|
|
|
} finally {
|
|
|
|
mTokenList.remove(token);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find and, if found, return a cursor, based on cached values, for the supplied id
|
|
|
|
* @param id the _id column of the desired row
|
|
|
|
* @param projection the requested projection for a query
|
|
|
|
* @return a cursor based on cached values, or null if the row is not cached
|
|
|
|
*/
|
|
|
|
public synchronized Cursor getCachedCursor(String id, String[] projection) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_STATISTICS) {
|
2010-10-27 23:50:54 +00:00
|
|
|
// Every 200 calls to getCursor, report cache statistics
|
|
|
|
dumpOnCount(200);
|
|
|
|
}
|
|
|
|
if (projection == mBaseProjection) {
|
|
|
|
return getCachedCursorImpl(id);
|
|
|
|
} else {
|
|
|
|
return getMatrixCursor(id, projection);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private CachedCursor getCachedCursorImpl(String id) {
|
|
|
|
Cursor c = get(id);
|
|
|
|
if (c != null) {
|
|
|
|
mStats.mHitCount++;
|
|
|
|
return new CachedCursor(c, this, id);
|
|
|
|
}
|
|
|
|
mStats.mMissCount++;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
private MatrixCursor getMatrixCursor(String id, String[] projection) {
|
|
|
|
return getMatrixCursor(id, projection, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
private MatrixCursor getMatrixCursor(String id, String[] projection,
|
|
|
|
ContentValues values) {
|
|
|
|
Cursor c = get(id);
|
|
|
|
if (c != null) {
|
|
|
|
// Make a new MatrixCursor with the requested columns
|
2013-02-06 23:21:15 +00:00
|
|
|
MatrixCursor mc = new MatrixCursorWithCachedColumns(projection, 1);
|
2010-12-20 21:58:11 +00:00
|
|
|
if (c.getCount() == 0) {
|
|
|
|
return mc;
|
|
|
|
}
|
2010-10-27 23:50:54 +00:00
|
|
|
Object[] row = new Object[projection.length];
|
|
|
|
if (values != null) {
|
|
|
|
// Make a copy; we don't want to change the original
|
|
|
|
values = new ContentValues(values);
|
|
|
|
}
|
|
|
|
int i = 0;
|
|
|
|
for (String column: projection) {
|
|
|
|
int columnIndex = c.getColumnIndex(column);
|
|
|
|
if (columnIndex < 0) {
|
|
|
|
mStats.mProjectionMissCount++;
|
|
|
|
return null;
|
|
|
|
} else {
|
|
|
|
String value;
|
|
|
|
if (values != null && values.containsKey(column)) {
|
|
|
|
Object val = values.get(column);
|
|
|
|
if (val instanceof Boolean) {
|
|
|
|
value = (val == Boolean.TRUE) ? "1" : "0";
|
|
|
|
} else {
|
|
|
|
value = values.getAsString(column);
|
|
|
|
}
|
|
|
|
values.remove(column);
|
|
|
|
} else {
|
|
|
|
value = c.getString(columnIndex);
|
|
|
|
}
|
|
|
|
row[i++] = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (values != null && values.size() != 0) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
mc.addRow(row);
|
|
|
|
mStats.mHitCount++;
|
|
|
|
return mc;
|
|
|
|
}
|
|
|
|
mStats.mMissCount++;
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lock a given row, such that no new valid CacheTokens can be created for the passed-in id.
|
|
|
|
* @param id the id of the row to lock
|
|
|
|
*/
|
|
|
|
public synchronized void lock(String id) {
|
|
|
|
// Prevent new valid tokens from being created
|
|
|
|
mLockMap.add(id);
|
|
|
|
// Invalidate current tokens
|
|
|
|
int count = mTokenList.invalidateTokens(id);
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_TOKENS) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mTokenList.mLogTag, "============ Lock invalidated " + count +
|
2010-10-27 23:50:54 +00:00
|
|
|
" tokens for: " + id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unlock a given row, allowing new valid CacheTokens to be created for the passed-in id.
|
|
|
|
* @param id the id of the item whose cursor is cached
|
|
|
|
*/
|
|
|
|
public synchronized void unlock(String id) {
|
|
|
|
unlockImpl(id, null, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If the row with id is currently cached, replaces the cached values with the supplied
|
|
|
|
* ContentValues. Then, unlock the row, so that new valid CacheTokens can be created.
|
|
|
|
*
|
|
|
|
* @param id the id of the item whose cursor is cached
|
|
|
|
* @param values updated values for this row
|
|
|
|
*/
|
|
|
|
public synchronized void unlock(String id, ContentValues values) {
|
|
|
|
unlockImpl(id, values, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If values are passed in, replaces any cached cursor with one containing new values, and
|
|
|
|
* then closes the previously cached one (if any, and if not in use)
|
|
|
|
* If values are not passed in, removes the row from cache
|
|
|
|
* If the row was locked, unlock it
|
|
|
|
* @param id the id of the row
|
|
|
|
* @param values new ContentValues for the row (or null if row should simply be removed)
|
|
|
|
* @param wasLocked whether or not the row was locked; if so, the lock will be removed
|
|
|
|
*/
|
2010-12-16 18:39:08 +00:00
|
|
|
private void unlockImpl(String id, ContentValues values, boolean wasLocked) {
|
2010-10-27 23:50:54 +00:00
|
|
|
Cursor c = get(id);
|
|
|
|
if (c != null) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_CACHE) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "=========== Unlocking cache for: " + id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
2011-06-08 18:51:55 +00:00
|
|
|
if (values != null && !sLockCache) {
|
2010-10-27 23:50:54 +00:00
|
|
|
MatrixCursor cursor = getMatrixCursor(id, mBaseProjection, values);
|
|
|
|
if (cursor != null) {
|
2014-08-28 18:00:08 +00:00
|
|
|
if (DebugUtils.DEBUG && DEBUG_CACHE) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "=========== Recaching with new values: " + id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
cursor.moveToFirst();
|
2011-02-11 21:34:23 +00:00
|
|
|
mLruCache.put(id, cursor);
|
2010-10-27 23:50:54 +00:00
|
|
|
} else {
|
2011-02-11 21:34:23 +00:00
|
|
|
mLruCache.remove(id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
} else {
|
2011-02-11 21:34:23 +00:00
|
|
|
mLruCache.remove(id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
// If there are no cursors using the old cached cursor, close it
|
|
|
|
if (!sActiveCursors.contains(c)) {
|
|
|
|
c.close();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (wasLocked) {
|
2010-11-17 06:49:04 +00:00
|
|
|
mLockMap.subtract(id);
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate the entire cache, without logging
|
|
|
|
*/
|
|
|
|
public synchronized void invalidate() {
|
|
|
|
invalidate(null, null, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidate the entire cache; the arguments are used for logging only, and indicate the
|
|
|
|
* write operation that caused the invalidation
|
|
|
|
*
|
|
|
|
* @param operation a string describing the operation causing the invalidate (or null)
|
|
|
|
* @param uri the uri causing the invalidate (or null)
|
|
|
|
* @param selection the selection used with the uri (or null)
|
|
|
|
*/
|
|
|
|
public synchronized void invalidate(String operation, Uri uri, String selection) {
|
|
|
|
if (DEBUG_CACHE && (operation != null)) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(mLogTag, "============ INVALIDATED BY " + operation + ": " + uri +
|
2010-10-27 23:50:54 +00:00
|
|
|
", SELECTION: " + selection);
|
|
|
|
}
|
|
|
|
mStats.mInvalidateCount++;
|
|
|
|
// Close all cached cursors that are no longer in use
|
2011-02-11 21:34:23 +00:00
|
|
|
mLruCache.evictAll();
|
2010-10-27 23:50:54 +00:00
|
|
|
// Invalidate all current tokens
|
|
|
|
mTokenList.invalidate();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Debugging code below
|
|
|
|
|
|
|
|
private void dumpOnCount(int num) {
|
|
|
|
mStats.mOpCount++;
|
|
|
|
if ((mStats.mOpCount % num) == 0) {
|
|
|
|
dumpStats();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*package*/ void recordQueryTime(Cursor c, long nanoTime) {
|
|
|
|
if (c instanceof CachedCursor) {
|
|
|
|
mStats.hitTimes += nanoTime;
|
|
|
|
mStats.hits++;
|
|
|
|
} else {
|
|
|
|
if (c.getCount() == 1) {
|
|
|
|
mStats.missTimes += nanoTime;
|
|
|
|
mStats.miss++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static synchronized void notCacheable(Uri uri, String selection) {
|
|
|
|
if (DEBUG_NOT_CACHEABLE) {
|
|
|
|
sNotCacheable++;
|
|
|
|
String str = uri.toString() + "$" + selection;
|
|
|
|
sNotCacheableMap.add(str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-17 17:47:36 +00:00
|
|
|
// For use with unit tests
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
public static void invalidateAllCaches() {
|
2010-11-17 17:47:36 +00:00
|
|
|
for (ContentCache cache: sContentCaches) {
|
|
|
|
cache.invalidate();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-06-08 18:51:55 +00:00
|
|
|
/** Sets the cache lock. If the lock is {@code true}, also invalidates all cached items. */
|
|
|
|
public static void setLockCacheForTest(boolean lock) {
|
|
|
|
sLockCache = lock;
|
|
|
|
if (sLockCache) {
|
Improve EmailContent caching...
* Guarantee that up to 16 Account (with HostAuths), and Policy rows
are always cached. Also, 6 commonly used Mailboxes per Account
(Inbox, Outbox, Drafts, Sent, Trash, and Search)
* Precache these rows when EmailProvider starts up
* Ensure that newly added, precachable rows are cached when created
* Clean up some inefficient/wrong caching code
* Fix a commonly called method in NotificationManager in which we
load a single Mailbox row using selection vs withAppendedId
* Confirm that we don't read from the database in typical use and
heavy message loading
* Add a special URI for finding mailbox by type (using the cache)
* Add special-case code for EmailContent.count(Account.CONTENT_URI)
which is used in a number of places (including on the UI thread)
and whose value is easily determined
* Add a special URI to get the default account id
* Confirm that all unit tests work
The goal here is to be able to load all Account, HostAuth, Policy,
and Mailbox objects (by id) without worrying about disk access.
There will still be a single disk read for uncommon Mailbox reads,
but this should be considered acceptable.
Change-Id: Ibc9aa7acc73185e360b0b6f3053b90a985e97210
TODO: Unit tests
2011-06-19 01:03:11 +00:00
|
|
|
invalidateAllCaches();
|
2011-06-08 18:51:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-10-27 23:50:54 +00:00
|
|
|
static class Statistics {
|
|
|
|
private final ContentCache mCache;
|
|
|
|
private final String mName;
|
|
|
|
|
|
|
|
// Cache statistics
|
|
|
|
// The item is in the cache AND is used to create a cursor
|
|
|
|
private int mHitCount = 0;
|
|
|
|
// Basic cache miss (the item is not cached)
|
|
|
|
private int mMissCount = 0;
|
|
|
|
// Incremented when a cachePut is invalid due to an intervening write
|
|
|
|
private int mStaleCount = 0;
|
|
|
|
// A projection miss occurs when the item is cached, but not all requested columns are
|
|
|
|
// available in the base projection
|
|
|
|
private int mProjectionMissCount = 0;
|
|
|
|
// Incremented whenever the entire cache is invalidated
|
|
|
|
private int mInvalidateCount = 0;
|
|
|
|
// Count of operations put/get
|
|
|
|
private int mOpCount = 0;
|
|
|
|
// The following are for timing statistics
|
|
|
|
private long hits = 0;
|
|
|
|
private long hitTimes = 0;
|
|
|
|
private long miss = 0;
|
|
|
|
private long missTimes = 0;
|
|
|
|
|
|
|
|
// Used in toString() and addCacheStatistics()
|
|
|
|
private int mCursorCount = 0;
|
|
|
|
private int mTokenCount = 0;
|
|
|
|
|
|
|
|
Statistics(ContentCache cache) {
|
|
|
|
mCache = cache;
|
|
|
|
mName = mCache.mName;
|
|
|
|
}
|
|
|
|
|
|
|
|
Statistics(String name) {
|
|
|
|
mCache = null;
|
|
|
|
mName = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void addCacheStatistics(ContentCache cache) {
|
|
|
|
if (cache != null) {
|
|
|
|
mHitCount += cache.mStats.mHitCount;
|
|
|
|
mMissCount += cache.mStats.mMissCount;
|
|
|
|
mProjectionMissCount += cache.mStats.mProjectionMissCount;
|
|
|
|
mStaleCount += cache.mStats.mStaleCount;
|
|
|
|
hitTimes += cache.mStats.hitTimes;
|
|
|
|
missTimes += cache.mStats.missTimes;
|
|
|
|
hits += cache.mStats.hits;
|
|
|
|
miss += cache.mStats.miss;
|
|
|
|
mCursorCount += cache.size();
|
|
|
|
mTokenCount += cache.mTokenList.size();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-06-25 22:15:02 +00:00
|
|
|
private static void append(StringBuilder sb, String name, Object value) {
|
2010-10-27 23:50:54 +00:00
|
|
|
sb.append(", ");
|
|
|
|
sb.append(name);
|
|
|
|
sb.append(": ");
|
|
|
|
sb.append(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
if (mHitCount + mMissCount == 0) return "No cache";
|
|
|
|
int totalTries = mMissCount + mProjectionMissCount + mHitCount;
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
sb.append("Cache " + mName);
|
|
|
|
append(sb, "Cursors", mCache == null ? mCursorCount : mCache.size());
|
|
|
|
append(sb, "Hits", mHitCount);
|
|
|
|
append(sb, "Misses", mMissCount + mProjectionMissCount);
|
|
|
|
append(sb, "Inval", mInvalidateCount);
|
|
|
|
append(sb, "Tokens", mCache == null ? mTokenCount : mCache.mTokenList.size());
|
|
|
|
append(sb, "Hit%", mHitCount * 100 / totalTries);
|
|
|
|
append(sb, "\nHit time", hitTimes / 1000000.0 / hits);
|
|
|
|
append(sb, "Miss time", missTimes / 1000000.0 / miss);
|
|
|
|
return sb.toString();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void dumpStats() {
|
|
|
|
Statistics totals = new Statistics("Totals");
|
|
|
|
|
|
|
|
for (ContentCache cache: sContentCaches) {
|
|
|
|
if (cache != null) {
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(cache.mName, cache.mStats.toString());
|
2010-10-27 23:50:54 +00:00
|
|
|
totals.addCacheStatistics(cache);
|
|
|
|
}
|
|
|
|
}
|
2013-05-26 04:32:32 +00:00
|
|
|
LogUtils.d(totals.mName, totals.toString());
|
2010-10-27 23:50:54 +00:00
|
|
|
}
|
|
|
|
}
|