Add sql statement cache miss info and test

This patch adds a test for checking that statement cache
works and it also improves dumpsys diagnostics to include
per database stats as previously it only showed per connection
stats which could have data loss if a connection was dropped
for any reason and not put back in the pool and it also
provides the aggregated view.

Test: dumpsys meminfo
Test: atest SQLiteDatabasePerfTest#testSelectCacheMissRate
Bug: 228498581

Change-Id: Ia49264e8f0e6db83234c2ba5943fd9e25fd5d8b2
This commit is contained in:
Edgar Arriaga
2022-04-12 18:29:53 -07:00
committed by Edgar Arriaga García
parent fa415fc4c8
commit 506e3f17c5
8 changed files with 159 additions and 21 deletions

View File

@@ -24,10 +24,12 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import java.io.File;
import java.util.ArrayList;
import java.util.Random;
import org.junit.After;
import org.junit.Before;
@@ -106,6 +108,51 @@ public class SQLiteDatabasePerfTest {
}
}
@Test
public void testSelectCacheMissRate() {
BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
insertT1TestDataSet();
ArrayList<String> queryPool = new ArrayList<>();
queryPool.add("SELECT _ID, COL_A, COL_B, COL_C FROM T1 WHERE _ID=?");
queryPool.add("SELECT _ID FROM T1 WHERE _ID=?");
queryPool.add("SELECT COL_A FROM T1 WHERE _ID=?");
queryPool.add("SELECT COL_B FROM T1 WHERE _ID=?");
queryPool.add("SELECT COL_C FROM T1 WHERE _ID=?");
queryPool.add("SELECT _ID, COL_A FROM T1 WHERE _ID=?");
queryPool.add("SELECT _ID, COL_B FROM T1 WHERE _ID=?");
queryPool.add("SELECT _ID, COL_C FROM T1 WHERE _ID=?");
queryPool.add("SELECT COL_A, COL_B FROM T1 WHERE _ID=?");
queryPool.add("SELECT COL_A, COL_C FROM T1 WHERE _ID=?");
queryPool.add("SELECT COL_B, COL_C FROM T1 WHERE _ID=?");
while (state.keepRunning()) {
Random rnd = new Random(0);
int queries = 1000;
for (int iQuery = 0; iQuery < queries; ++iQuery) {
int queryIndex = rnd.nextInt(queryPool.size());
int index = rnd.nextInt(DEFAULT_DATASET_SIZE);
try (Cursor cursor = mDatabase.rawQuery(
queryPool.get(queryIndex), new String[] {String.valueOf(index)})) {
assertTrue(cursor.moveToNext());
}
}
}
Log.d("testSelectMemory",
"cacheMissRate: " + mDatabase.getStatementCacheMissRate()
+ "Total Statements: " + mDatabase.getTotalPreparedStatements()
+ ". Misses: " + mDatabase.getTotalStatementCacheMisses());
// Make sure caching is working and our miss rate should definitely be less than 100%
// however, we would expect this number to be actually closer to 0.
assertTrue(mDatabase.getStatementCacheMissRate() < 1);
mDatabase.close();
mContext.deleteDatabase(DB_NAME);
}
@Test
public void testSelectMultipleRows() {
insertT1TestDataSet();

View File

@@ -975,8 +975,8 @@ package android.database.sqlite {
}
public static class SQLiteDebug.DbStats {
ctor public SQLiteDebug.DbStats(String, long, long, int, int, int, int);
field public String cache;
ctor public SQLiteDebug.DbStats(@NonNull String, long, long, int, int, int, int, boolean);
field public final boolean arePoolStats;
field public String dbName;
field public long dbSize;
field public int lookaside;

View File

@@ -1026,7 +1026,10 @@ public final class ActivityThread extends ClientTransactionHandler
}
private class ApplicationThread extends IApplicationThread.Stub {
private static final String DB_INFO_FORMAT = " %8s %8s %14s %14s %s";
private static final String DB_CONNECTION_INFO_HEADER = " %8s %8s %14s %5s %5s %5s %s";
private static final String DB_CONNECTION_INFO_FORMAT = " %8s %8s %14s %5d %5d %5d %s";
private static final String DB_POOL_INFO_HEADER = " %13s %13s %13s %s";
private static final String DB_POOL_INFO_FORMAT = " %13d %13d %13d %s";
public final void scheduleReceiver(Intent intent, ActivityInfo info,
CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
@@ -1454,8 +1457,9 @@ public final class ActivityThread extends ClientTransactionHandler
pw.print(','); pw.print(dbStats.pageSize);
pw.print(','); pw.print(dbStats.dbSize);
pw.print(','); pw.print(dbStats.lookaside);
pw.print(','); pw.print(dbStats.cache);
pw.print(','); pw.print(dbStats.cache);
pw.print(','); pw.print(dbStats.cacheHits);
pw.print(','); pw.print(dbStats.cacheMisses);
pw.print(','); pw.print(dbStats.cacheSize);
}
pw.println();
@@ -1490,15 +1494,34 @@ public final class ActivityThread extends ClientTransactionHandler
int N = stats.dbStats.size();
if (N > 0) {
pw.println(" DATABASES");
printRow(pw, DB_INFO_FORMAT, "pgsz", "dbsz", "Lookaside(b)", "cache",
"Dbname");
printRow(pw, DB_CONNECTION_INFO_HEADER, "pgsz", "dbsz", "Lookaside(b)",
"cache hits", "cache misses", "cache size", "Dbname");
pw.println("PER CONNECTION STATS");
for (int i = 0; i < N; i++) {
DbStats dbStats = stats.dbStats.get(i);
printRow(pw, DB_INFO_FORMAT,
if (dbStats.arePoolStats) {
// these will be printed after
continue;
}
printRow(pw, DB_CONNECTION_INFO_FORMAT,
(dbStats.pageSize > 0) ? String.valueOf(dbStats.pageSize) : " ",
(dbStats.dbSize > 0) ? String.valueOf(dbStats.dbSize) : " ",
(dbStats.lookaside > 0) ? String.valueOf(dbStats.lookaside) : " ",
dbStats.cache, dbStats.dbName);
dbStats.cacheHits, dbStats.cacheMisses, dbStats.cacheSize,
dbStats.dbName);
}
// Print stats accumulated through all the connections that have existed in the
// pool since it was opened.
pw.println("POOL STATS");
printRow(pw, DB_POOL_INFO_HEADER, "cache hits", "cache misses", "cache size",
"Dbname");
for (int i = 0; i < N; i++) {
DbStats dbStats = stats.dbStats.get(i);
if (!dbStats.arePoolStats) {
continue;
}
printRow(pw, DB_POOL_INFO_FORMAT, dbStats.cacheHits, dbStats.cacheMisses,
dbStats.cacheSize, dbStats.dbName);
}
}
@@ -1623,7 +1646,12 @@ public final class ActivityThread extends ClientTransactionHandler
proto.write(MemInfoDumpProto.AppData.SqlStats.Database.DB_SIZE, dbStats.dbSize);
proto.write(MemInfoDumpProto.AppData.SqlStats.Database.LOOKASIDE_B,
dbStats.lookaside);
proto.write(MemInfoDumpProto.AppData.SqlStats.Database.CACHE, dbStats.cache);
proto.write(
MemInfoDumpProto.AppData.SqlStats.Database.CACHE_HITS, dbStats.cacheHits);
proto.write(MemInfoDumpProto.AppData.SqlStats.Database.CACHE_MISSES,
dbStats.cacheMisses);
proto.write(
MemInfoDumpProto.AppData.SqlStats.Database.CACHE_SIZE, dbStats.cacheSize);
proto.end(dToken);
}
proto.end(sToken);

View File

@@ -1053,6 +1053,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
}
private PreparedStatement acquirePreparedStatement(String sql) {
++mPool.mTotalPrepareStatements;
PreparedStatement statement = mPreparedStatementCache.get(sql);
boolean skipCache = false;
if (statement != null) {
@@ -1064,7 +1065,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
// statement but do not cache it.
skipCache = true;
}
++mPool.mTotalPrepareStatementCacheMiss;
final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
try {
final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
@@ -1320,7 +1321,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
if (!path.isEmpty()) {
label.append(": ").append(path);
}
dbStatsList.add(new DbStats(label.toString(), pageCount, pageSize, 0, 0, 0, 0));
dbStatsList.add(
new DbStats(label.toString(), pageCount, pageSize, 0, 0, 0, 0, false));
}
} catch (SQLiteException ex) {
// Ignore.
@@ -1349,9 +1351,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
label = mConfiguration.path + " (" + mConnectionId + ")";
}
return new DbStats(label, pageCount, pageSize, lookaside,
mPreparedStatementCache.hitCount(),
mPreparedStatementCache.missCount(),
mPreparedStatementCache.size());
mPreparedStatementCache.hitCount(), mPreparedStatementCache.missCount(),
mPreparedStatementCache.size(), false);
}
@Override

