diff options
Diffstat (limited to 'core/java/android/database/sqlite/SQLiteDatabase.java')
-rw-r--r-- | core/java/android/database/sqlite/SQLiteDatabase.java | 726 |
1 files changed, 605 insertions, 121 deletions
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 9ebf5d9..fb5507d 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,10 +16,14 @@ package android.database.sqlite; +import com.google.android.collect.Maps; + +import android.app.ActivityThread; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.SQLException; +import android.database.sqlite.SQLiteDebug.DbStats; import android.os.Debug; import android.os.SystemClock; import android.os.SystemProperties; @@ -27,15 +31,22 @@ import android.text.TextUtils; import android.util.Config; import android.util.EventLog; import android.util.Log; +import android.util.Pair; import java.io.File; +import java.lang.ref.WeakReference; +import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.locks.ReentrantLock; +import java.util.regex.Pattern; /** * Exposes methods to manage a SQLite database. @@ -60,65 +71,61 @@ public class SQLiteDatabase extends SQLiteClosable { /** * Algorithms used in ON CONFLICT clause * http://www.sqlite.org/lang_conflict.html - * @hide */ - public enum ConflictAlgorithm { - /** - * When a constraint violation occurs, an immediate ROLLBACK occurs, - * thus ending the current transaction, and the command aborts with a - * return code of SQLITE_CONSTRAINT. If no transaction is active - * (other than the implied transaction that is created on every command) - * then this algorithm works the same as ABORT. - */ - ROLLBACK("ROLLBACK"), + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, + * thus ending the current transaction, and the command aborts with a + * return code of SQLITE_CONSTRAINT. If no transaction is active + * (other than the implied transaction that is created on every command) + * then this algorithm works the same as ABORT. + */ + public static final int CONFLICT_ROLLBACK = 1; - /** - * When a constraint violation occurs,no ROLLBACK is executed - * so changes from prior commands within the same transaction - * are preserved. This is the default behavior. - */ - ABORT("ABORT"), + /** + * When a constraint violation occurs,no ROLLBACK is executed + * so changes from prior commands within the same transaction + * are preserved. This is the default behavior. + */ + public static final int CONFLICT_ABORT = 2; - /** - * When a constraint violation occurs, the command aborts with a return - * code SQLITE_CONSTRAINT. But any changes to the database that - * the command made prior to encountering the constraint violation - * are preserved and are not backed out. - */ - FAIL("FAIL"), + /** + * When a constraint violation occurs, the command aborts with a return + * code SQLITE_CONSTRAINT. But any changes to the database that + * the command made prior to encountering the constraint violation + * are preserved and are not backed out. + */ + public static final int CONFLICT_FAIL = 3; - /** - * When a constraint violation occurs, the one row that contains - * the constraint violation is not inserted or changed. - * But the command continues executing normally. Other rows before and - * after the row that contained the constraint violation continue to be - * inserted or updated normally. No error is returned. - */ - IGNORE("IGNORE"), + /** + * When a constraint violation occurs, the one row that contains + * the constraint violation is not inserted or changed. + * But the command continues executing normally. Other rows before and + * after the row that contained the constraint violation continue to be + * inserted or updated normally. No error is returned. + */ + public static final int CONFLICT_IGNORE = 4; - /** - * When a UNIQUE constraint violation occurs, the pre-existing rows that - * are causing the constraint violation are removed prior to inserting - * or updating the current row. Thus the insert or update always occurs. - * The command continues executing normally. No error is returned. - * If a NOT NULL constraint violation occurs, the NULL value is replaced - * by the default value for that column. If the column has no default - * value, then the ABORT algorithm is used. If a CHECK constraint - * violation occurs then the IGNORE algorithm is used. When this conflict - * resolution strategy deletes rows in order to satisfy a constraint, - * it does not invoke delete triggers on those rows. - * This behavior might change in a future release. - */ - REPLACE("REPLACE"); + /** + * When a UNIQUE constraint violation occurs, the pre-existing rows that + * are causing the constraint violation are removed prior to inserting + * or updating the current row. Thus the insert or update always occurs. + * The command continues executing normally. No error is returned. + * If a NOT NULL constraint violation occurs, the NULL value is replaced + * by the default value for that column. If the column has no default + * value, then the ABORT algorithm is used. If a CHECK constraint + * violation occurs then the IGNORE algorithm is used. When this conflict + * resolution strategy deletes rows in order to satisfy a constraint, + * it does not invoke delete triggers on those rows. + * This behavior might change in a future release. + */ + public static final int CONFLICT_REPLACE = 5; - private final String mValue; - ConflictAlgorithm(String value) { - mValue = value; - } - public String value() { - return mValue; - } - } + /** + * use the following when no conflict action is specified. + */ + public static final int CONFLICT_NONE = 0; + private static final String[] CONFLICT_VALUES = new String[] + {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; /** * Maximum Length Of A LIKE Or GLOB Pattern @@ -198,8 +205,31 @@ public class SQLiteDatabase extends SQLiteClosable { private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000; + // The pattern we remove from database filenames before + // potentially logging them. + private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); + private long mLastLockMessageTime = 0L; + // Things related to query logging/sampling for debugging + // slow/frequent queries during development. Always log queries + // which take (by default) 500ms+; shorter queries are sampled + // accordingly. Commit statements, which are typically slow, are + // logged together with the most recently executed SQL statement, + // for disambiguation. The 500ms value is configurable via a + // SystemProperty, but developers actively debugging database I/O + // should probably use the regular log tunable, + // LOG_SLOW_QUERIES_PROPERTY, defined below. + private static int sQueryLogTimeInMillis = 0; // lazily initialized + private static final int QUERY_LOG_SQL_LENGTH = 64; + private static final String COMMIT_SQL = "COMMIT;"; + private final Random mRandom = new Random(); + private String mLastSqlStatement = null; + + // String prefix for slow database query EventLog records that show + // lock acquistions of the database. + /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:"; + /** Used by native code, do not rename */ /* package */ int mNativeHandle = 0; @@ -209,6 +239,9 @@ public class SQLiteDatabase extends SQLiteClosable { /** The path for the database file */ private String mPath; + /** The anonymized path for the database file for logging purposes */ + private String mPathForLogs = null; // lazily populated + /** The flags passed to open/create */ private int mFlags; @@ -217,11 +250,41 @@ public class SQLiteDatabase extends SQLiteClosable { private WeakHashMap<SQLiteClosable, Object> mPrograms; - private final RuntimeException mLeakedException; + /** + * for each instance of this class, a 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 setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because + * most of the apps don't use "?" syntax in their sql, caching is not useful for them. + */ + /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = Maps.newHashMap(); + /** + * @hide + */ + public static final int MAX_SQL_CACHE_SIZE = 250; + private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance + 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; - // package visible, since callers will access directly to minimize overhead in the case - // that logging is not enabled. - /* package */ final boolean mLogStats; + /** the following 2 members maintain the time when a database is opened and closed */ + private String mTimeOpened = null; + private String mTimeClosed = null; + + /** Used to find out where this object was created in case it never got closed. */ + private Throwable mStackTrace = null; // System property that enables logging of slow queries. Specify the threshold in ms. private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; @@ -251,6 +314,9 @@ public class SQLiteDatabase extends SQLiteClosable { @Override protected void onAllReferencesReleased() { if (isOpen()) { + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeClosed = getTime(); + } dbclose(); } } @@ -281,15 +347,18 @@ public class SQLiteDatabase extends SQLiteClosable { private boolean mLockingEnabled = true; /* package */ void onCorruption() { + Log.e(TAG, "Removing corrupt database: " + mPath); + EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); try { // Close the database (if we can), which will cause subsequent operations to fail. close(); } finally { - Log.e(TAG, "Removing corrupt database: " + mPath); - EventLog.writeEvent(EVENT_DB_CORRUPT, mPath); // Delete the corrupt file. Don't re-create it now -- that would just confuse people // -- but the next time someone tries to open it, they can set it up from scratch. - new File(mPath).delete(); + if (!mPath.equalsIgnoreCase(":memory")) { + // delete is only for non-memory database files + new File(mPath).delete(); + } } } @@ -432,6 +501,9 @@ public class SQLiteDatabase extends SQLiteClosable { */ public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { lockForced(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } boolean ok = false; try { // If this thread already had the lock then get out @@ -476,6 +548,9 @@ public class SQLiteDatabase extends SQLiteClosable { * are committed and rolled back. */ public void endTransaction() { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } if (!mLock.isHeldByCurrentThread()) { throw new IllegalStateException("no transaction pending"); } @@ -502,7 +577,7 @@ public class SQLiteDatabase extends SQLiteClosable { } } if (mTransactionIsSuccessful) { - execSQL("COMMIT;"); + execSQL(COMMIT_SQL); } else { try { execSQL("ROLLBACK;"); @@ -536,6 +611,9 @@ public class SQLiteDatabase extends SQLiteClosable { * transaction is already marked as successful. */ public void setTransactionSuccessful() { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } if (!mLock.isHeldByCurrentThread()) { throw new IllegalStateException("no transaction pending"); } @@ -733,18 +811,30 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLiteException if the database cannot be opened */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) { - SQLiteDatabase db = null; + SQLiteDatabase sqliteDatabase = null; try { // Open the database. - return new SQLiteDatabase(path, factory, flags); + sqliteDatabase = new SQLiteDatabase(path, factory, flags); + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + sqliteDatabase.enableSqlTracing(path); + } + if (SQLiteDebug.DEBUG_SQL_TIME) { + sqliteDatabase.enableSqlProfiling(path); + } } catch (SQLiteDatabaseCorruptException e) { // Try to recover from this, if we can. // TODO: should we do this for other open failures? Log.e(TAG, "Deleting and re-creating corrupt database " + path, e); EventLog.writeEvent(EVENT_DB_CORRUPT, path); - new File(path).delete(); - return new SQLiteDatabase(path, factory, flags); + if (!path.equalsIgnoreCase(":memory")) { + // delete is only for non-memory database files + new File(path).delete(); + } + sqliteDatabase = new SQLiteDatabase(path, factory, flags); } + ActiveDatabases.getInstance().mActiveDatabases.add( + new WeakReference<SQLiteDatabase>(sqliteDatabase)); + return sqliteDatabase; } /** @@ -781,16 +871,29 @@ public class SQLiteDatabase extends SQLiteClosable { * Close the database. */ public void close() { + if (!isOpen()) { + return; // already closed + } lock(); try { closeClosable(); - releaseReference(); + // close this database instance - regardless of its reference count value + onAllReferencesReleased(); } finally { unlock(); } } private void closeClosable() { + /* deallocate all compiled sql statement objects from mCompiledQueries cache. + * this should be done before de-referencing all {@link SQLiteClosable} objects + * from this database object because calling + * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database + * 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(); + Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<SQLiteClosable, Object> entry = iter.next(); @@ -814,6 +917,9 @@ public class SQLiteDatabase extends SQLiteClosable { public int getVersion() { SQLiteStatement prog = null; lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } try { prog = new SQLiteStatement(this, "PRAGMA user_version;"); long version = prog.simpleQueryForLong(); @@ -841,6 +947,9 @@ public class SQLiteDatabase extends SQLiteClosable { public long getMaximumSize() { SQLiteStatement prog = null; lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } try { prog = new SQLiteStatement(this, "PRAGMA max_page_count;"); @@ -862,6 +971,9 @@ public class SQLiteDatabase extends SQLiteClosable { public long setMaximumSize(long numBytes) { SQLiteStatement prog = null; lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } try { long pageSize = getPageSize(); long numPages = numBytes / pageSize; @@ -887,6 +999,9 @@ public class SQLiteDatabase extends SQLiteClosable { public long getPageSize() { SQLiteStatement prog = null; lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } try { prog = new SQLiteStatement(this, "PRAGMA page_size;"); @@ -1023,6 +1138,9 @@ public class SQLiteDatabase extends SQLiteClosable { */ public SQLiteStatement compileStatement(String sql) throws SQLException { lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } try { return new SQLiteStatement(this, sql); } finally { @@ -1102,6 +1220,9 @@ public class SQLiteDatabase extends SQLiteClosable { boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } String sql = SQLiteQueryBuilder.buildQueryString( distinct, table, columns, selection, groupBy, having, orderBy, limit); @@ -1208,6 +1329,9 @@ public class SQLiteDatabase extends SQLiteClosable { public Cursor rawQueryWithFactory( CursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) { + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } long timeStart = 0; if (Config.LOGV || mSlowQueryThreshold != -1) { @@ -1225,9 +1349,9 @@ public class SQLiteDatabase extends SQLiteClosable { if (Config.LOGV || mSlowQueryThreshold != -1) { // Force query execution + int count = -1; if (cursor != null) { - cursor.moveToFirst(); - cursor.moveToPosition(-1); + count = cursor.getCount(); } long duration = System.currentTimeMillis() - timeStart; @@ -1237,7 +1361,7 @@ public class SQLiteDatabase extends SQLiteClosable { "query (" + duration + " ms): " + driver.toString() + ", args are " + (selectionArgs != null ? TextUtils.join(",", selectionArgs) - : "<null>")); + : "<null>") + ", count is " + count); } } } @@ -1283,7 +1407,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insert(String table, String nullColumnHack, ContentValues values) { try { - return insertWithOnConflict(table, nullColumnHack, values, null); + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } catch (SQLException e) { Log.e(TAG, "Error inserting " + values, e); return -1; @@ -1305,7 +1429,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws SQLException { - return insertWithOnConflict(table, nullColumnHack, values, null); + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } /** @@ -1322,7 +1446,7 @@ public class SQLiteDatabase extends SQLiteClosable { public long replace(String table, String nullColumnHack, ContentValues initialValues) { try { return insertWithOnConflict(table, nullColumnHack, initialValues, - ConflictAlgorithm.REPLACE); + CONFLICT_REPLACE); } catch (SQLException e) { Log.e(TAG, "Error inserting " + initialValues, e); return -1; @@ -1344,7 +1468,7 @@ public class SQLiteDatabase extends SQLiteClosable { public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) throws SQLException { return insertWithOnConflict(table, nullColumnHack, initialValues, - ConflictAlgorithm.REPLACE); + CONFLICT_REPLACE); } /** @@ -1357,12 +1481,14 @@ public class SQLiteDatabase extends SQLiteClosable { * @param initialValues this map contains the initial column values for the * row. The keys should be the column names and the values the * column values - * @param algorithm {@link ConflictAlgorithm} for insert conflict resolver - * @return the row ID of the newly inserted row, or -1 if an error occurred - * @hide + * @param conflictAlgorithm for insert conflict resolver + * @return the row ID of the newly inserted row + * OR the primary key of the existing row if the input param 'conflictAlgorithm' = + * {@link #CONFLICT_IGNORE} + * OR -1 if any error */ public long insertWithOnConflict(String table, String nullColumnHack, - ContentValues initialValues, ConflictAlgorithm algorithm) { + ContentValues initialValues, int conflictAlgorithm) { if (!isOpen()) { throw new IllegalStateException("database not open"); } @@ -1370,10 +1496,7 @@ public class SQLiteDatabase extends SQLiteClosable { // Measurements show most sql lengths <= 152 StringBuilder sql = new StringBuilder(152); sql.append("INSERT"); - if (algorithm != null) { - sql.append(" OR "); - sql.append(algorithm.value()); - } + sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(" INTO "); sql.append(table); // Measurements show most values lengths < 40 @@ -1457,10 +1580,10 @@ public class SQLiteDatabase extends SQLiteClosable { * whereClause. */ public int delete(String table, String whereClause, String[] whereArgs) { + lock(); if (!isOpen()) { throw new IllegalStateException("database not open"); } - lock(); SQLiteStatement statement = null; try { statement = compileStatement("DELETE FROM " + table @@ -1473,7 +1596,6 @@ public class SQLiteDatabase extends SQLiteClosable { } } statement.execute(); - statement.close(); return lastChangeCount(); } catch (SQLiteDatabaseCorruptException e) { onCorruption(); @@ -1497,7 +1619,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the number of rows affected */ public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { - return updateWithOnConflict(table, values, whereClause, whereArgs, null); + return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE); } /** @@ -1508,28 +1630,18 @@ public class SQLiteDatabase extends SQLiteClosable { * valid value that will be translated to NULL. * @param whereClause the optional WHERE clause to apply when updating. * Passing null will update all rows. - * @param algorithm {@link ConflictAlgorithm} for update conflict resolver + * @param conflictAlgorithm for update conflict resolver * @return the number of rows affected - * @hide */ public int updateWithOnConflict(String table, ContentValues values, - String whereClause, String[] whereArgs, ConflictAlgorithm algorithm) { - if (!isOpen()) { - throw new IllegalStateException("database not open"); - } - + String whereClause, String[] whereArgs, int conflictAlgorithm) { if (values == null || values.size() == 0) { throw new IllegalArgumentException("Empty values"); } StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); - if (algorithm != null) { - sql.append("OR "); - sql.append(algorithm.value()); - sql.append(" "); - } - + sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(table); sql.append(" SET "); @@ -1551,6 +1663,9 @@ public class SQLiteDatabase extends SQLiteClosable { } lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } SQLiteStatement statement = null; try { statement = compileStatement(sql.toString()); @@ -1575,7 +1690,6 @@ public class SQLiteDatabase extends SQLiteClosable { // Run the program and then cleanup statement.execute(); - statement.close(); int numChangedRows = lastChangeCount(); if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Updated " + numChangedRows + " using " + values + " and " + sql); @@ -1603,9 +1717,12 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql) throws SQLException { - boolean logStats = mLogStats; - long timeStart = logStats ? SystemClock.elapsedRealtime() : 0; + long timeStart = SystemClock.uptimeMillis(); lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } + logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX); try { native_execSQL(sql); } catch (SQLiteDatabaseCorruptException e) { @@ -1614,8 +1731,14 @@ public class SQLiteDatabase extends SQLiteClosable { } finally { unlock(); } - if (logStats) { - logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime()); + + // Log commit statements along with the most recently executed + // SQL statement for disambiguation. Note that instance + // equality to COMMIT_SQL is safe here. + if (sql == COMMIT_SQL) { + logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL); + } else { + logTimeStat(sql, timeStart, null); } } @@ -1632,10 +1755,11 @@ public class SQLiteDatabase extends SQLiteClosable { if (bindArgs == null) { throw new IllegalArgumentException("Empty bindArgs"); } - - boolean logStats = mLogStats; - long timeStart = logStats ? SystemClock.elapsedRealtime() : 0; + long timeStart = SystemClock.uptimeMillis(); lock(); + if (!isOpen()) { + throw new IllegalStateException("database not open"); + } SQLiteStatement statement = null; try { statement = compileStatement(sql); @@ -1655,21 +1779,14 @@ public class SQLiteDatabase extends SQLiteClosable { } unlock(); } - if (logStats) { - logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime()); - } + logTimeStat(sql, timeStart); } @Override protected void finalize() { if (isOpen()) { - if (mPrograms.isEmpty()) { - Log.e(TAG, "Leak found", mLeakedException); - } else { - IllegalStateException leakProgram = new IllegalStateException( - "mPrograms size " + mPrograms.size(), mLeakedException); - Log.e(TAG, "Leak found", leakProgram); - } + Log.e(TAG, "close() was never explicitly called on database '" + + mPath + "' ", mStackTrace); closeClosable(); onAllReferencesReleased(); } @@ -1689,23 +1806,30 @@ public class SQLiteDatabase extends SQLiteClosable { } mFlags = flags; mPath = path; - mLogStats = "1".equals(android.os.SystemProperties.get("db.logstats")); mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); - - mLeakedException = new IllegalStateException(path + - " SQLiteDatabase created and never closed"); + mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); mFactory = factory; dbopen(mPath, mFlags); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeOpened = getTime(); + } mPrograms = new WeakHashMap<SQLiteClosable,Object>(); try { setLocale(Locale.getDefault()); } catch (RuntimeException e) { Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); dbclose(); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeClosed = getTime(); + } throw e; } } + private String getTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); + } + /** * return whether the DB is opened as read only. * @return true if DB is opened as read only @@ -1734,8 +1858,83 @@ public class SQLiteDatabase extends SQLiteClosable { return mPath; } - /* package */ void logTimeStat(boolean read, long begin, long end) { - EventLog.writeEvent(EVENT_DB_OPERATION, mPath, read ? 0 : 1, end - begin); + /* package */ void logTimeStat(String sql, long beginMillis) { + logTimeStat(sql, beginMillis, null); + } + + /* package */ void logTimeStat(String sql, long beginMillis, String prefix) { + // Keep track of the last statement executed here, as this is + // the common funnel through which all methods of hitting + // libsqlite eventually flow. + mLastSqlStatement = sql; + + // Sample fast queries in proportion to the time taken. + // Quantize the % first, so the logged sampling probability + // exactly equals the actual sampling rate for this query. + + int samplePercent; + long durationMillis = SystemClock.uptimeMillis() - beginMillis; + if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) { + // The common case is locks being uncontended. Don't log those, + // even at 1%, which is our default below. + return; + } + if (sQueryLogTimeInMillis == 0) { + sQueryLogTimeInMillis = SystemProperties.getInt("db.db_operation.threshold_ms", 500); + } + if (durationMillis >= sQueryLogTimeInMillis) { + samplePercent = 100; + } else {; + samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1; + if (mRandom.nextInt(100) >= samplePercent) return; + } + + // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do + // it here so we avoid allocating in the common case. + if (prefix != null) { + sql = prefix + sql; + } + + if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH); + + // ActivityThread.currentPackageName() only returns non-null if the + // current thread is an application main thread. This parameter tells + // us whether an event loop is blocked, and if so, which app it is. + // + // Sadly, there's no fast way to determine app name if this is *not* a + // main thread, or when we are invoked via Binder (e.g. ContentProvider). + // Hopefully the full path to the database will be informative enough. + + String blockingPackage = ActivityThread.currentPackageName(); + if (blockingPackage == null) blockingPackage = ""; + + EventLog.writeEvent( + EVENT_DB_OPERATION, + getPathForLogs(), + sql, + durationMillis, + blockingPackage, + samplePercent); + } + + /** + * Removes email addresses from database filenames before they're + * logged to the EventLog where otherwise apps could potentially + * read them. + */ + private String getPathForLogs() { + if (mPathForLogs != null) { + return mPathForLogs; + } + if (mPath == null) { + return null; + } + if (mPath.indexOf('@') == -1) { + mPathForLogs = mPath; + } else { + mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY"); + } + return mPathForLogs; } /** @@ -1754,6 +1953,267 @@ 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) { + if (mMaxSqlCacheSize == 0) { + // for this database, there is no cache of compiled sql. + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); + } + return; + } + + SQLiteCompiledSql compiledSql = null; + synchronized(mCompiledQueries) { + // don't insert the new mapping if a mapping already exists + compiledSql = mCompiledQueries.get(sql); + if (compiledSql != null) { + return; + } + // add this <sql, compiledStatement> to the cache + if (mCompiledQueries.size() == mMaxSqlCacheSize) { + /* + * cache size of {@link #mMaxSqlCacheSize} is not enough for this app. + * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times + * chances are it is NOT using ? for bindargs - so caching is useless. + * TODO: either let the callers set max cchesize for their app, or intelligently + * figure out what should be cached for a given app. + */ + if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) { + Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + + getPath() + "; i.e., NO space for this sql statement in cache: " + + sql + ". Please change your sql statements to use '?' for " + + "bindargs, instead of using actual values"); + } + // don't add this entry to cache + } else { + // cache is NOT full. add this to cache. + mCompiledQueries.put(sql, compiledStatement); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + + mCompiledQueries.size() + "|" + sql); + } + } + } + return; + } + + + private 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) { + if (mMaxSqlCacheSize == 0) { + // for this database, there is no cache of compiled sql. + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|cache NOT found|" + getPath()); + } + return null; + } + 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 + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); + } + return compiledStatement; + } + + /** + * returns true if the given sql is cached in compiled-sql cache. + * @hide + */ + public boolean isInCompiledSqlCache(String sql) { + synchronized(mCompiledQueries) { + return mCompiledQueries.containsKey(sql); + } + } + + /** + * purges the given sql from the compiled-sql cache. + * @hide + */ + public void purgeFromCompiledSqlCache(String sql) { + synchronized(mCompiledQueries) { + mCompiledQueries.remove(sql); + } + } + + /** + * remove everything from the compiled sql cache + * @hide + */ + public void resetCompiledSqlCache() { + synchronized(mCompiledQueries) { + mCompiledQueries.clear(); + } + } + + /** + * return the current maxCacheSqlCacheSize + * @hide + */ + public synchronized int getMaxSqlCacheSize() { + return mMaxSqlCacheSize; + } + + /** + * set the max size of the compiled sql cache for this database after purging the cache. + * (size of the cache = number of compiled-sql-statements stored in the cache). + * + * max cache size can ONLY be increased from its current size (default = 0). + * if this method is called with smaller size than the current value of mMaxSqlCacheSize, + * then IllegalStateException is thrown + * + * synchronized because we don't want t threads to change cache size at the same time. + * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) + * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or + * < the value set with previous setMaxSqlCacheSize() call. + * + * @hide + */ + 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; + } + + static class ActiveDatabases { + private static final ActiveDatabases activeDatabases = new ActiveDatabases(); + private HashSet<WeakReference<SQLiteDatabase>> mActiveDatabases = + new HashSet<WeakReference<SQLiteDatabase>>(); + private ActiveDatabases() {} // disable instantiation of this class + static ActiveDatabases getInstance() {return activeDatabases;} + } + + /** + * this method is used to collect data about ALL open databases in the current process. + * bugreport is a user of this data. + */ + /* package */ static ArrayList<DbStats> getDbStats() { + ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>(); + for (WeakReference<SQLiteDatabase> w : ActiveDatabases.getInstance().mActiveDatabases) { + SQLiteDatabase db = w.get(); + if (db == null || !db.isOpen()) { + continue; + } + // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db + int lookasideUsed = db.native_getDbLookaside(); + + // get the lastnode of the dbname + String path = db.getPath(); + int indx = path.lastIndexOf("/"); + String lastnode = path.substring((indx != -1) ? ++indx : 0); + + // get list of attached dbs and for each db, get its size and pagesize + ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db); + if (attachedDbs == null) { + continue; + } + for (int i = 0; i < attachedDbs.size(); i++) { + Pair<String, String> p = attachedDbs.get(i); + long pageCount = getPragmaVal(db, p.first + ".page_count;"); + + // first entry in the attached db list is always the main database + // don't worry about prefixing the dbname with "main" + String dbName; + if (i == 0) { + dbName = lastnode; + } else { + // lookaside is only relevant for the main db + lookasideUsed = 0; + dbName = " (attached) " + p.first; + // if the attached db has a path, attach the lastnode from the path to above + if (p.second.trim().length() > 0) { + int idx = p.second.lastIndexOf("/"); + dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0); + } + } + if (pageCount > 0) { + dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(), + lookasideUsed)); + } + } + } + return dbStatsList; + } + + /** + * get the specified pragma value from sqlite for the specified database. + * only handles pragma's that return int/long. + * NO JAVA locks are held in this method. + * TODO: use this to do all pragma's in this class + */ + private static long getPragmaVal(SQLiteDatabase db, String pragma) { + if (!db.isOpen()) { + return 0; + } + SQLiteStatement prog = null; + try { + prog = new SQLiteStatement(db, "PRAGMA " + pragma); + long val = prog.simpleQueryForLong(); + return val; + } finally { + if (prog != null) prog.close(); + } + } + + /** + * returns list of full pathnames of all attached databases + * including the main database + * TODO: move this to {@link DatabaseUtils} + */ + private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) { + if (!dbObj.isOpen()) { + return null; + } + ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>(); + Cursor c = dbObj.rawQuery("pragma database_list;", null); + while (c.moveToNext()) { + attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2))); + } + c.close(); + return attachedDbs; + } + /** * Native call to open the database. * @@ -1762,6 +2222,23 @@ public class SQLiteDatabase extends SQLiteClosable { private native void dbopen(String path, int flags); /** + * Native call to setup tracing of all sql statements + * + * @param path the full path to the database + */ + private native void enableSqlTracing(String path); + + /** + * Native call to setup profiling of all sql statements. + * currently, sqlite's profiling = printing of execution-time + * (wall-clock time) of each of the sql statements, as they + * are executed. + * + * @param path the full path to the database + */ + private native void enableSqlProfiling(String path); + + /** * Native call to execute a raw SQL statement. {@link #lock} must be held * when calling this method. * @@ -1790,4 +2267,11 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the number of changes made in the last statement executed. */ /* package */ native int lastChangeCount(); + + /** + * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here + * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html + * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED + */ + private native int native_getDbLookaside(); } |