package org.fdroid.fdroid.data; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import org.fdroid.fdroid.BuildConfig; import org.fdroid.fdroid.Utils; /** * Helper class to log slow queries to logcat when in debug mode. When not in debug mode, it * runs the queries without any logging. * * Here is an example of what would be output to logcat for a query that takes too long (except the * query would not be formatted as nicely): * * Query [155ms]: * SELECT fdroid_app.rowid as _id, ... * FROM fdroid_app * LEFT JOIN fdroid_apk ON (fdroid_apk.appId = fdroid_app.rowid) * LEFT JOIN fdroid_repo ON (fdroid_apk.repo = fdroid_repo._id) * LEFT JOIN fdroid_installedApp AS installed ON (installed.appId = fdroid_app.id) * LEFT JOIN fdroid_apk AS suggestedApk ON (fdroid_app.suggestedVercode = suggestedApk.vercode AND fdroid_app.rowid = suggestedApk.appId) * WHERE * fdroid_repo.isSwap = 0 OR fdroid_repo.isSwap IS NULL * GROUP BY fdroid_app.rowid * ORDER BY fdroid_app.name COLLATE LOCALIZED * Explain: * SCAN TABLE fdroid_app * SEARCH TABLE fdroid_apk USING COVERING INDEX sqlite_autoindex_fdroid_apk_1 (appId=?) * SEARCH TABLE fdroid_repo USING INTEGER PRIMARY KEY (rowid=?) * SEARCH TABLE fdroid_installedApp AS installed USING INDEX sqlite_autoindex_fdroid_installedApp_1 (appId=?) * SEARCH TABLE fdroid_apk AS suggestedApk USING INDEX sqlite_autoindex_fdroid_apk_1 (appId=? AND vercode=?) * USE TEMP B-TREE FOR ORDER BY */ final class LoggingQuery { private static final long SLOW_QUERY_DURATION = 100; private static final String TAG = "Slow Query"; private final SQLiteDatabase db; private final String query; private final String[] queryArgs; private LoggingQuery(SQLiteDatabase db, String query, String[] queryArgs) { this.db = db; this.query = query; this.queryArgs = queryArgs; } /** * When running a debug build, this will log details (including query plans) for any query which * takes longer than {@link LoggingQuery#SLOW_QUERY_DURATION}. */ private Cursor rawQuery() { if (BuildConfig.DEBUG) { long startTime = System.currentTimeMillis(); Cursor cursor = db.rawQuery(query, queryArgs); long queryDuration = System.currentTimeMillis() - startTime; if (queryDuration >= SLOW_QUERY_DURATION) { logSlowQuery(queryDuration); } return cursor; } return db.rawQuery(query, queryArgs); } /** * Log the query and its duration to the console. In addition, execute an "EXPLAIN QUERY PLAN" * for the query in question so that the query can be diagnosed (https://sqlite.org/eqp.html) */ private void logSlowQuery(long queryDuration) { StringBuilder sb = new StringBuilder(); sb.append("Query [") .append(queryDuration) .append("ms]: ") .append(query); try { StringBuilder sbExplain = new StringBuilder("\nExplain:\n"); for (String plan : getExplainQueryPlan()) { sbExplain.append(" ").append(plan).append("\n"); } sb.append(sbExplain); } catch (Exception e) { // Ignore exception, we caught this because the SQLite docs say explain query plan can // change between versions. We do our best in getExplainQueryPlan() to mitigate this, // but it may still break with newer releases. In that case, at least we will still be // logging the slow query to logcat which is helpful. } Utils.debugLog(TAG, sb.toString()); } private String[] getExplainQueryPlan() { Cursor cursor = db.rawQuery("EXPLAIN QUERY PLAN " + query, queryArgs); String[] plan = new String[cursor.getCount()]; cursor.moveToFirst(); while (!cursor.isAfterLast()) { // The docs at https://sqlite.org/eqp.html talk about how the output format of // EXPLAIN QUERY PLAN can change between SQLite versions. This has been observed // between the sqlite versions on Android 2.3.3 and Android 5.0. However, it seems // that the last column is always the one with the interesting details that we wish // to log. If this fails for some reason, then hey, it is only for debug builds, right? if (cursor.getColumnCount() > 0) { int index = cursor.getColumnCount() - 1; plan[cursor.getPosition()] = cursor.getString(index); } cursor.moveToNext(); } cursor.close(); return plan; } public static Cursor query(SQLiteDatabase db, String query, String[] queryBuilderArgs) { return new LoggingQuery(db, query, queryBuilderArgs).rawQuery(); } }