caching code retooled to reduce locking + handle SMP

1. Moved all code related to compiled-sql statement cache to SQLiteCache.java
Removed all caching related code from everywhere else.
2. Moved all code related to compiling a sql statement and caching it to
SQLiteCompiledSql.java. There was some code in SQLiteProgram.java
releated to this. moved it out.
3. Added state to SQLiteCompiledSql. This is to help in debugging.
Change-Id: I63ab0c9c4419e964eb9796d284dd389985763d83
diff --git a/api/current.xml b/api/current.xml
index 4c549bd..ed51bbf 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -65918,7 +65918,7 @@
  return="void"
  abstract="false"
  native="false"
- synchronized="true"
+ synchronized="false"
  static="false"
  final="false"
  deprecated="not deprecated"
@@ -65957,7 +65957,7 @@
  return="void"
  abstract="false"
  native="false"
- synchronized="true"
+ synchronized="false"
  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 54b0605..aede438 100644
--- a/core/java/android/database/sqlite/DatabaseConnectionPool.java
+++ b/core/java/android/database/sqlite/DatabaseConnectionPool.java
@@ -99,7 +99,7 @@
                         poolObj = mPool.get(0);
                     } else {
                         for (int i = 0; i < mMaxPoolSize; i++) {
-                            if (mPool.get(i).mDb.isSqlInStatementCache(sql)) {
+                            if (mPool.get(i).mDb.mCache.isSqlInStatementCache(sql)) {
                                 poolObj = mPool.get(i);
                                 break;
                             }
@@ -125,7 +125,8 @@
                 // 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.isSqlInStatementCache(sql)) {
+                    if (mPool.get(i).isFree() &&
+                            mPool.get(i).mDb.mCache.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
new file mode 100644
index 0000000..049fb62
--- /dev/null
+++ b/core/java/android/database/sqlite/SQLiteCache.java
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ *<p>
+ *<ul>
+ *     <li>key = SQL statement with "?" for bind args</li>
+ *     <li>value = {@link SQLiteCompiledSql}</li>
+ *</ul>
+ * 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.
+ *<p>
+ * Why is this cache NOT static? because sqlite attaches compiled-sql statements to the
+ * database connections.
+ *<p>
+ * 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<String, SQLiteCompiledSql> mCompiledQueries =
+        new LinkedHashMap<String, SQLiteCompiledSql>(mMaxSqlCacheSize + 1, 0.75f, true) {
+            @Override
+            public boolean removeEldestEntry(Map.Entry<String, SQLiteCompiledSql> 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.
+     * <p>
+     * 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.
+     * <p>
+     * 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<String> getKeys() {
+        return mCompiledQueries.keySet();
+    }
+}
diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java
index 0b5a4df..120d090 100644
--- a/core/java/android/database/sqlite/SQLiteCompiledSql.java
+++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java
@@ -16,6 +16,7 @@
 
 package android.database.sqlite;
 
+import android.database.DatabaseUtils;
 import android.util.Log;
 
 /**
@@ -24,19 +25,19 @@
  * 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()}
- * releaases this obj.
+ * releases this obj.
  */
 /* package */ class SQLiteCompiledSql {
 
     private static final String TAG = "SQLiteCompiledSql";
 
     /** The database this program is compiled against. */
-    /* package */ SQLiteDatabase mDatabase;
+    /* package */ final SQLiteDatabase mDatabase;
 
     /**
      * Native linkage, do not modify. This comes from the database.
      */
-    /* package */ int nHandle = 0;
+    /* package */ final int nHandle;
 
     /**
      * Native linkage, do not modify. When non-0 this holds a reference to a valid
@@ -46,52 +47,53 @@
      */
     /* package */ int nStatement = 0;
 
-    /** the following are for debugging purposes */
-    private String mSqlStmt = null;
-    private Throwable mStackTrace = null;
+    /** 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;
 
-    /** 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");
-        }
+    private SQLiteCompiledSql(SQLiteDatabase db, String sql) {
+        db.verifyDbIsOpen();
+        db.verifyLockOwner();
         mDatabase = db;
         mSqlStmt = sql;
         mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
-        this.nHandle = db.mNativeHandle;
-        compile(sql, true);
+        nHandle = db.mNativeHandle;
+        native_compile(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.
-     *
-     * <P>Note: this method acquires the database lock.</P>
-     *
-     * @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);
+    /* 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);
         }
+        // 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 */ void releaseSqlStatement() {
+    /* package */ synchronized void releaseFromDatabase() {
         // 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;
         }
@@ -101,20 +103,47 @@
      * returns true if acquire() succeeds. false otherwise.
      */
     /* package */ synchronized boolean acquire() {
-        if (mInUse) {
-            // someone already has acquired it.
+        if ((nState & NSTATE_INUSE) > 0 ) {
+            // this object is already in use
             return false;
         }
-        mInUse = true;
+        nState |= NSTATE_INUSE;
         return true;
     }
 
-    /* package */ synchronized void release() {
-        mInUse = false;
+    /* package */ synchronized void free() {
+        nState &= NSTATE_INUSE_RESETMASK;
     }
 
+    /* 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 mInUse;
+        return (nState & NSTATE_INUSE) > 0;
+    }
+
+    /* package */ synchronized SQLiteCompiledSql setState(int val) {
+        nState = nState & val;
+        return this; // for chaining
     }
 
     /**
@@ -125,11 +154,18 @@
         try {
             if (nStatement == 0) return;
             // finalizer should NEVER get called
-            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();
+            // 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();
         } finally {
             super.finalize();
         }
@@ -140,6 +176,27 @@
             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 6937da0..70bd3d8 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -41,7 +41,6 @@
 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;
@@ -260,60 +259,11 @@
     private final WeakHashMap<SQLiteClosable, Object> mPrograms;
 
     /**
-     * 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<String, SQLiteCompiledSql> mCompiledQueries =
-        new LinkedHashMap<String, SQLiteCompiledSql>(mMaxSqlCacheSize + 1, 0.75f, true) {
-            @Override
-            public boolean removeEldestEntry(Map.Entry<String, SQLiteCompiledSql> 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
+     * 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;
@@ -322,6 +272,9 @@
     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<Integer> mClosedStatementIds = new ArrayList<Integer>();
 
@@ -1087,7 +1040,7 @@
          * to be closed. sqlite doesn't let a database close if there are
          * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries.
          */
-        deallocCachedSqlStatements();
+        mCache.dealloc();
 
         Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
         while (iter.hasNext()) {
@@ -2090,113 +2043,20 @@
         }
     }
 
-    /*
-     * ============================================================================
-     *
-     *       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'.
-     * <p>
-     * 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).
      *<p>
-     * Maximum cache size can ONLY be increased from its current size (default = 10).
+     * Maximum cache size can ONLY be increased from its current size (default = 25).
      * If this method is called with smaller size than the current maximum value,
      * then IllegalStateException is thrown.
-     *<p>
-     * 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 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);
-        }
+    public void setMaxSqlCacheSize(int cacheSize) {
+        mCache.setMaxSqlCacheSize(cacheSize);
     }
 
     /* package */ void finalizeStatementLater(int id) {
@@ -2352,17 +2212,19 @@
      *
      * @param size the value the connection handle pool size should be set to.
      */
-    public synchronized void setConnectionPoolSize(int size) {
-        if (mConnectionPool == null) {
-            throw new IllegalStateException("connection pool not enabled");
+    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);
         }
-        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) {
@@ -2478,16 +2340,16 @@
                         }
                         if (pageCount > 0) {
                             dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(),
-                                    lookasideUsed, db.mNumCacheHits, db.mNumCacheMisses,
-                                    db.mCompiledQueries.size()));
+                                    lookasideUsed, db.mCache.getCacheHitNum(),
+                                    db.mCache.getCacheMissNum(), db.mCache.getCachesize()));
                         }
                     }
                     // 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.mNumCacheHits, pDb.mNumCacheMisses,
-                                    pDb.mCompiledQueries.size()));
+                                    + lastnode, 0, 0, 0, pDb.mCache.getCacheHitNum(),
+                                    pDb.mCache.getCacheMissNum(), pDb.mCache.getCachesize()));
                         }
                     }
                 } catch (SQLiteException e) {
diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java
index c7c0c79..b390369 100644
--- a/core/java/android/database/sqlite/SQLiteProgram.java
+++ b/core/java/android/database/sqlite/SQLiteProgram.java
@@ -110,42 +110,6 @@
         }
     }
 