View File

@@ -32,6 +32,7 @@ import android.util.Printer;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import dalvik.annotation.optimization.NeverCompile;
import dalvik.system.CloseGuard;
import java.io.Closeable;
@@ -103,6 +104,10 @@ public final class SQLiteConnectionPool implements Closeable {
new ArrayList<SQLiteConnection>();
private SQLiteConnection mAvailablePrimaryConnection;
// Prepare statement cache statistics
public int mTotalPrepareStatementCacheMiss = 0;
public int mTotalPrepareStatements = 0;
@GuardedBy("mLock")
private IdleConnectionHandler mIdleConnectionHandler;
@@ -507,6 +512,12 @@ public final class SQLiteConnectionPool implements Closeable {
for (SQLiteConnection connection : mAcquiredConnections.keySet()) {
connection.collectDbStatsUnsafe(dbStatsList);
}
// Global pool stats
DbStats poolStats = new DbStats(mConfiguration.path, 0, 0, 0,
mTotalPrepareStatements - mTotalPrepareStatementCacheMiss,
mTotalPrepareStatementCacheMiss, mTotalPrepareStatements, true);
dbStatsList.add(poolStats);
}
}
@@ -1203,6 +1214,16 @@ public final class SQLiteConnectionPool implements Closeable {
}
}
/** @hide */
@NeverCompile
public double getStatementCacheMissRate() {
if (mTotalPrepareStatements == 0) {
// no statements executed thus no miss rate.
return 0;
}
return (double) mTotalPrepareStatementCacheMiss / (double) mTotalPrepareStatements;
}
public long getTotalStatementsTime() {
return mTotalStatementsTime.get();
}

