Precalculate the preferred metadata, rather than always at runtime

The  query which dynamically figured out the preferred metadata based on
repo priority ended up being quite slow (although it did work). On lower
end devices, it has the potential to make F-Droid quite sluggish. By
optimistically precalculating the preferred metadata where possible, we
don't need to ask the question during the usual usage of F-Droid, only
when:
 * Repo priorities are changed (there is not currently a UI for this, but
  there are tests)
 * Repos are enabled/disabled
 * Repo updates are performed
This commit is contained in:
Peter Serwylo 2016-09-26 18:32:12 +10:00
parent 01b8f7f4bf
commit ab02058ece
5 changed files with 90 additions and 11 deletions

View File

@ -184,6 +184,11 @@ public class AppProvider extends FDroidProvider {
public static List<App> findCanUpdate(Context context, String[] projection) {
return cursorToList(context.getContentResolver().query(AppProvider.getCanUpdateUri(), projection, null, null, null));
}
public static void recalculatePreferredMetadata(Context context) {
Uri uri = Uri.withAppendedPath(AppProvider.getContentUri(), PATH_CALC_PREFERRED_METADATA);
context.getContentResolver().query(uri, null, null, null, null);
}
}
/**
@ -445,6 +450,7 @@ public class AppProvider extends FDroidProvider {
private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex";
private static final String PATH_REPO = "repo";
private static final String PATH_HIGHEST_PRIORITY = "highestPriority";
private static final String PATH_CALC_PREFERRED_METADATA = "calcPreferredMetadata";
private static final int CAN_UPDATE = CODE_SINGLE + 1;
private static final int INSTALLED = CAN_UPDATE + 1;
@ -459,6 +465,7 @@ public class AppProvider extends FDroidProvider {
private static final int SEARCH_INSTALLED = SEARCH_REPO + 1;
private static final int SEARCH_CAN_UPDATE = SEARCH_INSTALLED + 1;
private static final int HIGHEST_PRIORITY = SEARCH_CAN_UPDATE + 1;
private static final int CALC_PREFERRED_METADATA = HIGHEST_PRIORITY + 1;
static {
MATCHER.addURI(getAuthority(), null, CODE_LIST);
@ -476,6 +483,7 @@ public class AppProvider extends FDroidProvider {
MATCHER.addURI(getAuthority(), PATH_NO_APKS, NO_APKS);
MATCHER.addURI(getAuthority(), PATH_HIGHEST_PRIORITY + "/*", HIGHEST_PRIORITY);
MATCHER.addURI(getAuthority(), PATH_SPECIFIC_APP + "/#/*", CODE_SINGLE);
MATCHER.addURI(getAuthority(), PATH_CALC_PREFERRED_METADATA, CALC_PREFERRED_METADATA);
}
public static Uri getContentUri() {
@ -719,16 +727,11 @@ public class AppProvider extends FDroidProvider {
/**
* Ensures that for each app metadata row with the same package name, only the one from the repo
* with the best priority is represented in the result set.
* with the best priority is represented in the result set. While possible to calculate this
* dynamically each time the query is run, we precalculate it during repo updates for performance.
*/
private AppQuerySelection queryHighestPriority() {
final String highestPriority =
"SELECT MIN(r." + RepoTable.Cols.PRIORITY + ") " +
"FROM " + RepoTable.NAME + " AS r " +
"JOIN " + AppMetadataTable.NAME + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " +
"WHERE m." + Cols.PACKAGE_ID + " = " + getTableName() + "." + Cols.PACKAGE_ID;
final String selection = RepoTable.NAME + "." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")";
final String selection = PackageTable.NAME + "." + PackageTable.Cols.PREFERRED_METADATA + " = " + getTableName() + "." + Cols.ROW_ID;
return new AppQuerySelection(selection);
}
@ -793,6 +796,10 @@ public class AppProvider extends FDroidProvider {
boolean repoIsKnown = false;
switch (MATCHER.match(uri)) {
case CALC_PREFERRED_METADATA:
updatePreferredMetadata();
return null;
case CODE_LIST:
includeSwap = false;
break;
@ -937,12 +944,37 @@ public class AppProvider extends FDroidProvider {
}
protected void updateAppDetails() {
updatePreferredMetadata();
updateCompatibleFlags();
updateSuggestedFromUpstream();
updateSuggestedFromLatest();
updateIconUrls(getContext(), db(), getTableName(), getApkTableName());
}
private void updatePreferredMetadata() {
Utils.debugLog(TAG, "Deciding on which metadata should take priority for each package.");
final String app = getTableName();
final String highestPriority =
"SELECT MIN(r." + RepoTable.Cols.PRIORITY + ") " +
"FROM " + RepoTable.NAME + " AS r " +
"JOIN " + getTableName() + " AS m ON (m." + Cols.REPO_ID + " = r." + RepoTable.Cols._ID + ") " +
"WHERE m." + Cols.PACKAGE_ID + " = " + "metadata." + Cols.PACKAGE_ID;
String updateSql =
"UPDATE " + PackageTable.NAME + " " +
"SET " + PackageTable.Cols.PREFERRED_METADATA + " = ( " +
" SELECT metadata." + Cols.ROW_ID +
" FROM " + app + " AS metadata " +
" JOIN " + RepoTable.NAME + " AS repo ON (metadata." + Cols.REPO_ID + " = repo." + RepoTable.Cols._ID + ") " +
" WHERE metadata." + Cols.PACKAGE_ID + " = " + PackageTable.NAME + "." + PackageTable.Cols.ROW_ID +
" AND repo." + RepoTable.Cols.PRIORITY + " = (" + highestPriority + ")" +
");";
db().execSQL(updateSql);
}
/**
* For each app, we want to set the isCompatible flag to 1 if any of the apks we know
* about are compatible, and 0 otherwise.

View File

@ -53,7 +53,8 @@ class DBHelper extends SQLiteOpenHelper {
private static final String CREATE_TABLE_PACKAGE = "CREATE TABLE " + PackageTable.NAME
+ " ( "
+ PackageTable.Cols.PACKAGE_NAME + " text not null"
+ PackageTable.Cols.PACKAGE_NAME + " text not null, "
+ PackageTable.Cols.PREFERRED_METADATA + " integer"
+ ");";
private static final String CREATE_TABLE_REPO = "create table "
@ -838,6 +839,7 @@ class DBHelper extends SQLiteOpenHelper {
if (tableExists(db, PackageTable.NAME)) {
Utils.debugLog(TAG, "Ensuring indexes exist for " + PackageTable.NAME);
db.execSQL("CREATE INDEX IF NOT EXISTS package_packageName on " + PackageTable.NAME + " (" + PackageTable.Cols.PACKAGE_NAME + ");");
db.execSQL("CREATE INDEX IF NOT EXISTS package_preferredMetadata on " + PackageTable.NAME + " (" + PackageTable.Cols.PREFERRED_METADATA + ");");
}
Utils.debugLog(TAG, "Ensuring indexes exist for " + AppMetadataTable.NAME);

View File

@ -232,6 +232,8 @@ public class RepoProvider extends FDroidProvider {
Uri appUri = AppProvider.getNoApksUri();
int appCount = resolver.delete(appUri, null, null);
Utils.debugLog(TAG, "Removed " + appCount + " apps with no apks.");
AppProvider.Helper.recalculatePreferredMetadata(context);
}
public static int countAppsForRepo(Context context, long repoId) {
@ -399,7 +401,37 @@ public class RepoProvider extends FDroidProvider {
@Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
// When the priority of a repo changes, we need to update the "preferred metadata" foreign
// key in the package table to point to the best possible record in the app metadata table.
// The full list of times when we need to recalculate the preferred metadata includes:
// * After the priority of a repo changes
// * After a repo is disabled
// * After a repo is enabled
// * After an update is performed
// This code only checks for the priority changing. All other occasions we can't do the
// recalculation right now, because we likely haven't added/removed the relevant apps
// from the metadata table yet. Usually the repo details are updated, then a request is
// made to do the heavier work (e.g. a repo update to get new list of apps from server).
// After the heavier work is complete, then that process can request the preferred metadata
// to be recalculated.
boolean priorityChanged = false;
if (values.containsKey(Cols.PRIORITY)) {
Cursor priorityCursor = db().query(getTableName(), new String[]{Cols.PRIORITY}, where, whereArgs, null, null, null);
if (priorityCursor.getCount() > 0) {
priorityCursor.moveToFirst();
int oldPriority = priorityCursor.getInt(priorityCursor.getColumnIndex(Cols.PRIORITY));
priorityChanged = oldPriority != values.getAsInteger(Cols.PRIORITY);
}
priorityCursor.close();
}
int numRows = db().update(getTableName(), values, where, whereArgs);
if (priorityChanged) {
AppProvider.Helper.recalculatePreferredMetadata(getContext());
}
Utils.debugLog(TAG, "Updated repo. Notifying provider change: '" + uri + "'.");
getContext().getContentResolver().notifyChange(uri, null);
return numRows;

View File

@ -17,8 +17,18 @@ public interface Schema {
String ROW_ID = "rowid";
String PACKAGE_NAME = "packageName";
/**
* Metadata about a package (e.g. description, icon, etc) can come from multiple
* different repos. This is a foreign key to the row in {@link AppMetadataTable} for
* this package that comes from the repo with the best priority. Although it can be
* calculated at runtime using an SQL query, it is more efficient to figure out the
* preferred metadata once, after a repo update, rather than every time we need to know
* about a package.
*/
String PREFERRED_METADATA = "preferredMetadata";
String[] ALL = {
ROW_ID, PACKAGE_NAME,
ROW_ID, PACKAGE_NAME, PREFERRED_METADATA,
};
}
}
@ -101,7 +111,7 @@ public interface Schema {
* or which are from other related tables (e.g. {@link Cols.SuggestedApk#VERSION_NAME}).
*/
String[] ALL_COLS = {
ROW_ID, PACKAGE_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION,
ROW_ID, PACKAGE_ID, REPO_ID, IS_COMPATIBLE, NAME, SUMMARY, ICON, DESCRIPTION,
LICENSE, AUTHOR, EMAIL, WEB_URL, TRACKER_URL, SOURCE_URL,
CHANGELOG_URL, DONATE_URL, BITCOIN_ADDR, LITECOIN_ADDR, FLATTR_ID,
UPSTREAM_VERSION_NAME, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,

View File

@ -371,6 +371,9 @@ public class AppProviderTest extends FDroidProviderTest {
Uri uri = AppProvider.getContentUri();
contentResolver.insert(uri, values);
AppProvider.Helper.recalculatePreferredMetadata(context);
return AppProvider.Helper.findSpecificApp(context.getContentResolver(), id, 1, Cols.ALL);
}
}