diff --git a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java index 973e996a17c36..762b16c23ef79 100644 --- a/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java +++ b/apct-tests/perftests/core/src/android/database/SQLiteDatabasePerfTest.java @@ -24,19 +24,17 @@ import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.perftests.utils.BenchmarkState; import android.perftests.utils.PerfStatusReporter; - import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; - +import java.io.File; +import java.util.Random; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.Random; - /** * Performance tests for typical CRUD operations and loading rows into the Cursor * @@ -58,12 +56,9 @@ public class SQLiteDatabasePerfTest { public void setUp() { mContext = InstrumentationRegistry.getTargetContext(); mContext.deleteDatabase(DB_NAME); - mDatabase = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null); - mDatabase.execSQL("CREATE TABLE T1 " - + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)"); - mDatabase.execSQL("CREATE TABLE T2 (" - + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER," - + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))"); + + createOrOpenTestDatabase( + SQLiteDatabase.JOURNAL_MODE_TRUNCATE, SQLiteDatabase.SYNC_MODE_FULL); } @After @@ -72,6 +67,25 @@ public class SQLiteDatabasePerfTest { mContext.deleteDatabase(DB_NAME); } + private void createOrOpenTestDatabase(String journalMode, String syncMode) { + SQLiteDatabase.OpenParams.Builder paramsBuilder = new SQLiteDatabase.OpenParams.Builder(); + File dbFile = mContext.getDatabasePath(DB_NAME); + if (journalMode != null) { + paramsBuilder.setJournalMode(journalMode); + } + if (syncMode != null) { + paramsBuilder.setSynchronousMode(syncMode); + } + paramsBuilder.addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY); + + mDatabase = SQLiteDatabase.openDatabase(dbFile, paramsBuilder.build()); + mDatabase.execSQL("CREATE TABLE T1 " + + "(_ID INTEGER PRIMARY KEY, COL_A INTEGER, COL_B VARCHAR(100), COL_C REAL)"); + mDatabase.execSQL("CREATE TABLE T2 (" + + "_ID INTEGER PRIMARY KEY, COL_A VARCHAR(100), T1_ID INTEGER," + + "FOREIGN KEY(T1_ID) REFERENCES T1 (_ID))"); + } + @Test public void testSelect() { insertT1TestDataSet(); @@ -192,22 +206,114 @@ public class SQLiteDatabasePerfTest { } } + /** + * This test measures the insertion of a single row into a database using DELETE journal and + * synchronous modes. + */ @Test public void testInsert() { insertT1TestDataSet(); + testInsertInternal("testInsert"); + } + + @Test + public void testInsertWithPersistFull() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL); + insertT1TestDataSet(); + testInsertInternal("testInsertWithPersistFull"); + } + + private void testInsertInternal(String traceTag) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); ContentValues cv = new ContentValues(); cv.put("_ID", DEFAULT_DATASET_SIZE); cv.put("COL_B", "NewValue"); cv.put("COL_C", 1.1); - String[] deleteArgs = new String[]{String.valueOf(DEFAULT_DATASET_SIZE)}; + String[] deleteArgs = new String[] {String.valueOf(DEFAULT_DATASET_SIZE)}; + while (state.keepRunning()) { + android.os.Trace.beginSection(traceTag); assertEquals(DEFAULT_DATASET_SIZE, mDatabase.insert("T1", null, cv)); state.pauseTiming(); assertEquals(1, mDatabase.delete("T1", "_ID=?", deleteArgs)); state.resumeTiming(); + android.os.Trace.endSection(); + } + } + + /** + * This test measures the insertion of a single row into a database using WAL journal mode and + * NORMAL synchronous mode. + */ + @Test + public void testInsertWithWalNormalMode() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL); + insertT1TestDataSet(); + + testInsertInternal("testInsertWithWalNormalMode"); + } + + /** + * This test measures the insertion of a single row into a database using WAL journal mode and + * FULL synchronous mode. The goal is to see the difference between NORMAL vs FULL sync modes. + */ + @Test + public void testInsertWithWalFullMode() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_FULL); + + insertT1TestDataSet(); + + testInsertInternal("testInsertWithWalFullMode"); + } + + /** + * This test measures the insertion of a multiple rows in a single transaction using WAL journal + * mode and NORMAL synchronous mode. + */ + @Test + public void testBulkInsertWithWalNormalMode() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL); + testBulkInsertInternal("testBulkInsertWithWalNormalMode"); + } + + @Test + public void testBulkInsertWithPersistFull() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL); + testBulkInsertInternal("testBulkInsertWithPersistFull"); + } + + /** + * This test measures the insertion of a multiple rows in a single transaction using TRUNCATE + * journal mode and FULL synchronous mode. + */ + @Test + public void testBulkInsert() { + testBulkInsertInternal("testBulkInsert"); + } + + private void testBulkInsertInternal(String traceTag) { + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + + String[] statements = new String[DEFAULT_DATASET_SIZE]; + for (int i = 0; i < DEFAULT_DATASET_SIZE; ++i) { + statements[i] = "INSERT INTO T1 VALUES (?,?,?,?)"; + } + + while (state.keepRunning()) { + android.os.Trace.beginSection(traceTag); + mDatabase.beginTransaction(); + for (int i = 0; i < DEFAULT_DATASET_SIZE; ++i) { + mDatabase.execSQL(statements[i], new Object[] {i, i, "T1Value" + i, i * 1.1}); + } + mDatabase.setTransactionSuccessful(); + mDatabase.endTransaction(); + android.os.Trace.endSection(); + + state.pauseTiming(); + mDatabase.execSQL("DELETE FROM T1"); + state.resumeTiming(); } } @@ -227,9 +333,30 @@ public class SQLiteDatabasePerfTest { } } + /** + * This test measures the update of a random row in a database. + */ + @Test + public void testUpdateWithWalNormalMode() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL); + insertT1TestDataSet(); + testUpdateInternal("testUpdateWithWalNormalMode"); + } + + @Test + public void testUpdateWithPersistFull() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_PERSIST, SQLiteDatabase.SYNC_MODE_FULL); + insertT1TestDataSet(); + testUpdateInternal("testUpdateWithPersistFull"); + } + @Test public void testUpdate() { insertT1TestDataSet(); + testUpdateInternal("testUpdate"); + } + + private void testUpdateInternal(String traceTag) { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); Random rnd = new Random(0); @@ -237,6 +364,7 @@ public class SQLiteDatabasePerfTest { ContentValues cv = new ContentValues(); String[] argArray = new String[1]; while (state.keepRunning()) { + android.os.Trace.beginSection(traceTag); int id = rnd.nextInt(DEFAULT_DATASET_SIZE); cv.put("COL_A", i); cv.put("COL_B", "UpdatedValue"); @@ -244,6 +372,109 @@ public class SQLiteDatabasePerfTest { argArray[0] = String.valueOf(id); assertEquals(1, mDatabase.update("T1", cv, "_ID=?", argArray)); i++; + android.os.Trace.endSection(); + } + } + + /** + * This test measures a multi-threaded read-write environment where there are 2 readers and + * 1 writer in the database using TRUNCATE journal mode and FULL syncMode. + */ + @Test + public void testMultithreadedReadWrite() { + insertT1TestDataSet(); + performMultithreadedReadWriteTest(); + } + + private void doReadLoop(int totalIterations) { + Random rnd = new Random(0); + int currentIteration = 0; + while (currentIteration < totalIterations) { + android.os.Trace.beginSection("ReadDatabase"); + int index = rnd.nextInt(DEFAULT_DATASET_SIZE); + try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 " + + "WHERE _ID=?", + new String[] {String.valueOf(index)})) { + cursor.moveToNext(); + cursor.getInt(0); + cursor.getInt(1); + cursor.getString(2); + cursor.getDouble(3); + } + ++currentIteration; + android.os.Trace.endSection(); + } + } + + private void doReadLoop(BenchmarkState state) { + Random rnd = new Random(0); + while (state.keepRunning()) { + android.os.Trace.beginSection("ReadDatabase"); + int index = rnd.nextInt(DEFAULT_DATASET_SIZE); + try (Cursor cursor = mDatabase.rawQuery("SELECT _ID, COL_A, COL_B, COL_C FROM T1 " + + "WHERE _ID=?", + new String[] {String.valueOf(index)})) { + cursor.moveToNext(); + cursor.getInt(0); + cursor.getInt(1); + cursor.getString(2); + cursor.getDouble(3); + } + android.os.Trace.endSection(); + } + } + + private void doUpdateLoop(int totalIterations) { + SQLiteDatabase db = mContext.openOrCreateDatabase(DB_NAME, Context.MODE_PRIVATE, null); + Random rnd = new Random(0); + int i = 0; + ContentValues cv = new ContentValues(); + String[] argArray = new String[1]; + + while (i < totalIterations) { + android.os.Trace.beginSection("UpdateDatabase"); + int id = rnd.nextInt(DEFAULT_DATASET_SIZE); + cv.put("COL_A", i); + cv.put("COL_B", "UpdatedValue"); + cv.put("COL_C", i); + argArray[0] = String.valueOf(id); + db.update("T1", cv, "_ID=?", argArray); + i++; + android.os.Trace.endSection(); + } + } + + /** + * This test measures a multi-threaded read-write environment where there are 2 readers and + * 1 writer in the database using WAL journal mode and NORMAL syncMode. + */ + @Test + public void testMultithreadedReadWriteWithWalNormal() { + recreateTestDatabase(SQLiteDatabase.JOURNAL_MODE_WAL, SQLiteDatabase.SYNC_MODE_NORMAL); + insertT1TestDataSet(); + + performMultithreadedReadWriteTest(); + } + + private void performMultithreadedReadWriteTest() { + int totalBGIterations = 10000; + // Writer - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations + Thread updateThread = new Thread(() -> { doUpdateLoop(totalBGIterations); }); + + // Reader 1 - Fixed iterations to avoid consuming cycles from mainloop benchmark iterations + Thread readerThread = new Thread(() -> { doReadLoop(totalBGIterations); }); + + updateThread.start(); + readerThread.start(); + + // Reader 2 + BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + doReadLoop(state); + + try { + updateThread.join(); + readerThread.join(); + } catch (Exception e) { } } @@ -270,5 +501,11 @@ public class SQLiteDatabasePerfTest { mDatabase.setTransactionSuccessful(); mDatabase.endTransaction(); } + + private void recreateTestDatabase(String journalMode, String syncMode) { + mDatabase.close(); + mContext.deleteDatabase(DB_NAME); + createOrOpenTestDatabase(journalMode, syncMode); + } } diff --git a/core/api/current.txt b/core/api/current.txt index 9d0ff4d88dad7..ab185aa54ccca 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -14376,11 +14376,21 @@ package android.database.sqlite { field public static final int CONFLICT_ROLLBACK = 1; // 0x1 field public static final int CREATE_IF_NECESSARY = 268435456; // 0x10000000 field public static final int ENABLE_WRITE_AHEAD_LOGGING = 536870912; // 0x20000000 + field public static final String JOURNAL_MODE_DELETE = "DELETE"; + field public static final String JOURNAL_MODE_MEMORY = "MEMORY"; + field public static final String JOURNAL_MODE_OFF = "OFF"; + field public static final String JOURNAL_MODE_PERSIST = "PERSIST"; + field public static final String JOURNAL_MODE_TRUNCATE = "TRUNCATE"; + field public static final String JOURNAL_MODE_WAL = "WAL"; field public static final int MAX_SQL_CACHE_SIZE = 100; // 0x64 field public static final int NO_LOCALIZED_COLLATORS = 16; // 0x10 field public static final int OPEN_READONLY = 1; // 0x1 field public static final int OPEN_READWRITE = 0; // 0x0 field public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; // 0xc350 + field public static final String SYNC_MODE_EXTRA = "EXTRA"; + field public static final String SYNC_MODE_FULL = "FULL"; + field public static final String SYNC_MODE_NORMAL = "NORMAL"; + field public static final String SYNC_MODE_OFF = "OFF"; } public static interface SQLiteDatabase.CursorFactory { diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 1db948aba7c69..ccb7cf19d0b14 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -143,7 +143,6 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { throw new AssertionError(); // Not possible, the native code won't return it. } mCloseGuard.open("close"); - recordNewWindow(Binder.getCallingPid(), mWindowPtr); } /** @@ -191,7 +190,6 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { mCloseGuard.close(); } if (mWindowPtr != 0) { - recordClosingOfWindow(mWindowPtr); nativeDispose(mWindowPtr); mWindowPtr = 0; } @@ -746,64 +744,6 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { dispose(); } - @UnsupportedAppUsage - private static final LongSparseArray sWindowToPidMap = new LongSparseArray(); - - private void recordNewWindow(int pid, long window) { - synchronized (sWindowToPidMap) { - sWindowToPidMap.put(window, pid); - if (Log.isLoggable(STATS_TAG, Log.VERBOSE)) { - Log.i(STATS_TAG, "Created a new Cursor. " + printStats()); - } - } - } - - private void recordClosingOfWindow(long window) { - synchronized (sWindowToPidMap) { - if (sWindowToPidMap.size() == 0) { - // this means we are not in the ContentProvider. - return; - } - sWindowToPidMap.delete(window); - } - } - - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - private String printStats() { - StringBuilder buff = new StringBuilder(); - int myPid = Process.myPid(); - int total = 0; - SparseIntArray pidCounts = new SparseIntArray(); - synchronized (sWindowToPidMap) { - int size = sWindowToPidMap.size(); - if (size == 0) { - // this means we are not in the ContentProvider. - return ""; - } - for (int indx = 0; indx < size; indx++) { - int pid = sWindowToPidMap.valueAt(indx); - int value = pidCounts.get(pid); - pidCounts.put(pid, ++value); - } - } - int numPids = pidCounts.size(); - for (int i = 0; i < numPids;i++) { - buff.append(" (# cursors opened by "); - int pid = pidCounts.keyAt(i); - if (pid == myPid) { - buff.append("this proc="); - } else { - buff.append("pid ").append(pid).append('='); - } - int num = pidCounts.get(pid); - buff.append(num).append(')'); - total += num; - } - // limit the returned string size to 1000 - String s = (buff.length() > 980) ? buff.substring(0, 980) : buff.toString(); - return "# Open Cursors=" + total + s; - } - private static int getCursorWindowSize() { if (sCursorWindowSize < 0) { // The cursor window size. resource xml file specifies the value in kB. diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 7e61b48b25470..328858b260acb 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -26,14 +26,13 @@ import android.os.OperationCanceledException; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.os.Trace; +import android.text.TextUtils; import android.util.Log; import android.util.LruCache; import android.util.Pair; import android.util.Printer; - import dalvik.system.BlockGuard; import dalvik.system.CloseGuard; - import java.io.File; import java.io.IOException; import java.nio.file.FileSystems; @@ -177,7 +176,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mConfiguration = new SQLiteDatabaseConfiguration(configuration); mConnectionId = connectionId; mIsPrimaryConnection = primaryConnection; - mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0; + mIsReadOnlyConnection = mConfiguration.isReadOnlyDatabase(); mPreparedStatementCache = new PreparedStatementCache( mConfiguration.maxSqlCacheSize); mCloseGuard.open("close"); @@ -266,7 +265,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } setPageSize(); setForeignKeyModeFromConfiguration(); - setWalModeFromConfiguration(); + setJournalFromConfiguration(); + setSyncModeFromConfiguration(); setJournalSizeLimit(); setAutoCheckpointInterval(); setLocaleFromConfiguration(); @@ -334,30 +334,19 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } } - private void setWalModeFromConfiguration() { - if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { - final boolean walEnabled = - (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; - // Use compatibility WAL unless an app explicitly set journal/synchronous mode - // or DISABLE_COMPATIBILITY_WAL flag is set - final boolean isCompatibilityWalEnabled = - mConfiguration.isLegacyCompatibilityWalEnabled(); - if (walEnabled || isCompatibilityWalEnabled) { - setJournalMode("WAL"); - if (mConfiguration.syncMode != null) { - setSyncMode(mConfiguration.syncMode); - } else if (isCompatibilityWalEnabled) { - setSyncMode(SQLiteCompatibilityWalFlags.getWALSyncMode()); - } else { - setSyncMode(SQLiteGlobal.getWALSyncMode()); - } - maybeTruncateWalFile(); - } else { - setJournalMode(mConfiguration.journalMode == null - ? SQLiteGlobal.getDefaultJournalMode() : mConfiguration.journalMode); - setSyncMode(mConfiguration.syncMode == null - ? SQLiteGlobal.getDefaultSyncMode() : mConfiguration.syncMode); - } + private void setJournalFromConfiguration() { + if (!mIsReadOnlyConnection) { + setJournalMode(mConfiguration.resolveJournalMode()); + maybeTruncateWalFile(); + } else { + // No need to truncate for read only databases. + mConfiguration.shouldTruncateWalFile = false; + } + } + + private void setSyncModeFromConfiguration() { + if (!mIsReadOnlyConnection) { + setSyncMode(mConfiguration.resolveSyncMode()); } } @@ -366,6 +355,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen * PRAGMA wal_checkpoint. */ private void maybeTruncateWalFile() { + if (!mConfiguration.shouldTruncateWalFile) { + return; + } + final long threshold = SQLiteGlobal.getWALTruncateSize(); if (DEBUG) { Log.d(TAG, "Truncate threshold=" + threshold); @@ -390,12 +383,17 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen + threshold + "; truncating"); try { executeForString("PRAGMA wal_checkpoint(TRUNCATE)", null, null); + mConfiguration.shouldTruncateWalFile = false; } catch (SQLiteException e) { Log.w(TAG, "Failed to truncate the -wal file", e); } } - private void setSyncMode(String newValue) { + private void setSyncMode(@SQLiteDatabase.SyncMode String newValue) { + if (TextUtils.isEmpty(newValue)) { + // No change to the sync mode is intended + return; + } String value = executeForString("PRAGMA synchronous", null, null); if (!canonicalizeSyncMode(value).equalsIgnoreCase( canonicalizeSyncMode(newValue))) { @@ -403,16 +401,21 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } } - private static String canonicalizeSyncMode(String value) { + private static @SQLiteDatabase.SyncMode String canonicalizeSyncMode(String value) { switch (value) { - case "0": return "OFF"; - case "1": return "NORMAL"; - case "2": return "FULL"; + case "0": return SQLiteDatabase.SYNC_MODE_OFF; + case "1": return SQLiteDatabase.SYNC_MODE_NORMAL; + case "2": return SQLiteDatabase.SYNC_MODE_FULL; + case "3": return SQLiteDatabase.SYNC_MODE_EXTRA; } return value; } - private void setJournalMode(String newValue) { + private void setJournalMode(@SQLiteDatabase.JournalMode String newValue) { + if (TextUtils.isEmpty(newValue)) { + // No change to the journal mode is intended + return; + } String value = executeForString("PRAGMA journal_mode", null, null); if (!value.equalsIgnoreCase(newValue)) { try { @@ -565,9 +568,6 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen // Remember what changed. boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled != mConfiguration.foreignKeyConstraintsEnabled; - boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) - & (SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING - | SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL)) != 0; boolean localeChanged = !configuration.locale.equals(mConfiguration.locale); boolean customScalarFunctionsChanged = !configuration.customScalarFunctions .equals(mConfiguration.customScalarFunctions); @@ -586,9 +586,19 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen if (foreignKeyModeChanged) { setForeignKeyModeFromConfiguration(); } - if (walModeChanged) { - setWalModeFromConfiguration(); + + boolean journalModeChanged = !configuration.resolveJournalMode().equalsIgnoreCase( + mConfiguration.resolveJournalMode()); + if (journalModeChanged) { + setJournalFromConfiguration(); } + + boolean syncModeChanged = + !configuration.resolveSyncMode().equalsIgnoreCase(mConfiguration.resolveSyncMode()); + if (syncModeChanged) { + setSyncModeFromConfiguration(); + } + if (localeChanged) { setLocaleFromConfiguration(); } diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index 852f8f2a4204a..d3ad6bb27b3cf 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -106,7 +106,11 @@ public final class SQLiteConnectionPool implements Closeable { @GuardedBy("mLock") private IdleConnectionHandler mIdleConnectionHandler; - private final AtomicLong mTotalExecutionTimeCounter = new AtomicLong(0); + // whole execution time for this connection in milliseconds. + private final AtomicLong mTotalStatementsTime = new AtomicLong(0); + + // total statements executed by this connection + private final AtomicLong mTotalStatementsCount = new AtomicLong(0); // Describes what should happen to an acquired connection when it is returned to the pool. enum AcquiredConnectionStatus { @@ -286,8 +290,11 @@ public final class SQLiteConnectionPool implements Closeable { synchronized (mLock) { throwIfClosedLocked(); - boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) - & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; + boolean isWalCurrentMode = mConfiguration.resolveJournalMode().equalsIgnoreCase( + SQLiteDatabase.JOURNAL_MODE_WAL); + boolean isWalNewMode = configuration.resolveJournalMode().equalsIgnoreCase( + SQLiteDatabase.JOURNAL_MODE_WAL); + boolean walModeChanged = isWalCurrentMode ^ isWalNewMode; if (walModeChanged) { // WAL mode can only be changed if there are no acquired connections // because we need to close all but the primary connection first. @@ -536,7 +543,8 @@ public final class SQLiteConnectionPool implements Closeable { } void onStatementExecuted(long executionTimeMs) { - mTotalExecutionTimeCounter.addAndGet(executionTimeMs); + mTotalStatementsTime.addAndGet(executionTimeMs); + mTotalStatementsCount.incrementAndGet(); } // Can't throw. @@ -1037,8 +1045,7 @@ public final class SQLiteConnectionPool implements Closeable { } private void setMaxConnectionPoolSizeLocked() { - if (!mConfiguration.isInMemoryDb() - && (mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { + if (mConfiguration.resolveJournalMode().equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_WAL)) { mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize(); } else { // We don't actually need to always restrict the connection pool size to 1 @@ -1117,11 +1124,18 @@ public final class SQLiteConnectionPool implements Closeable { printer.println("Connection pool for " + mConfiguration.path + ":"); printer.println(" Open: " + mIsOpen); printer.println(" Max connections: " + mMaxConnectionPoolSize); - printer.println(" Total execution time: " + mTotalExecutionTimeCounter); + printer.println(" Total execution time (ms): " + mTotalStatementsTime); + printer.println(" Total statements executed: " + mTotalStatementsCount); + if (mTotalStatementsCount.get() > 0) { + // Avoid division by 0 by filtering out logs where there are no statements executed. + printer.println(" Average time per statement (ms): " + + mTotalStatementsTime.get() / mTotalStatementsCount.get()); + } printer.println(" Configuration: openFlags=" + mConfiguration.openFlags + ", isLegacyCompatibilityWalEnabled=" + isCompatibilityWalEnabled - + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.journalMode) - + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.syncMode)); + + ", journalMode=" + TextUtils.emptyIfNull(mConfiguration.resolveJournalMode()) + + ", syncMode=" + TextUtils.emptyIfNull(mConfiguration.resolveSyncMode())); + printer.println(" IsReadOnlyDatabase=" + mConfiguration.isReadOnlyDatabase()); if (isCompatibilityWalEnabled) { printer.println(" Compatibility WAL enabled: wal_syncmode=" @@ -1182,6 +1196,14 @@ public final class SQLiteConnectionPool implements Closeable { } } + public long getTotalStatementsTime() { + return mTotalStatementsTime.get(); + } + + public long getTotalStatementsCount() { + return mTotalStatementsCount.get(); + } + @Override public String toString() { return "SQLiteConnectionPool: " + mConfiguration.path; diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 9684ece5d9c1f..0d0615a28af3d 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -20,6 +20,8 @@ import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringDef; +import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityThread; import android.compat.annotation.UnsupportedAppUsage; @@ -42,11 +44,8 @@ import android.util.EventLog; import android.util.Log; import android.util.Pair; import android.util.Printer; - import com.android.internal.util.Preconditions; - import dalvik.system.CloseGuard; - import java.io.File; import java.io.FileFilter; import java.io.IOException; @@ -290,15 +289,182 @@ public final class SQLiteDatabase extends SQLiteClosable { */ public static final int MAX_SQL_CACHE_SIZE = 100; - private SQLiteDatabase(final String path, final int openFlags, - CursorFactory cursorFactory, DatabaseErrorHandler errorHandler, + /** + * @hide + */ + @StringDef(prefix = {"JOURNAL_MODE_"}, + value = + { + JOURNAL_MODE_WAL, + JOURNAL_MODE_PERSIST, + JOURNAL_MODE_TRUNCATE, + JOURNAL_MODE_MEMORY, + JOURNAL_MODE_DELETE, + JOURNAL_MODE_OFF, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface JournalMode {} + + /** + * The {@code WAL} journaling mode uses a write-ahead log instead of a rollback journal to + * implement transactions. The WAL journaling mode is persistent; after being set it stays + * in effect across multiple database connections and after closing and reopening the database. + * + * Performance Considerations: + * This mode is recommended when the goal is to improve write performance or parallel read/write + * performance. However, it is important to note that WAL introduces checkpoints which commit + * all transactions that have not been synced to the database thus to maximize read performance + * and lower checkpointing cost a small journal size is recommended. However, other modes such + * as {@code DELETE} will not perform checkpoints, so it is a trade off that needs to be + * considered as part of the decision of which journal mode to use. + * + *

See here for more + * details.

+ */ + public static final String JOURNAL_MODE_WAL = "WAL"; + + /** + * The {@code PERSIST} journaling mode prevents the rollback journal from being deleted at the + * end of each transaction. Instead, the header of the journal is overwritten with zeros. + * This will prevent other database connections from rolling the journal back. + * + * This mode is useful as an optimization on platforms where deleting or truncating a file is + * much more expensive than overwriting the first block of a file with zeros. + * + *

See here for more + * details.

+ */ + public static final String JOURNAL_MODE_PERSIST = "PERSIST"; + + /** + * The {@code TRUNCATE} journaling mode commits transactions by truncating the rollback journal + * to zero-length instead of deleting it. On many systems, truncating a file is much faster than + * deleting the file since the containing directory does not need to be changed. + * + *

See here for more + * details.

+ */ + public static final String JOURNAL_MODE_TRUNCATE = "TRUNCATE"; + + /** + * The {@code MEMORY} journaling mode stores the rollback journal in volatile RAM. + * This saves disk I/O but at the expense of database safety and integrity. If the application + * using SQLite crashes in the middle of a transaction when the MEMORY journaling mode is set, + * then the database file will very likely go corrupt. + * + *

See here for more + * details.

+ */ + public static final String JOURNAL_MODE_MEMORY = "MEMORY"; + + /** + * The {@code DELETE} journaling mode is the normal behavior. In the DELETE mode, the rollback + * journal is deleted at the conclusion of each transaction. + * + *

See here for more + * details.

+ */ + public static final String JOURNAL_MODE_DELETE = "DELETE"; + + /** + * The {@code OFF} journaling mode disables the rollback journal completely. No rollback journal + * is ever created and hence there is never a rollback journal to delete. The OFF journaling + * mode disables the atomic commit and rollback capabilities of SQLite. The ROLLBACK command + * behaves in an undefined way thus applications must avoid using the ROLLBACK command. + * If the application crashes in the middle of a transaction, then the database file will very + * likely go corrupt. + * + *

See here for more + * details.

+ */ + public static final String JOURNAL_MODE_OFF = "OFF"; + + /** + * @hide + */ + @StringDef(prefix = {"SYNC_MODE_"}, + value = + { + SYNC_MODE_EXTRA, + SYNC_MODE_FULL, + SYNC_MODE_NORMAL, + SYNC_MODE_OFF, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SyncMode {} + + /** + * The {@code EXTRA} sync mode is like {@code FULL} sync mode with the addition that the + * directory containing a rollback journal is synced after that journal is unlinked to commit a + * transaction in {@code DELETE} journal mode. + * + * {@code EXTRA} provides additional durability if the commit is followed closely by a + * power loss. + * + *

See here for more + * details.

+ */ + @SuppressLint("IntentName") public static final String SYNC_MODE_EXTRA = "EXTRA"; + + /** + * In {@code FULL} sync mode the SQLite database engine will use the xSync method of the VFS + * to ensure that all content is safely written to the disk surface prior to continuing. + * This ensures that an operating system crash or power failure will not corrupt the database. + * {@code FULL} is very safe, but it is also slower. + * + * {@code FULL} is the most commonly used synchronous setting when not in WAL mode. + * + *

See here for more + * details.

+ */ + public static final String SYNC_MODE_FULL = "FULL"; + + /** + * The {@code NORMAL} sync mode, the SQLite database engine will still sync at the most critical + * moments, but less often than in {@code FULL} mode. There is a very small chance that a + * power failure at the wrong time could corrupt the database in {@code DELETE} journal mode on + * an older filesystem. + * + * {@code WAL} journal mode is safe from corruption with {@code NORMAL} sync mode, and probably + * {@code DELETE} sync mode is safe too on modern filesystems. WAL mode is always consistent + * with {@code NORMAL} sync mode, but WAL mode does lose durability. A transaction committed in + * WAL mode with {@code NORMAL} might roll back following a power loss or system crash. + * Transactions are durable across application crashes regardless of the synchronous setting + * or journal mode. + * + * The {@code NORMAL} sync mode is a good choice for most applications running in WAL mode. + * + *

Caveat: Even though this sync mode is safe Be careful when using {@code NORMAL} sync mode + * when dealing with data dependencies between multiple databases, unless those databases use + * the same durability or are somehow synced, there could be corruption.

+ * + *

See here for more + * details.

+ */ + public static final String SYNC_MODE_NORMAL = "NORMAL"; + + /** + * In {@code OFF} sync mode SQLite continues without syncing as soon as it has handed data off + * to the operating system. If the application running SQLite crashes, the data will be safe, + * but the database might become corrupted if the operating system crashes or the computer loses + * power before that data has been written to the disk surface. On the other hand, commits can + * be orders of magnitude faster with synchronous {@code OFF}. + * + *

See here for more + * details.

+ */ + public static final String SYNC_MODE_OFF = "OFF"; + + private SQLiteDatabase(@Nullable final String path, @Nullable final int openFlags, + @Nullable CursorFactory cursorFactory, @Nullable DatabaseErrorHandler errorHandler, int lookasideSlotSize, int lookasideSlotCount, long idleConnectionTimeoutMs, - String journalMode, String syncMode) { + @Nullable String journalMode, @Nullable String syncMode) { mCursorFactory = cursorFactory; mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); mConfigurationLocked.lookasideSlotSize = lookasideSlotSize; mConfigurationLocked.lookasideSlotCount = lookasideSlotCount; + // Disable lookaside allocator on low-RAM devices if (ActivityManager.isLowRamDeviceStatic()) { mConfigurationLocked.lookasideSlotCount = 0; @@ -316,11 +482,11 @@ public final class SQLiteDatabase extends SQLiteClosable { } } mConfigurationLocked.idleConnectionTimeoutMs = effectiveTimeoutMs; - mConfigurationLocked.journalMode = journalMode; - mConfigurationLocked.syncMode = syncMode; if (SQLiteCompatibilityWalFlags.isLegacyCompatibilityWalEnabled()) { mConfigurationLocked.openFlags |= ENABLE_LEGACY_COMPATIBILITY_WAL; } + mConfigurationLocked.journalMode = journalMode; + mConfigurationLocked.syncMode = syncMode; } @Override @@ -2191,7 +2357,8 @@ public final class SQLiteDatabase extends SQLiteClosable { synchronized (mLock) { throwIfNotOpenLocked(); - if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0) { + if (mConfigurationLocked.resolveJournalMode().equalsIgnoreCase( + SQLiteDatabase.JOURNAL_MODE_WAL)) { return true; } @@ -2241,11 +2408,9 @@ public final class SQLiteDatabase extends SQLiteClosable { throwIfNotOpenLocked(); final int oldFlags = mConfigurationLocked.openFlags; - final boolean walEnabled = (oldFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0; - final boolean compatibilityWalEnabled = - (oldFlags & ENABLE_LEGACY_COMPATIBILITY_WAL) != 0; // WAL was never enabled for this database, so there's nothing left to do. - if (!walEnabled && !compatibilityWalEnabled) { + if (!mConfigurationLocked.resolveJournalMode().equalsIgnoreCase( + SQLiteDatabase.JOURNAL_MODE_WAL)) { return; } @@ -2275,7 +2440,8 @@ public final class SQLiteDatabase extends SQLiteClosable { synchronized (mLock) { throwIfNotOpenLocked(); - return (mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0; + return mConfigurationLocked.resolveJournalMode().equalsIgnoreCase( + SQLiteDatabase.JOURNAL_MODE_WAL); } } @@ -2309,6 +2475,20 @@ public final class SQLiteDatabase extends SQLiteClosable { return databases; } + private static ArrayList getActiveDatabasePools() { + ArrayList connectionPools = new ArrayList(); + synchronized (sActiveDatabases) { + for (SQLiteDatabase db : sActiveDatabases.keySet()) { + synchronized (db.mLock) { + if (db.mConnectionPoolLocked != null) { + connectionPools.add(db.mConnectionPoolLocked); + } + } + } + } + return connectionPools; + } + /** * Dump detailed information about all open databases in the current process. * Used by bug report. @@ -2317,8 +2497,45 @@ public final class SQLiteDatabase extends SQLiteClosable { // Use this ArraySet to collect file paths. final ArraySet directories = new ArraySet<>(); - for (SQLiteDatabase db : getActiveDatabases()) { - db.dump(printer, verbose, isSystem, directories); + // Accounting across all databases + long totalStatementsTimeInMs = 0; + long totalStatementsCount = 0; + + ArrayList activeConnectionPools = getActiveDatabasePools(); + + activeConnectionPools.sort( + (a, b) -> Long.compare(b.getTotalStatementsCount(), a.getTotalStatementsCount())); + for (SQLiteConnectionPool dbPool : activeConnectionPools) { + dbPool.dump(printer, verbose, directories); + totalStatementsTimeInMs += dbPool.getTotalStatementsTime(); + totalStatementsCount += dbPool.getTotalStatementsCount(); + } + + if (totalStatementsCount > 0) { + // Only print when there is information available + + // Sorted statements per database + printer.println("Statements Executed per Database"); + for (SQLiteConnectionPool dbPool : activeConnectionPools) { + printer.println( + " " + dbPool.getPath() + " : " + dbPool.getTotalStatementsCount()); + } + printer.println(""); + printer.println( + "Total Statements Executed for all Active Databases: " + totalStatementsCount); + + // Sorted execution time per database + activeConnectionPools.sort( + (a, b) -> Long.compare(b.getTotalStatementsTime(), a.getTotalStatementsTime())); + printer.println(""); + printer.println(""); + printer.println("Statement Time per Database (ms)"); + for (SQLiteConnectionPool dbPool : activeConnectionPools) { + printer.println( + " " + dbPool.getPath() + " : " + dbPool.getTotalStatementsTime()); + } + printer.println("Total Statements Time for all Active Databases (ms): " + + totalStatementsTimeInMs); } // Dump DB files in the directories. @@ -2331,15 +2548,6 @@ public final class SQLiteDatabase extends SQLiteClosable { } } - private void dump(Printer printer, boolean verbose, boolean isSystem, ArraySet directories) { - synchronized (mLock) { - if (mConnectionPoolLocked != null) { - printer.println(""); - mConnectionPoolLocked.dump(printer, verbose, directories); - } - } - } - private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) { pw.println(""); pw.println("Database files in " + dir.getAbsolutePath() + ":"); @@ -2598,9 +2806,7 @@ public final class SQLiteDatabase extends SQLiteClosable { /** * Returns journal mode. - * This journal mode will only be used if {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} - * flag is not set, otherwise a platform will use "WAL" journal mode. - * @see Builder#setJournalMode(String) + * set via {@link Builder#setJournalMode(String)}. */ @Nullable public String getJournalMode() { @@ -2799,25 +3005,28 @@ public final class SQLiteDatabase extends SQLiteClosable { return this; } - /** * Sets journal mode - * to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} flag is not set. + * to use. + * + *

Note: If journal mode is not set, the platform will use a manufactured-specified + * default which can vary across devices. */ @NonNull - public Builder setJournalMode(@NonNull String journalMode) { + public Builder setJournalMode(@JournalMode @NonNull String journalMode) { Objects.requireNonNull(journalMode); mJournalMode = journalMode; return this; } - /**w + /** * Sets synchronous mode - * . - * @return + * + *

Note: If sync mode is not set, the platform will use a manufactured-specified + * default which can vary across devices. */ @NonNull - public Builder setSynchronousMode(@NonNull String syncMode) { + public Builder setSynchronousMode(@SyncMode @NonNull String syncMode) { Objects.requireNonNull(syncMode); mSyncMode = syncMode; return this; diff --git a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java index 21c21c902fed4..8305843140399 100644 --- a/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java +++ b/core/java/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -17,9 +17,9 @@ package android.database.sqlite; import android.compat.annotation.UnsupportedAppUsage; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Pair; - import java.util.ArrayList; import java.util.Locale; import java.util.function.BinaryOperator; @@ -132,14 +132,16 @@ public final class SQLiteDatabaseConfiguration { * Journal mode to use when {@link SQLiteDatabase#ENABLE_WRITE_AHEAD_LOGGING} is not set. *

Default is returned by {@link SQLiteGlobal#getDefaultJournalMode()} */ - public String journalMode; + public @SQLiteDatabase.JournalMode String journalMode; /** * Synchronous mode to use. *

Default is returned by {@link SQLiteGlobal#getDefaultSyncMode()} * or {@link SQLiteGlobal#getWALSyncMode()} depending on journal mode */ - public String syncMode; + public @SQLiteDatabase.SyncMode String syncMode; + + public boolean shouldTruncateWalFile; /** * Creates a database configuration with the required parameters for opening a @@ -217,6 +219,10 @@ public final class SQLiteDatabaseConfiguration { return path.equalsIgnoreCase(MEMORY_DB_PATH); } + public boolean isReadOnlyDatabase() { + return (openFlags & SQLiteDatabase.OPEN_READONLY) != 0; + } + boolean isLegacyCompatibilityWalEnabled() { return journalMode == null && syncMode == null && (openFlags & SQLiteDatabase.ENABLE_LEGACY_COMPATIBILITY_WAL) != 0; @@ -232,4 +238,81 @@ public final class SQLiteDatabaseConfiguration { boolean isLookasideConfigSet() { return lookasideSlotCount >= 0 && lookasideSlotSize >= 0; } + + /** + * Resolves the journal mode that should be used when opening a connection to the database. + * + * Note: assumes openFlags have already been set. + * + * @return Resolved journal mode that should be used for this database connection or an empty + * string if no journal mode should be set. + */ + public @SQLiteDatabase.JournalMode String resolveJournalMode() { + if (isReadOnlyDatabase()) { + // No need to specify a journal mode when only reading. + return ""; + } + + if (isInMemoryDb()) { + if (journalMode != null + && journalMode.equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_OFF)) { + return SQLiteDatabase.JOURNAL_MODE_OFF; + } + return SQLiteDatabase.JOURNAL_MODE_MEMORY; + } + + shouldTruncateWalFile = false; + + if (isWalEnabledInternal()) { + shouldTruncateWalFile = true; + return SQLiteDatabase.JOURNAL_MODE_WAL; + } else { + // WAL is not explicitly set so use requested journal mode or platform default + return this.journalMode != null ? this.journalMode + : SQLiteGlobal.getDefaultJournalMode(); + } + } + + /** + * Resolves the sync mode that should be used when opening a connection to the database. + * + * Note: assumes openFlags have already been set. + * @return Resolved journal mode that should be used for this database connection or null + * if no journal mode should be set. + */ + public @SQLiteDatabase.SyncMode String resolveSyncMode() { + if (isReadOnlyDatabase()) { + // No sync mode will be used since database will be only used for reading. + return ""; + } + + if (isInMemoryDb()) { + // No sync mode will be used since database will be in volatile memory + return ""; + } + + if (!TextUtils.isEmpty(syncMode)) { + return syncMode; + } + + if (isWalEnabledInternal()) { + if (isLegacyCompatibilityWalEnabled()) { + return SQLiteCompatibilityWalFlags.getWALSyncMode(); + } else { + return SQLiteGlobal.getDefaultSyncMode(); + } + } else { + return SQLiteGlobal.getDefaultSyncMode(); + } + } + + private boolean isWalEnabledInternal() { + final boolean walEnabled = (openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; + // Use compatibility WAL unless an app explicitly set journal/synchronous mode + // or DISABLE_COMPATIBILITY_WAL flag is set + final boolean isCompatibilityWalEnabled = isLegacyCompatibilityWalEnabled(); + return walEnabled || isCompatibilityWalEnabled + || (journalMode != null + && journalMode.equalsIgnoreCase(SQLiteDatabase.JOURNAL_MODE_WAL)); + } } diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index 1afa0f8d70902..b84a8d24d42a3 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -70,7 +70,8 @@ public final class SQLiteDebug { /** * True to enable database performance testing instrumentation. */ - public static final boolean DEBUG_LOG_SLOW_QUERIES = Build.IS_DEBUGGABLE; + public static final boolean DEBUG_LOG_SLOW_QUERIES = + Log.isLoggable("SQLiteSlowQueries", Log.VERBOSE); private static final String SLOW_QUERY_THRESHOLD_PROP = "db.log.slow_query_threshold"; diff --git a/core/java/android/database/sqlite/SQLiteGlobal.java b/core/java/android/database/sqlite/SQLiteGlobal.java index 5e2875d02d90e..28964fdd5bf61 100644 --- a/core/java/android/database/sqlite/SQLiteGlobal.java +++ b/core/java/android/database/sqlite/SQLiteGlobal.java @@ -84,7 +84,7 @@ public final class SQLiteGlobal { /** * Gets the default journal mode when WAL is not in use. */ - public static String getDefaultJournalMode() { + public static @SQLiteDatabase.JournalMode String getDefaultJournalMode() { return SystemProperties.get("debug.sqlite.journalmode", Resources.getSystem().getString( com.android.internal.R.string.db_default_journal_mode)); @@ -102,7 +102,7 @@ public final class SQLiteGlobal { /** * Gets the default database synchronization mode when WAL is not in use. */ - public static String getDefaultSyncMode() { + public static @SQLiteDatabase.SyncMode String getDefaultSyncMode() { // Use the FULL synchronous mode for system processes by default. String defaultMode = sDefaultSyncMode; if (defaultMode != null) { @@ -116,7 +116,7 @@ public final class SQLiteGlobal { /** * Gets the database synchronization mode when in WAL mode. */ - public static String getWALSyncMode() { + public static @SQLiteDatabase.SyncMode String getWALSyncMode() { // Use the FULL synchronous mode for system processes by default. String defaultMode = sDefaultSyncMode; if (defaultMode != null) {