View File

@@ -45,6 +45,8 @@ import android.util.Log;
import android.util.Pair;
import android.util.Printer;
import com.android.internal.util.Preconditions;
import dalvik.annotation.optimization.NeverCompile;
import dalvik.system.CloseGuard;
import java.io.File;
import java.io.FileFilter;
@@ -2233,6 +2235,16 @@ public final class SQLiteDatabase extends SQLiteClosable {
}
}
/** @hide */
@NeverCompile
public double getStatementCacheMissRate() {
synchronized (mLock) {
throwIfNotOpenLocked();
return mConnectionPoolLocked.getStatementCacheMissRate();
}
}
/**
* Sets whether foreign key constraints are enabled for the database.
* <p>
@@ -2489,6 +2501,22 @@ public final class SQLiteDatabase extends SQLiteClosable {
return connectionPools;
}
/** @hide */
@NeverCompile
public int getTotalPreparedStatements() {
throwIfNotOpenLocked();
return mConnectionPoolLocked.mTotalPrepareStatements;
}
/** @hide */
@NeverCompile
public int getTotalStatementCacheMisses() {
throwIfNotOpenLocked();
return mConnectionPoolLocked.mTotalPrepareStatementCacheMiss;
}
/**
* Dump detailed information about all open databases in the current process.
* Used by bug report.

View File

@@ -16,6 +16,7 @@
package android.database.sqlite;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
@@ -23,7 +24,6 @@ import android.os.Process;
import android.os.SystemProperties;
import android.util.Log;
import android.util.Printer;
import java.util.ArrayList;
/**
@@ -173,16 +173,26 @@ public final class SQLiteDebug {
@UnsupportedAppUsage
public int lookaside;
/** statement cache stats: hits/misses/cachesize */
public String cache;
/** @hide */
final public int cacheHits;
/** @hide */
final public int cacheMisses;
/** @hide */
final public int cacheSize;
public DbStats(String dbName, long pageCount, long pageSize, int lookaside,
int hits, int misses, int cachesize) {
/** true if connection specific stats or whole connection pool if false */
public final boolean arePoolStats;
public DbStats(@NonNull String dbName, long pageCount, long pageSize, int lookaside,
int hits, int misses, int cachesize, boolean arePoolStats) {
this.dbName = dbName;
this.pageSize = pageSize / 1024;
dbSize = (pageCount * pageSize) / 1024;
this.lookaside = lookaside;
this.cache = hits + "/" + misses + "/" + cachesize;
this.cacheHits = hits;
this.cacheMisses = misses;
this.cacheSize = cachesize;
this.arePoolStats = arePoolStats;
}
}

View File

@@ -266,6 +266,9 @@ message MemInfoDumpProto {
optional int32 lookaside_b = 4;
// Statement cache stats: hits/misses/cachesize
optional string cache = 5;
optional int32 cache_hits = 6;
optional int32 cache_misses = 7;
optional int32 cache_size = 8;
}
repeated Database databases = 4;
}