diff --git a/api/current.xml b/api/current.xml index 310944f6c3e72..c17f2acdd03a8 100644 --- a/api/current.xml +++ b/api/current.xml @@ -65918,7 +65918,7 @@ return="void" abstract="false" native="false" - synchronized="false" + synchronized="true" static="false" final="false" deprecated="not deprecated" @@ -65957,7 +65957,7 @@ return="void" abstract="false" native="false" - synchronized="false" + synchronized="true" static="false" final="false" deprecated="not deprecated" diff --git a/core/java/android/database/sqlite/DatabaseConnectionPool.java b/core/java/android/database/sqlite/DatabaseConnectionPool.java index aede438657840..54b06054f86c4 100644 --- a/core/java/android/database/sqlite/DatabaseConnectionPool.java +++ b/core/java/android/database/sqlite/DatabaseConnectionPool.java @@ -99,7 +99,7 @@ import java.util.Random; poolObj = mPool.get(0); } else { for (int i = 0; i < mMaxPoolSize; i++) { - if (mPool.get(i).mDb.mCache.isSqlInStatementCache(sql)) { + if (mPool.get(i).mDb.isSqlInStatementCache(sql)) { poolObj = mPool.get(i); break; } @@ -125,8 +125,7 @@ import java.util.Random; // there are free connections available. pick one // preferably a connection caching the pre-compiled statement of the given SQL for (int i = 0; i < poolSize; i++) { - if (mPool.get(i).isFree() && - mPool.get(i).mDb.mCache.isSqlInStatementCache(sql)) { + if (mPool.get(i).isFree() && mPool.get(i).mDb.isSqlInStatementCache(sql)) { poolObj = mPool.get(i); break; } diff --git a/core/java/android/database/sqlite/SQLiteCache.java b/core/java/android/database/sqlite/SQLiteCache.java deleted file mode 100644 index 049fb626197f7..0000000000000 --- a/core/java/android/database/sqlite/SQLiteCache.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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 android.database.sqlite; - -import android.util.Log; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -/** - * For each instance of {@link SQLiteDatabase}, this class maintains a LRU cache to store - * the compiled query statement ids returned by sqlite database. - *

- *

- * If an application opens the database and keeps it open during its entire life, then - * there will not be an overhead of compilation of SQL statements by sqlite. - *

- * Why is this cache NOT static? because sqlite attaches compiled-sql statements to the - * database connections. - *

- * This cache has an upper limit of mMaxSqlCacheSize (settable by calling the method - * (@link #setMaxSqlCacheSize(int)}). - */ -/* package */ class SQLiteCache { - private static final String TAG = "SQLiteCache"; - - /** The {@link SQLiteDatabase} instance this cache is attached to */ - private final SQLiteDatabase mDatabase; - - /** Default statement-cache size per database connection ( = instance of this class) */ - private int mMaxSqlCacheSize = 25; - - /** The LRU cache */ - private final Map mCompiledQueries = - new LinkedHashMap(mMaxSqlCacheSize + 1, 0.75f, true) { - @Override - public boolean removeEldestEntry(Map.Entry eldest) { - // eldest = least-recently used entry - // if it needs to be removed to accommodate a new entry, - // close {@link SQLiteCompiledSql} represented by this entry, if not in use - // and then let it be removed from the Map. - mDatabase.verifyLockOwner(); - if (this.size() <= mMaxSqlCacheSize) { - // cache is not full. nothing needs to be removed - return false; - } - // cache is full. eldest will be removed. - eldest.getValue().releaseIfNotInUse(); - // return true, so that this entry is removed automatically by the caller. - return true; - } - }; - - /** Maintains whether or not cacheFullWarning has been logged */ - private boolean mCacheFullWarning; - - /** The following 2 members maintain stats about cache hits and misses */ - private int mNumCacheHits; - private int mNumCacheMisses; - - /** - * Constructor used by {@link SQLiteDatabase}. - * @param db - */ - /* package */ SQLiteCache(SQLiteDatabase db) { - mDatabase = db; - } - - /** - * Adds the given SQL and its compiled-statement to the cache, if the given SQL statement - * doesn't already exist in cache. - * - * @return true if added to cache. false otherwise. - */ - /* package */ synchronized boolean addToCompiledQueries(String sql, - SQLiteCompiledSql compiledStatement) { - if (mCompiledQueries.containsKey(sql)) { - // already exists. - return false; - } - - /* add the given SQLiteCompiledSql compiledStatement to cache. - * no need to worry about the cache size - because {@link #mCompiledQueries} - * self-limits its size to {@link #mMaxSqlCacheSize}. - */ - mCompiledQueries.put(sql, compiledStatement); - - // need to log a warning to say that the cache is full? - if (!isCacheFullWarningLogged() && mCompiledQueries.size() == mMaxSqlCacheSize) { - /* - * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. - * log a warning. - * chances are it is NOT using ? for bindargs - or cachesize is too small. - */ - Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + - mDatabase.getPath() + - ". Use setMaxSqlCacheSize() in SQLiteDatabase to increase cachesize. "); - setCacheFullWarningLogged(); - } - return true; - } - - /** - * Returns the compiled-statement for the given SQL statement, if the entry exists in cache - * and is free to use. Returns null otherwise. - *

- * If a compiled-sql statement is returned for the caller, it is reserved for the caller. - * So, don't use this method unless the caller needs to acquire the object. - */ - /* package */ synchronized SQLiteCompiledSql getCompiledStatementForSql(String sql) { - SQLiteCompiledSql compiledStatement = mCompiledQueries.get(sql); - if (compiledStatement == null) { - mNumCacheMisses++; - return null; - } - mNumCacheHits++; - // reserve it for the caller, if it is not already in use - if (!compiledStatement.acquire()) { - // couldn't acquire it since it is already in use. bug in app? - if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.w(TAG, "Possible bug: Either using the same SQL in 2 threads at " + - " the same time, or previous instance of this SQL statement was " + - "never close()d. " + compiledStatement.toString()); - } - return null; - } - return compiledStatement; - } - - /** - * If the given statement is in cache, it is released back to cache and it is made available for - * others to use. - *

- * return true if the statement is put back in cache, false otherwise (false = the statement - * is NOT in cache) - */ - /* package */ synchronized boolean releaseBackToCache(SQLiteCompiledSql stmt) { - if (!mCompiledQueries.containsValue(stmt)) { - return false; - } - // it is in cache. release it from the caller, make it available for others to use - stmt.free(); - return true; - } - - /** - * releases all compiled-sql statements in the cache. - */ - /* package */ synchronized void dealloc() { - for (SQLiteCompiledSql stmt : mCompiledQueries.values()) { - stmt.setState(SQLiteCompiledSql.NSTATE_CACHE_DEALLOC); - stmt.releaseFromDatabase(); - } - mCompiledQueries.clear(); - } - - /** - * see documentation on {@link SQLiteDatabase#setMaxSqlCacheSize(int)}. - */ - /* package */ synchronized void setMaxSqlCacheSize(int cacheSize) { - if (cacheSize > SQLiteDatabase.MAX_SQL_CACHE_SIZE || cacheSize < 0) { - throw new IllegalStateException("expected value between 0 and " + - SQLiteDatabase.MAX_SQL_CACHE_SIZE); - } else if (cacheSize < mMaxSqlCacheSize) { - throw new IllegalStateException("cannot set cacheSize to a value less than the value " + - "set with previous setMaxSqlCacheSize() call."); - } - mMaxSqlCacheSize = cacheSize; - } - - /* package */ synchronized boolean isSqlInStatementCache(String sql) { - return mCompiledQueries.containsKey(sql); - } - - private synchronized boolean isCacheFullWarningLogged() { - return mCacheFullWarning; - } - - private synchronized void setCacheFullWarningLogged() { - mCacheFullWarning = true; - } - /* package */ synchronized int getCacheHitNum() { - return mNumCacheHits; - } - /* package */ synchronized int getCacheMissNum() { - return mNumCacheMisses; - } - /* package */ synchronized int getCachesize() { - return mCompiledQueries.size(); - } - - // only for testing - /* package */ synchronized Set getKeys() { - return mCompiledQueries.keySet(); - } -} diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java index 120d090a881ea..0b5a4dff3d4d5 100644 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java @@ -16,7 +16,6 @@ package android.database.sqlite; -import android.database.DatabaseUtils; import android.util.Log; /** @@ -25,19 +24,19 @@ import android.util.Log; * and it is released in one of the 2 following ways * 1. when {@link SQLiteDatabase} object is closed. * 2. if this is not cached in {@link SQLiteDatabase}, {@link android.database.Cursor#close()} - * releases this obj. + * releaases this obj. */ /* package */ class SQLiteCompiledSql { private static final String TAG = "SQLiteCompiledSql"; /** The database this program is compiled against. */ - /* package */ final SQLiteDatabase mDatabase; + /* package */ SQLiteDatabase mDatabase; /** * Native linkage, do not modify. This comes from the database. */ - /* package */ final int nHandle; + /* package */ int nHandle = 0; /** * Native linkage, do not modify. When non-0 this holds a reference to a valid @@ -47,53 +46,52 @@ import android.util.Log; */ /* package */ int nStatement = 0; - /** the following 3 members are for debugging purposes */ - private final String mSqlStmt; - private final Throwable mStackTrace; - private int nState = 0; - /** values the above member can take */ - private static final int NSTATE_CACHEABLE = 64; - private static final int NSTATE_IN_CACHE = 32; - private static final int NSTATE_INUSE = 16; - private static final int NSTATE_INUSE_RESETMASK = 0x0f; - /* package */ static final int NSTATE_CLOSE_NOOP = 1; - private static final int NSTATE_EVICTED_FROM_CACHE = 2; - /* package */ static final int NSTATE_CACHE_DEALLOC = 4; - private static final int NSTATE_IN_FINALIZER_Q = 8; + /** the following are for debugging purposes */ + private String mSqlStmt = null; + private Throwable mStackTrace = null; - private SQLiteCompiledSql(SQLiteDatabase db, String sql) { - db.verifyDbIsOpen(); - db.verifyLockOwner(); + /** when in cache and is in use, this member is set */ + private boolean mInUse = false; + + /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) { + if (!db.isOpen()) { + throw new IllegalStateException("database " + db.getPath() + " already closed"); + } mDatabase = db; mSqlStmt = sql; mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - nHandle = db.mNativeHandle; - native_compile(sql); + this.nHandle = db.mNativeHandle; + compile(sql, true); } - /* package */ static SQLiteCompiledSql get(SQLiteDatabase db, String sql, int type) { - // only CRUD statements are cached. - if (type != DatabaseUtils.STATEMENT_SELECT && type != DatabaseUtils.STATEMENT_UPDATE) { - return new SQLiteCompiledSql(db, sql); + /** + * Compiles the given SQL into a SQLite byte code program using sqlite3_prepare_v2(). If + * this method has been called previously without a call to close and forCompilation is set + * to false the previous compilation will be used. Setting forceCompilation to true will + * always re-compile the program and should be done if you pass differing SQL strings to this + * method. + * + *

Note: this method acquires the database lock.

+ * + * @param sql the SQL string to compile + * @param forceCompilation forces the SQL to be recompiled in the event that there is an + * existing compiled SQL program already around + */ + private void compile(String sql, boolean forceCompilation) { + mDatabase.verifyLockOwner(); + // Only compile if we don't have a valid statement already or the caller has + // explicitly requested a recompile. + if (forceCompilation) { + // Note that the native_compile() takes care of destroying any previously + // existing programs before it compiles. + native_compile(sql); } - // the given SQL statement is cacheable. - SQLiteCompiledSql stmt = db.mCache.getCompiledStatementForSql(sql); - if (stmt != null) { - return stmt; - } - // either the entry doesn't exist in cache or the one in cache is currently in use. - // try to add it to cache and let cache worry about what copy to keep - stmt = new SQLiteCompiledSql(db, sql); - stmt.nState |= NSTATE_CACHEABLE | - ((db.mCache.addToCompiledQueries(sql, stmt)) ? NSTATE_IN_CACHE : 0); - return stmt; } - /* package */ synchronized void releaseFromDatabase() { + /* package */ void releaseSqlStatement() { // Note that native_finalize() checks to make sure that nStatement is // non-null before destroying it. if (nStatement != 0) { - nState |= NSTATE_IN_FINALIZER_Q; mDatabase.finalizeStatementLater(nStatement); nStatement = 0; } @@ -103,47 +101,20 @@ import android.util.Log; * returns true if acquire() succeeds. false otherwise. */ /* package */ synchronized boolean acquire() { - if ((nState & NSTATE_INUSE) > 0 ) { - // this object is already in use + if (mInUse) { + // someone already has acquired it. return false; } - nState |= NSTATE_INUSE; + mInUse = true; return true; } - /* package */ synchronized void free() { - nState &= NSTATE_INUSE_RESETMASK; + /* package */ synchronized void release() { + mInUse = false; } - /* package */ void release(int type) { - if (type != DatabaseUtils.STATEMENT_SELECT && type != DatabaseUtils.STATEMENT_UPDATE) { - // it is not cached. release its memory from the database. - releaseFromDatabase(); - return; - } - // if in cache, reset its in-use flag - if (!mDatabase.mCache.releaseBackToCache(this)) { - // not in cache. release its memory from the database. - releaseFromDatabase(); - } - } - - /* package */ synchronized void releaseIfNotInUse() { - nState |= NSTATE_EVICTED_FROM_CACHE; - // if it is not in use, release its memory from the database - if ((nState & NSTATE_INUSE) == 0) { - releaseFromDatabase(); - } - } - - // only for testing purposes /* package */ synchronized boolean isInUse() { - return (nState & NSTATE_INUSE) > 0; - } - - /* package */ synchronized SQLiteCompiledSql setState(int val) { - nState = nState & val; - return this; // for chaining + return mInUse; } /** @@ -154,18 +125,11 @@ import android.util.Log; try { if (nStatement == 0) return; // finalizer should NEVER get called - // but if the database itself is not closed and is GC'ed, then - // all sub-objects attached to the database could end up getting GC'ed too. - // in that case, don't print any warning. - if ((nState & NSTATE_INUSE) == 0) { - // no need to print warning - } else { - int len = mSqlStmt.length(); - Log.w(TAG, "Releasing SQL statement in finalizer. " + - "Could be due to close() not being called on the cursor or on the database. " + - toString(), mStackTrace); - } - releaseFromDatabase(); + int len = mSqlStmt.length(); + Log.w(TAG, "Releasing statement in a finalizer. Please ensure " + + "that you explicitly call close() on your cursor: " + + mSqlStmt.substring(0, (len > 100) ? 100 : len), mStackTrace); + releaseSqlStatement(); } finally { super.finalize(); } @@ -176,27 +140,6 @@ import android.util.Log; StringBuilder buff = new StringBuilder(); buff.append(" nStatement="); buff.append(nStatement); - if ((nState & NSTATE_CACHEABLE) > 0) { - buff.append(",cacheable"); - } - if ((nState & NSTATE_IN_CACHE) > 0) { - buff.append(",cached"); - } - if ((nState & NSTATE_INUSE) > 0) { - buff.append(",in_use"); - } - if ((nState & NSTATE_CLOSE_NOOP) > 0) { - buff.append(",no_op_close"); - } - if ((nState & NSTATE_EVICTED_FROM_CACHE) > 0) { - buff.append(",evicted_from_cache"); - } - if ((nState & NSTATE_CACHE_DEALLOC) > 0) { - buff.append(",dealloc_cache"); - } - if ((nState & NSTATE_IN_FINALIZER_Q) > 0) { - buff.append(",in dbFInalizerQ"); - } buff.append(", db="); buff.append(mDatabase.getPath()); buff.append(", db_connectionNum="); diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 70bd3d8304787..6937da01f6458 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -41,6 +41,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Random; @@ -259,11 +260,60 @@ public class SQLiteDatabase extends SQLiteClosable { private final WeakHashMap mPrograms; /** - * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}. - * Size of each prepared-statement is between 1K - 6K, depending on the complexity of the + * for each instance of this class, a LRU cache is maintained to store + * the compiled query statement ids returned by sqlite database. + * key = SQL statement with "?" for bind args + * value = {@link SQLiteCompiledSql} + * If an application opens the database and keeps it open during its entire life, then + * there will not be an overhead of compilation of SQL statements by sqlite. + * + * why is this cache NOT static? because sqlite attaches compiledsql statements to the + * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is + * invoked. + * + * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method + * (@link #setMaxSqlCacheSize(int)}). + */ + // default statement-cache size per database connection ( = instance of this class) + private int mMaxSqlCacheSize = 25; + /* package */ final Map mCompiledQueries = + new LinkedHashMap(mMaxSqlCacheSize + 1, 0.75f, true) { + @Override + public boolean removeEldestEntry(Map.Entry eldest) { + // eldest = least-recently used entry + // if it needs to be removed to accommodate a new entry, + // close {@link SQLiteCompiledSql} represented by this entry, if not in use + // and then let it be removed from the Map. + // when this is called, the caller must be trying to add a just-compiled stmt + // to cache; i.e., caller should already have acquired database lock AND + // the lock on mCompiledQueries. do as assert of these two 2 facts. + verifyLockOwner(); + if (this.size() <= mMaxSqlCacheSize) { + // cache is not full. nothing needs to be removed + return false; + } + // cache is full. eldest will be removed. + SQLiteCompiledSql entry = eldest.getValue(); + if (!entry.isInUse()) { + // this {@link SQLiteCompiledSql} is not in use. release it. + entry.releaseSqlStatement(); + } + // return true, so that this entry is removed automatically by the caller. + return true; + } + }; + /** + * absolute max value that can be set by {@link #setMaxSqlCacheSize(int)} + * size of each prepared-statement is between 1K - 6K, depending on the complexity of the * SQL statement & schema. */ public static final int MAX_SQL_CACHE_SIZE = 100; + private int mCacheFullWarnings; + private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1; + + /** maintain stats about number of cache hits and misses */ + private int mNumCacheHits; + private int mNumCacheMisses; /** Used to find out where this object was created in case it never got closed. */ private final Throwable mStackTrace; @@ -272,9 +322,6 @@ public class SQLiteDatabase extends SQLiteClosable { private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; private final int mSlowQueryThreshold; - /** the LRU cache */ - /* package */ final SQLiteCache mCache = new SQLiteCache(this); - /** stores the list of statement ids that need to be finalized by sqlite */ private final ArrayList mClosedStatementIds = new ArrayList(); @@ -1040,7 +1087,7 @@ public class SQLiteDatabase extends SQLiteClosable { * to be closed. sqlite doesn't let a database close if there are * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries. */ - mCache.dealloc(); + deallocCachedSqlStatements(); Iterator> iter = mPrograms.entrySet().iterator(); while (iter.hasNext()) { @@ -2043,20 +2090,113 @@ public class SQLiteDatabase extends SQLiteClosable { } } + /* + * ============================================================================ + * + * The following methods deal with compiled-sql cache + * ============================================================================ + */ + /** + * Adds the given SQL and its compiled-statement-id-returned-by-sqlite to the + * cache of compiledQueries attached to 'this'. + *

+ * If there is already a {@link SQLiteCompiledSql} in compiledQueries for the given SQL, + * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current + * mapping is NOT replaced with the new mapping). + */ + /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { + synchronized(mCompiledQueries) { + // don't insert the new mapping if a mapping already exists + if (mCompiledQueries.containsKey(sql)) { + return; + } + + if (mCompiledQueries.size() == mMaxSqlCacheSize) { + /* + * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. + * log a warning. + * chances are it is NOT using ? for bindargs - or cachesize is too small. + */ + if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { + Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + + getPath() + ". Use setMaxSqlCacheSize() to increase cachesize. "); + } + } + /* add the given SQLiteCompiledSql compiledStatement to cache. + * no need to worry about the cache size - because {@link #mCompiledQueries} + * self-limits its size to {@link #mMaxSqlCacheSize}. + */ + mCompiledQueries.put(sql, compiledStatement); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + + mCompiledQueries.size() + "|" + sql); + } + } + } + + /** package-level access for testing purposes */ + /* package */ void deallocCachedSqlStatements() { + synchronized (mCompiledQueries) { + for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) { + compiledSql.releaseSqlStatement(); + } + mCompiledQueries.clear(); + } + } + + /** + * From the compiledQueries cache, returns the compiled-statement-id for the given SQL. + * Returns null, if not found in the cache. + */ + /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { + SQLiteCompiledSql compiledStatement = null; + boolean cacheHit; + synchronized(mCompiledQueries) { + cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; + } + if (cacheHit) { + mNumCacheHits++; + } else { + mNumCacheMisses++; + } + + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|cache_stats|" + + getPath() + "|" + mCompiledQueries.size() + + "|" + mNumCacheHits + "|" + mNumCacheMisses + + "|" + cacheHit + "|" + sql); + } + return compiledStatement; + } + /** * Sets the maximum size of the prepared-statement cache for this database. * (size of the cache = number of compiled-sql-statements stored in the cache). *

- * Maximum cache size can ONLY be increased from its current size (default = 25). + * Maximum cache size can ONLY be increased from its current size (default = 10). * If this method is called with smaller size than the current maximum value, * then IllegalStateException is thrown. + *

+ * This method is thread-safe. * * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE}) * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE} or * the value set with previous setMaxSqlCacheSize() call. */ - public void setMaxSqlCacheSize(int cacheSize) { - mCache.setMaxSqlCacheSize(cacheSize); + public synchronized void setMaxSqlCacheSize(int cacheSize) { + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE); + } else if (cacheSize < mMaxSqlCacheSize) { + throw new IllegalStateException("cannot set cacheSize to a value less than the value " + + "set with previous setMaxSqlCacheSize() call."); + } + mMaxSqlCacheSize = cacheSize; + } + + /* package */ boolean isSqlInStatementCache(String sql) { + synchronized (mCompiledQueries) { + return mCompiledQueries.containsKey(sql); + } } /* package */ void finalizeStatementLater(int id) { @@ -2212,19 +2352,17 @@ public class SQLiteDatabase extends SQLiteClosable { * * @param size the value the connection handle pool size should be set to. */ - public void setConnectionPoolSize(int size) { - synchronized(this) { - if (mConnectionPool == null) { - throw new IllegalStateException("connection pool not enabled"); - } - int i = mConnectionPool.getMaxPoolSize(); - if (size < i) { - throw new IllegalArgumentException( - "cannot set max pool size to a value less than the current max value(=" + - i + ")"); - } - mConnectionPool.setMaxPoolSize(size); + public synchronized void setConnectionPoolSize(int size) { + if (mConnectionPool == null) { + throw new IllegalStateException("connection pool not enabled"); } + int i = mConnectionPool.getMaxPoolSize(); + if (size < i) { + throw new IllegalArgumentException( + "cannot set max pool size to a value less than the current max value(=" + + i + ")"); + } + mConnectionPool.setMaxPoolSize(size); } /* package */ SQLiteDatabase createPoolConnection(short connectionNum) { @@ -2340,16 +2478,16 @@ public class SQLiteDatabase extends SQLiteClosable { } if (pageCount > 0) { dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), - lookasideUsed, db.mCache.getCacheHitNum(), - db.mCache.getCacheMissNum(), db.mCache.getCachesize())); + lookasideUsed, db.mNumCacheHits, db.mNumCacheMisses, + db.mCompiledQueries.size())); } } // if there are pooled connections, return the cache stats for them also. if (db.mConnectionPool != null) { for (SQLiteDatabase pDb : db.mConnectionPool.getConnectionList()) { dbStatsList.add(new DbStats("(pooled # " + pDb.mConnectionNum + ") " - + lastnode, 0, 0, 0, pDb.mCache.getCacheHitNum(), - pDb.mCache.getCacheMissNum(), pDb.mCache.getCachesize())); + + lastnode, 0, 0, 0, pDb.mNumCacheHits, pDb.mNumCacheMisses, + pDb.mCompiledQueries.size())); } } } catch (SQLiteException e) { diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index b3903693dfa44..c7c0c79a31649 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -110,6 +110,42 @@ public abstract class SQLiteProgram extends SQLiteClosable { } } + private void compileSql() { + // only cache CRUD statements + if (mStatementType != DatabaseUtils.STATEMENT_SELECT && + mStatementType != DatabaseUtils.STATEMENT_UPDATE) { + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); + nStatement = mCompiledSql.nStatement; + // since it is not in the cache, no need to acquire() it. + return; + } + + mCompiledSql = mDatabase.getCompiledStatementForSql(mSql); + if (mCompiledSql == null) { + // create a new compiled-sql obj + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); + + // add it to the cache of compiled-sqls + // but before adding it and thus making it available for anyone else to use it, + // make sure it is acquired by me. + mCompiledSql.acquire(); + mDatabase.addToCompiledQueries(mSql, mCompiledSql); + } else { + // it is already in compiled-sql cache. + // try to acquire the object. + if (!mCompiledSql.acquire()) { + int last = mCompiledSql.nStatement; + // the SQLiteCompiledSql in cache is in use by some other SQLiteProgram object. + // we can't have two different SQLiteProgam objects can't share the same + // CompiledSql object. create a new one. + // finalize it when I am done with it in "this" object. + mCompiledSql = new SQLiteCompiledSql(mDatabase, mSql); + // since it is not in the cache, no need to acquire() it. + } + } + nStatement = mCompiledSql.nStatement; + } + @Override protected void onAllReferencesReleased() { release(); @@ -127,7 +163,16 @@ public abstract class SQLiteProgram extends SQLiteClosable { if (mCompiledSql == null) { return; } - mCompiledSql.release(mStatementType); + synchronized(mDatabase.mCompiledQueries) { + if (!mDatabase.mCompiledQueries.containsValue(mCompiledSql)) { + // it is NOT in compiled-sql cache. i.e., responsibility of + // releasing this statement is on me. + mCompiledSql.releaseSqlStatement(); + } else { + // it is in compiled-sql cache. reset its CompiledSql#mInUse flag + mCompiledSql.release(); + } + } mCompiledSql = null; nStatement = 0; } @@ -288,9 +333,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { synchronized (this) { mBindArgs = null; if (nHandle == 0 || !mDatabase.isOpen()) { - if (mCompiledSql != null) { - mCompiledSql.setState(SQLiteCompiledSql.NSTATE_CLOSE_NOOP); - } return; } releaseReference(); @@ -307,8 +349,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { /* package */ synchronized void compileAndbindAllArgs() { if (nStatement == 0) { // SQL statement is not compiled yet. compile it now. - mCompiledSql = SQLiteCompiledSql.get(mDatabase, mSql, mStatementType); - nStatement = mCompiledSql.nStatement; + compileSql(); } if (mBindArgs == null) { return; diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteCacheTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteCacheTest.java deleted file mode 100644 index 245981531c83b..0000000000000 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteCacheTest.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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 android.database.sqlite; - - -import android.content.Context; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDatabaseTest.ClassToTestSqlCompilationAndCaching; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import java.io.File; -import java.util.ArrayList; - -public class SQLiteCacheTest extends AndroidTestCase { - private SQLiteDatabase mDatabase; - private File mDatabaseFile; - private static final String TABLE_NAME = "testCache"; - private SQLiteCache mCache; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); - mDatabaseFile = new File(dbDir, "test.db"); - if (mDatabaseFile.exists()) { - mDatabaseFile.delete(); - } - mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); - assertNotNull(mDatabase); - mCache = mDatabase.mCache; - assertNotNull(mCache); - - // create a test table - mDatabase.execSQL("CREATE TABLE " + TABLE_NAME + " (i int, j int);"); - } - - @Override - protected void tearDown() throws Exception { - mDatabase.lock(); - // flush the above statement from cache and close all the pending statements to be released - mCache.dealloc(); - mDatabase.closePendingStatements(); - mDatabase.unlock(); - assertEquals(0, mDatabase.getQueuedUpStmtList().size()); - mDatabase.close(); - mDatabaseFile.delete(); - super.tearDown(); - } - - /** - * do N+1 queries - and when the 0th entry is removed from LRU cache due to the - * insertion of (N+1)th entry, make sure 0th entry is closed - */ - @SmallTest - public void testLruCaching() { - mDatabase.disableWriteAheadLogging(); - // set cache size - int N = 25; - mDatabase.setMaxSqlCacheSize(N); - - ArrayList stmtObjs = new ArrayList(); - ArrayList sqlStrings = new ArrayList(); - int stmt0 = 0; - for (int i = 0; i < N+1; i++) { - String s = "insert into " + TABLE_NAME + " values(" + i + ",?);"; - sqlStrings.add(s); - ClassToTestSqlCompilationAndCaching c = - ClassToTestSqlCompilationAndCaching.create(mDatabase, s); - int n = c.getSqlStatementId(); - stmtObjs.add(i, n); - if (i == 0) { - // save the statementId of this obj. we want to make sure it is thrown out of - // the cache at the end of this test. - stmt0 = n; - } - c.close(); - } - assertEquals(N, mCache.getCachesize()); - // is 0'th entry out of the cache? it should be in the list of statementIds - // corresponding to the pre-compiled sql statements to be finalized. - assertTrue(mDatabase.getQueuedUpStmtList().contains(stmt0)); - for (int i = 1; i < N+1; i++) { - SQLiteCompiledSql compSql = - mDatabase.mCache.getCompiledStatementForSql(sqlStrings.get(i)); - assertNotNull(compSql); - assertTrue(stmtObjs.contains(compSql.nStatement)); - } - assertEquals(N, mCache.getCachesize()); - - } - - /** - * Cache should only have Select / Insert / Update / Delete / Replace. - */ - @SmallTest - public void testCachingOfCRUDstatementsOnly() { - ClassToTestSqlCompilationAndCaching c; - // do some CRUD sql - int crudSqlNum = 7 * 4; - mDatabase.setMaxSqlCacheSize(crudSqlNum); - for (int i = 0; i < crudSqlNum / 4; i++) { - c= ClassToTestSqlCompilationAndCaching.create(mDatabase, - "insert into " + TABLE_NAME + " values(" + i + ",?);"); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "update " + TABLE_NAME + " set i = " + i); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "select * from " + TABLE_NAME + " where i = " + i); - c.close(); - c= ClassToTestSqlCompilationAndCaching.create(mDatabase, - "delete from " + TABLE_NAME + " where i = " + i); - c.close(); - } - // do some non-CRUD sql - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "create table j (i int);"); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "pragma database_list;"); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "begin transaction;"); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "commit;"); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "attach database \"blah\" as blah_db;"); - c.close(); - // cache size should be crudSqlNum - assertEquals(crudSqlNum, mCache.getCachesize()); - // everything in the cache should be a CRUD sql statement - for (String s : mCache.getKeys()) { - int type = DatabaseUtils.getSqlStatementType(s); - assertTrue(type == DatabaseUtils.STATEMENT_SELECT || - type == DatabaseUtils.STATEMENT_UPDATE); - } - } - - /** - * calling SQLiteCache.getCompiledStatementForSql() should reserve the cached-entry - * for the caller, if the entry exists - */ - @SmallTest - public void testGetShouldReserveEntry() { - String sql = "insert into " + TABLE_NAME + " values(1,?);"; - ClassToTestSqlCompilationAndCaching c = - ClassToTestSqlCompilationAndCaching.create(mDatabase, sql); - c.close(); - SQLiteCompiledSql compiledSql = mCache.getCompiledStatementForSql(sql); - assertNotNull(compiledSql); - assertTrue(compiledSql.isInUse()); - // get entry for the same sql again. should get null and a warning in log - assertNull(mCache.getCompiledStatementForSql(sql)); - compiledSql.free(); - assertFalse(compiledSql.isInUse()); - } -} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteCompiledSqlTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteCompiledSqlTest.java deleted file mode 100644 index 1606cf6bfd9a0..0000000000000 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteCompiledSqlTest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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 android.database.sqlite; - - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteDatabaseTest.ClassToTestSqlCompilationAndCaching; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -import java.io.File; - -public class SQLiteCompiledSqlTest extends AndroidTestCase { - private SQLiteDatabase mDatabase; - private File mDatabaseFile; - private static final String TABLE_NAME = "testCache"; - private SQLiteCache mCache; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - File dbDir = getContext().getDir(this.getClass().getName(), Context.MODE_PRIVATE); - mDatabaseFile = new File(dbDir, "sqlitecursor_test.db"); - if (mDatabaseFile.exists()) { - mDatabaseFile.delete(); - } - mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null); - assertNotNull(mDatabase); - mCache = mDatabase.mCache; - assertNotNull(mCache); - - // create a test table - mDatabase.execSQL("CREATE TABLE " + TABLE_NAME + " (i int, j int);"); - } - - @Override - protected void tearDown() throws Exception { - mDatabase.close(); - mDatabaseFile.delete(); - super.tearDown(); - } - - /** - * releaseIfNotInUse() should release only if it is not in use - */ - @SmallTest - public void testReleaseIfNotInUse() { - ClassToTestSqlCompilationAndCaching c; - // do some CRUD sql - int crudSqlNum = 20 * 4; - mDatabase.setMaxSqlCacheSize(crudSqlNum); - for (int i = 0; i < crudSqlNum / 4; i++) { - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "insert into " + TABLE_NAME + " values(" + i + ",?);"); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "update " + TABLE_NAME + " set i = " + i); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "select * from " + TABLE_NAME + " where i = " + i); - c.close(); - c = ClassToTestSqlCompilationAndCaching.create(mDatabase, - "delete from " + TABLE_NAME + " where i = " + i); - c.close(); - } - assertEquals(crudSqlNum, mCache.getCachesize()); - String sql = "insert into " + TABLE_NAME + " values(1,?);"; - SQLiteCompiledSql compiledSql = mCache.getCompiledStatementForSql(sql); - assertNotNull(compiledSql); - assertTrue(compiledSql.isInUse()); - // the object is in use. try to release it - compiledSql.releaseIfNotInUse(); - // compiledSql should not be released yet - int stmtId = compiledSql.nStatement; - assertTrue(stmtId > 0); - // free the object and call releaseIfNotInUse() again - and it should work this time - compiledSql.free(); - assertFalse(compiledSql.isInUse()); - compiledSql.releaseIfNotInUse(); - assertEquals(0, compiledSql.nStatement); - assertTrue(mDatabase.getQueuedUpStmtList().contains(stmtId)); - } -} diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index 3249cb63f45f9..86eda710266f6 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -447,6 +447,42 @@ public class SQLiteDatabaseTest extends AndroidTestCase { } } + @SmallTest + public void testLruCachingOfSqliteCompiledSqlObjs() { + createTableAndClearCache(); + // set cache size + int N = SQLiteDatabase.MAX_SQL_CACHE_SIZE; + mDatabase.setMaxSqlCacheSize(N); + + // do N+1 queries - and when the 0th entry is removed from LRU cache due to the + // insertion of (N+1)th entry, make sure 0th entry is closed + ArrayList stmtObjs = new ArrayList(); + ArrayList sqlStrings = new ArrayList(); + int stmt0 = 0; + for (int i = 0; i < N+1; i++) { + String s = "insert into test values(" + i + ",?);"; + sqlStrings.add(s); + ClassToTestSqlCompilationAndCaching c = + ClassToTestSqlCompilationAndCaching.create(mDatabase, s); + int n = c.getSqlStatementId(); + stmtObjs.add(i, n); + if (i == 0) { + // save the statementId of this obj. we want to make sure it is thrown out of + // the cache at the end of this test. + stmt0 = n; + } + c.close(); + } + // is 0'th entry out of the cache? it should be in the list of statementIds + // corresponding to the pre-compiled sql statements to be finalized. + assertTrue(mDatabase.getQueuedUpStmtList().contains(stmt0)); + for (int i = 1; i < N+1; i++) { + SQLiteCompiledSql compSql = mDatabase.getCompiledStatementForSql(sqlStrings.get(i)); + assertNotNull(compSql); + assertTrue(stmtObjs.contains(compSql.nStatement)); + } + } + @MediumTest public void testDbCloseReleasingAllCachedSql() { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, text1 TEXT, text2 TEXT, " + @@ -467,7 +503,7 @@ public class SQLiteDatabaseTest extends AndroidTestCase { mDatabase.enableWriteAheadLogging(); mDatabase.lock(); // flush the above statement from cache and close all the pending statements to be released - mDatabase.mCache.dealloc(); + mDatabase.deallocCachedSqlStatements(); mDatabase.closePendingStatements(); mDatabase.unlock(); assertEquals(0, mDatabase.getQueuedUpStmtList().size()); diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java index bdd6d5cc57e45..217545f9cf6c0 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java @@ -53,12 +53,6 @@ public class SQLiteStatementTest extends AndroidTestCase { * Start 2 threads to repeatedly execute the above SQL statement. * Even though 2 threads are executing the same SQL, they each should get their own copy of * prepared SQL statement id and there SHOULD NOT be an error from sqlite or android. - *

- * This method will produce a lot of the following warnings: - * Possible bug: Either using the same SQL in 2 threads at the same time, or - * previous instance of this SQL statement was never close()d. - * That is expected behavior. - * * @throws InterruptedException thrown if the test threads started by this test are interrupted */ @LargeTest