-    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();
@@ -163,16 +127,7 @@
         if (mCompiledSql == null) {
             return;
         }
-        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.release(mStatementType);
         mCompiledSql = null;
         nStatement = 0;
     }
@@ -333,6 +288,9 @@
         synchronized (this) {
             mBindArgs = null;
             if (nHandle == 0 || !mDatabase.isOpen()) {
+                if (mCompiledSql != null) {
+                    mCompiledSql.setState(SQLiteCompiledSql.NSTATE_CLOSE_NOOP);
+                }
                 return;
             }
             releaseReference();
@@ -349,7 +307,8 @@
     /* package */ synchronized void compileAndbindAllArgs() {
         if (nStatement == 0) {
             // SQL statement is not compiled yet. compile it now.
-            compileSql();
+            mCompiledSql = SQLiteCompiledSql.get(mDatabase, mSql, mStatementType);
+            nStatement = mCompiledSql.nStatement;
         }
         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
new file mode 100644
index 0000000..2459815
--- /dev/null
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteCacheTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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<Integer> stmtObjs = new ArrayList<Integer>();
+        ArrayList<String> sqlStrings = new ArrayList<String>();
+        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
new file mode 100644
index 0000000..1606cf6
--- /dev/null
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteCompiledSqlTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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 86eda71..3249cb6 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -447,42 +447,6 @@
         }
     }
 
-    @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<Integer> stmtObjs = new ArrayList<Integer>();
-        ArrayList<String> sqlStrings = new ArrayList<String>();
-        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, " +
@@ -503,7 +467,7 @@
         mDatabase.enableWriteAheadLogging();
         mDatabase.lock();
         // flush the above statement from cache and close all the pending statements to be released
-        mDatabase.deallocCachedSqlStatements();
+        mDatabase.mCache.dealloc();
         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 217545f..bdd6d5c 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteStatementTest.java
@@ -53,6 +53,12 @@
      * 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.
+     *<p>
+     * 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