From b988ab77cb28ea29e16d976e7fe03f7484de8414 Mon Sep 17 00:00:00 2001 From: Schneider Victor-tulias Date: Wed, 18 Aug 2021 14:53:50 -0700 Subject: [PATCH] Improve workspace loading times. Updated loadWorkspace to load all required icons in a series of bulk sql queries. This reduces the cost of SQL lookups (up to two lookups per user, rather than one lookup per icon) Bug: 195674813 Test: Added all icons to workspace, added duplicate icons, added icons for same component name from different users Change-Id: I56afaa04e7c7701f0d3c86b31c53f578dfa73fe6 --- .../launcher3/config/FeatureFlags.java | 5 + .../android/launcher3/icons/IconCache.java | 94 ++++++++++++++++ .../android/launcher3/model/LoaderCursor.java | 58 +++++----- .../android/launcher3/model/LoaderTask.java | 24 ++++- .../launcher3/model/data/IconRequestInfo.java | 101 ++++++++++++++++++ 5 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 src/com/android/launcher3/model/data/IconRequestInfo.java diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index f091262c35..382f7a75cb 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -147,6 +147,11 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_THEMED_ICONS = getDebugFlag( "ENABLE_THEMED_ICONS", true, "Enable themed icons on workspace"); + public static final BooleanFlag ENABLE_BULK_WORKSPACE_ICON_LOADING = getDebugFlag( + "ENABLE_BULK_WORKSPACE_ICON_LOADING", + false, + "Enable loading workspace icons in bulk."); + // Keep as DeviceFlag for remote disable in emergency. public static final BooleanFlag ENABLE_OVERVIEW_SELECTIONS = new DeviceFlag( "ENABLE_OVERVIEW_SELECTIONS", true, "Show Select Mode button in Overview Actions"); diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java index 1a468aeb88..60d6e830c6 100644 --- a/src/com/android/launcher3/icons/IconCache.java +++ b/src/com/android/launcher3/icons/IconCache.java @@ -19,6 +19,8 @@ package com.android.launcher3.icons; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static java.util.stream.Collectors.groupingBy; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -30,10 +32,15 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ShortcutInfo; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; import android.graphics.drawable.Drawable; import android.os.Process; +import android.os.Trace; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import androidx.annotation.NonNull; @@ -47,6 +54,7 @@ import com.android.launcher3.icons.cache.BaseIconCache; import com.android.launcher3.icons.cache.CachingLogic; import com.android.launcher3.icons.cache.HandlerRunnable; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; @@ -56,8 +64,13 @@ import com.android.launcher3.util.InstantAppResolver; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Stream; /** * Cache of application icons. Icons can be made from any thread. @@ -306,6 +319,87 @@ public class IconCache extends BaseIconCache { applyCacheEntry(entry, infoInOut); } + /** + * Creates an sql cursor for a query of a set of ItemInfoWithIcon icons and titles. + * + * @param iconRequestInfos List of IconRequestInfos representing titles and icons to query. + * @param user UserHandle all the given iconRequestInfos share + * @param useLowResIcons whether we should exclude the icon column from the sql results. + */ + private Cursor createBulkQueryCursor( + List> iconRequestInfos, UserHandle user, boolean useLowResIcons) + throws SQLiteException { + String[] queryParams = Stream.concat( + iconRequestInfos.stream() + .map(r -> r.itemInfo.getTargetComponent()) + .filter(Objects::nonNull) + .distinct() + .map(ComponentName::flattenToString), + Stream.of(Long.toString(getSerialNumberForUser(user)))).toArray(String[]::new); + String componentNameQuery = TextUtils.join( + ",", Collections.nCopies(queryParams.length - 1, "?")); + + return mIconDb.query( + useLowResIcons ? IconDB.COLUMNS_LOW_RES : IconDB.COLUMNS_HIGH_RES, + IconDB.COLUMN_COMPONENT + + " IN ( " + componentNameQuery + " )" + + " AND " + IconDB.COLUMN_USER + " = ?", + queryParams); + } + + /** + * Load and fill icons requested in iconRequestInfos using a single bulk sql query. + */ + public synchronized void getTitlesAndIconsInBulk( + List> iconRequestInfos) { + Map, List>> iconLoadSubsectionsMap = + iconRequestInfos.stream() + .collect(groupingBy(iconRequest -> + Pair.create(iconRequest.itemInfo.user, iconRequest.useLowResIcon))); + + Trace.beginSection("loadIconsInBulk"); + iconLoadSubsectionsMap.forEach((sectionKey, filteredList) -> { + Map>> duplicateIconRequestsMap = + filteredList.stream() + .collect(groupingBy(iconRequest -> + iconRequest.itemInfo.getTargetComponent())); + + Trace.beginSection("loadIconSubsectionInBulk"); + try (Cursor c = createBulkQueryCursor( + filteredList, + /* user = */ sectionKey.first, + /* useLowResIcons = */ sectionKey.second)) { + int componentNameColumnIndex = c.getColumnIndexOrThrow(IconDB.COLUMN_COMPONENT); + while (c.moveToNext()) { + ComponentName cn = ComponentName.unflattenFromString( + c.getString(componentNameColumnIndex)); + List> duplicateIconRequests = + duplicateIconRequestsMap.get(cn); + + if (cn != null) { + CacheEntry entry = cacheLocked( + cn, + /* user = */ sectionKey.first, + () -> duplicateIconRequests.get(0).launcherActivityInfo, + mLauncherActivityInfoCachingLogic, + c, + /* usePackageIcon= */ false, + /* useLowResIcons = */ sectionKey.second); + + for (IconRequestInfo iconRequest : duplicateIconRequests) { + applyCacheEntry(entry, iconRequest.itemInfo); + } + } + } + } catch (SQLiteException e) { + Log.d(TAG, "Error reading icon cache", e); + } finally { + Trace.endSection(); + } + }); + Trace.endSection(); + } + /** * Fill in {@param infoInOut} with the corresponding icon and label. diff --git a/src/com/android/launcher3/model/LoaderCursor.java b/src/com/android/launcher3/model/LoaderCursor.java index 7e3bceee1d..8a5a9bf327 100644 --- a/src/com/android/launcher3/model/LoaderCursor.java +++ b/src/com/android/launcher3/model/LoaderCursor.java @@ -16,13 +16,10 @@ package com.android.launcher3.model; -import static android.graphics.BitmapFactory.decodeByteArray; - import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.Intent.ShortcutIconResource; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; @@ -45,11 +42,10 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.icons.BitmapInfo; import com.android.launcher3.icons.IconCache; -import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.shortcuts.ShortcutKey; @@ -184,32 +180,21 @@ public class LoaderCursor extends CursorWrapper { * Loads the icon from the cursor and updates the {@param info} if the icon is an app resource. */ protected boolean loadIcon(WorkspaceItemInfo info) { - try (LauncherIcons li = LauncherIcons.obtain(mContext)) { - if (itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { - String packageName = getString(iconPackageIndex); - String resourceName = getString(iconResourceIndex); - if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) { - info.iconResource = new ShortcutIconResource(); - info.iconResource.packageName = packageName; - info.iconResource.resourceName = resourceName; - BitmapInfo iconInfo = li.createIconBitmap(info.iconResource); - if (iconInfo != null) { - info.bitmap = iconInfo; - return true; - } - } - } + return createIconRequestInfo(info, false).loadWorkspaceIcon(mContext); + } - // Failed to load from resource, try loading from DB. - byte[] data = getBlob(iconIndex); - try { - info.bitmap = li.createIconBitmap(decodeByteArray(data, 0, data.length)); - return true; - } catch (Exception e) { - Log.e(TAG, "Failed to decode byte array for info " + info, e); - return false; - } - } + public IconRequestInfo createIconRequestInfo( + WorkspaceItemInfo wai, boolean useLowResIcon) { + String packageName = itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT + ? getString(iconPackageIndex) : null; + String resourceName = itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT + ? getString(iconResourceIndex) : null; + byte[] iconBlob = itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT + || restoreFlag != 0 + ? getBlob(iconIndex) : null; + + return new IconRequestInfo<>( + wai, mActivityInfo, packageName, resourceName, iconBlob, useLowResIcon); } /** @@ -262,6 +247,11 @@ public class LoaderCursor extends CursorWrapper { */ public WorkspaceItemInfo getAppShortcutInfo( Intent intent, boolean allowMissingTarget, boolean useLowResIcon) { + return getAppShortcutInfo(intent, allowMissingTarget, useLowResIcon, true); + } + + public WorkspaceItemInfo getAppShortcutInfo( + Intent intent, boolean allowMissingTarget, boolean useLowResIcon, boolean loadIcon) { if (user == null) { Log.d(TAG, "Null user found in getShortcutInfo"); return null; @@ -288,9 +278,11 @@ public class LoaderCursor extends CursorWrapper { info.user = user; info.intent = newIntent; - mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon); - if (mIconCache.isDefaultIcon(info.bitmap, user)) { - loadIcon(info); + if (loadIcon) { + mIconCache.getTitleAndIcon(info, mActivityInfo, useLowResIcon); + if (mIconCache.isDefaultIcon(info.bitmap, user)) { + loadIcon(info); + } } if (mActivityInfo != null) { diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index c178b02d43..124960679b 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -72,6 +72,7 @@ import com.android.launcher3.icons.cache.IconCacheUpdateHandler; import com.android.launcher3.logging.FileLog; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.FolderInfo; +import com.android.launcher3.model.data.IconRequestInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.LauncherAppWidgetInfo; @@ -420,6 +421,7 @@ public class LoaderTask implements Runnable { LauncherAppWidgetProviderInfo widgetProviderInfo; Intent intent; String targetPkg; + List> iconRequestInfos = new ArrayList<>(); while (!mStopped && c.moveToNext()) { try { @@ -542,7 +544,10 @@ public class LoaderTask implements Runnable { } else if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { info = c.getAppShortcutInfo( - intent, allowMissingTarget, useLowResIcon); + intent, + allowMissingTarget, + useLowResIcon, + !FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()); } else if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { @@ -594,6 +599,8 @@ public class LoaderTask implements Runnable { } if (info != null) { + iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon)); + c.applyCommonProperties(info); info.intent = intent; @@ -811,6 +818,21 @@ public class LoaderTask implements Runnable { Log.e(TAG, "Desktop items loading interrupted", e); } } + if (FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()) { + Trace.beginSection("LoadWorkspaceIconsInBulk"); + try { + mIconCache.getTitlesAndIconsInBulk(iconRequestInfos); + for (IconRequestInfo iconRequestInfo : + iconRequestInfos) { + WorkspaceItemInfo wai = iconRequestInfo.itemInfo; + if (mIconCache.isDefaultIcon(wai.bitmap, wai.user)) { + iconRequestInfo.loadWorkspaceIcon(mApp.getContext()); + } + } + } finally { + Trace.endSection(); + } + } } finally { IOUtils.closeSilently(c); } diff --git a/src/com/android/launcher3/model/data/IconRequestInfo.java b/src/com/android/launcher3/model/data/IconRequestInfo.java new file mode 100644 index 0000000000..2f566f6fc0 --- /dev/null +++ b/src/com/android/launcher3/model/data/IconRequestInfo.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.model.data; + +import static android.graphics.BitmapFactory.decodeByteArray; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherActivityInfo; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.LauncherSettings; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.LauncherIcons; + +/** + * Class representing one request for an icon to be queried in a sql database. + * + * @param ItemInfoWithIcon subclass whose title and icon can be loaded and filled by an sql + * query. + */ +public class IconRequestInfo { + + private static final String TAG = "IconRequestInfo"; + + @NonNull public final T itemInfo; + @Nullable public final LauncherActivityInfo launcherActivityInfo; + @Nullable public final String packageName; + @Nullable public final String resourceName; + @Nullable public final byte[] iconBlob; + public final boolean useLowResIcon; + + public IconRequestInfo( + @NonNull T itemInfo, + @Nullable LauncherActivityInfo launcherActivityInfo, + @Nullable String packageName, + @Nullable String resourceName, + @Nullable byte[] iconBlob, + boolean useLowResIcon) { + this.itemInfo = itemInfo; + this.launcherActivityInfo = launcherActivityInfo; + this.packageName = packageName; + this.resourceName = resourceName; + this.iconBlob = iconBlob; + this.useLowResIcon = useLowResIcon; + } + + /** Loads */ + public boolean loadWorkspaceIcon(Context context) { + if (!(itemInfo instanceof WorkspaceItemInfo)) { + throw new IllegalStateException( + "loadWorkspaceIcon should only be use for a WorkspaceItemInfos: " + itemInfo); + } + + try (LauncherIcons li = LauncherIcons.obtain(context)) { + WorkspaceItemInfo info = (WorkspaceItemInfo) itemInfo; + if (itemInfo.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { + if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) { + info.iconResource = new Intent.ShortcutIconResource(); + info.iconResource.packageName = packageName; + info.iconResource.resourceName = resourceName; + BitmapInfo iconInfo = li.createIconBitmap(info.iconResource); + if (iconInfo != null) { + info.bitmap = iconInfo; + return true; + } + } + } + + // Failed to load from resource, try loading from DB. + try { + if (iconBlob == null) { + return false; + } + info.bitmap = li.createIconBitmap(decodeByteArray( + iconBlob, 0, iconBlob.length)); + return true; + } catch (Exception e) { + Log.e(TAG, "Failed to decode byte array for info " + info, e); + return false; + } + } + } +}