Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 992f7d52 authored by Vasu Nori's avatar Vasu Nori
Browse files

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
parent f4cae9f9
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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"
+3 −2
Original line number Diff line number Diff line
@@ -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.isSqlInStatementCache(sql)) {
                            if (mPool.get(i).mDb.mCache.isSqlInStatementCache(sql)) {
                                poolObj = mPool.get(i);
                                break;
                            }
@@ -125,7 +125,8 @@ 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.isSqlInStatementCache(sql)) {
                    if (mPool.get(i).isFree() &&
                            mPool.get(i).mDb.mCache.isSqlInStatementCache(sql)) {
                        poolObj = mPool.get(i);
                        break;
                    }
+214 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+106 −49
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package android.database.sqlite;

import android.database.DatabaseUtils;
import android.util.Log;

/**
@@ -24,19 +25,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()}
 * 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 @@ import android.util.Log;
     */
    /* package */ int nStatement = 0;

    /** the following are for debugging purposes */
    private String mSqlStmt = null;
    private Throwable mStackTrace = null;

    /** 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");
        }
    /** 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;

    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 @@ import android.util.Log;
     * 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 @@ 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 statement in a finalizer. Please ensure " +
                    "that you explicitly call close() on your cursor: " +
                    mSqlStmt.substring(0, (len > 100) ? 100 : len), mStackTrace);
            releaseSqlStatement();
                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 @@ 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=");
+25 −163

File changed.

Preview size limit exceeded, changes collapsed.

Loading