From f311e0b26d2fc11cd483993215aef867a85dfd91 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Thu, 13 Aug 2020 14:16:58 -0600 Subject: [PATCH] More robust createAppData() batching. The perf team noticed that createAppData() is pretty slow, holding up typical device boot timings because it loops over all installed apps twice (once for DE, once for CE storage), making blocking installd calls for each individual app. There was a createAppDataBatched() method added awhile back, but it was only wired up for multi-user initialization, and it was fragile because any failure in the batch would leave the device in a non-deterministic state. This change rewrites the installd batch call to return a unique result object for each request, allowing us to share both detailed success (returning CE inode values) and detailed error messages, instead of failing the entire batch request. On the framework side, we use CompletableFuture to collect multiple requests for batching, while also allowing "chaining" of follow-up work, which PackageManagerService heavily relies upon. (For example, when a specific package fails, we might want to try destroying and recreating its data directories.) Bug: 163861619 Test: atest FrameworksServicesTests:com.android.server.pm Test: atest android.appsecurity.cts.DirectBootHostTest Change-Id: I79aa28b59067f5b82e448359ff2febb2d1bcdafc --- .../java/com/android/server/pm/Installer.java | 148 ++++++++++++-- .../server/pm/PackageManagerService.java | 187 +++++++++++------- .../java/com/android/server/pm/Settings.java | 34 +--- 3 files changed, 250 insertions(+), 119 deletions(-) diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index 672ad5e594284..cd383b9d1d7a1 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -23,6 +23,8 @@ import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.PackageStats; import android.os.Build; +import android.os.CreateAppDataArgs; +import android.os.CreateAppDataResult; import android.os.IBinder; import android.os.IBinder.DeathRecipient; import android.os.IInstalld; @@ -39,7 +41,10 @@ import dalvik.system.BlockGuard; import dalvik.system.VMRuntime; import java.io.FileDescriptor; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; public class Installer extends SystemService { private static final String TAG = "Installer"; @@ -176,37 +181,140 @@ public class Installer extends SystemService { } } + private static CreateAppDataArgs buildCreateAppDataArgs(String uuid, String packageName, + int userId, int flags, int appId, String seInfo, int targetSdkVersion) { + final CreateAppDataArgs args = new CreateAppDataArgs(); + args.uuid = uuid; + args.packageName = packageName; + args.userId = userId; + args.flags = flags; + args.appId = appId; + args.seInfo = seInfo; + args.targetSdkVersion = targetSdkVersion; + return args; + } + + private static CreateAppDataResult buildPlaceholderCreateAppDataResult() { + final CreateAppDataResult result = new CreateAppDataResult(); + result.ceDataInode = -1; + result.exceptionCode = 0; + result.exceptionMessage = null; + return result; + } + + /** + * @deprecated callers are encouraged to migrate to using {@link Batch} to + * more efficiently handle operations in bulk. + */ + @Deprecated public long createAppData(String uuid, String packageName, int userId, int flags, int appId, String seInfo, int targetSdkVersion) throws InstallerException { - if (!checkBeforeRemote()) return -1; + final CreateAppDataArgs args = buildCreateAppDataArgs(uuid, packageName, userId, flags, + appId, seInfo, targetSdkVersion); + final CreateAppDataResult result = createAppData(args); + if (result.exceptionCode == 0) { + return result.ceDataInode; + } else { + throw new InstallerException(result.exceptionMessage); + } + } + + public @NonNull CreateAppDataResult createAppData(@NonNull CreateAppDataArgs args) + throws InstallerException { + if (!checkBeforeRemote()) { + return buildPlaceholderCreateAppDataResult(); + } try { - return mInstalld.createAppData(uuid, packageName, userId, flags, appId, seInfo, - targetSdkVersion); + return mInstalld.createAppData(args); + } catch (Exception e) { + throw InstallerException.from(e); + } + } + + public @NonNull CreateAppDataResult[] createAppDataBatched(@NonNull CreateAppDataArgs[] args) + throws InstallerException { + if (!checkBeforeRemote()) { + final CreateAppDataResult[] results = new CreateAppDataResult[args.length]; + Arrays.fill(results, buildPlaceholderCreateAppDataResult()); + return results; + } + try { + return mInstalld.createAppDataBatched(args); } catch (Exception e) { throw InstallerException.from(e); } } /** - * Batched version of createAppData for use with multiple packages. + * Class that collects multiple {@code installd} operations together in an + * attempt to more efficiently execute them in bulk. + *

+ * Instead of returning results immediately, {@link CompletableFuture} + * instances are returned which can be used to chain follow-up work for each + * request. + *

+ * The creator of this object must invoke {@link #execute()} + * exactly once to begin execution of all pending operations. Once execution + * has been kicked off, no additional events can be enqueued into this + * instance, but multiple instances can safely exist in parallel. */ - public void createAppDataBatched(String[] uuids, String[] packageNames, int userId, int flags, - int[] appIds, String[] seInfos, int[] targetSdkVersions) throws InstallerException { - if (!checkBeforeRemote()) return; - final int batchSize = 256; - for (int i = 0; i < uuids.length; i += batchSize) { - int to = i + batchSize; - if (to > uuids.length) { - to = uuids.length; - } + public static class Batch { + private static final int CREATE_APP_DATA_BATCH_SIZE = 256; - try { - mInstalld.createAppDataBatched(Arrays.copyOfRange(uuids, i, to), - Arrays.copyOfRange(packageNames, i, to), userId, flags, - Arrays.copyOfRange(appIds, i, to), Arrays.copyOfRange(seInfos, i, to), - Arrays.copyOfRange(targetSdkVersions, i, to)); - } catch (Exception e) { - throw InstallerException.from(e); + private boolean mExecuted; + + private final List mArgs = new ArrayList<>(); + private final List> mFutures = new ArrayList<>(); + + /** + * Enqueue the given {@code installd} operation to be executed in the + * future when {@link #execute(Installer)} is invoked. + *

+ * Callers of this method are not required to hold a monitor lock on an + * {@link Installer} object. + */ + public synchronized @NonNull CompletableFuture createAppData(String uuid, + String packageName, int userId, int flags, int appId, String seInfo, + int targetSdkVersion) { + if (mExecuted) throw new IllegalStateException(); + + final CreateAppDataArgs args = buildCreateAppDataArgs(uuid, packageName, userId, flags, + appId, seInfo, targetSdkVersion); + final CompletableFuture future = new CompletableFuture<>(); + mArgs.add(args); + mFutures.add(future); + return future; + } + + /** + * Execute all pending {@code installd} operations that have been + * collected by this batch in a blocking fashion. + *

+ * Callers of this method must hold a monitor lock on the given + * {@link Installer} object. + */ + public synchronized void execute(@NonNull Installer installer) throws InstallerException { + if (mExecuted) throw new IllegalStateException(); + mExecuted = true; + + final int size = mArgs.size(); + for (int i = 0; i < size; i += CREATE_APP_DATA_BATCH_SIZE) { + final CreateAppDataArgs[] args = new CreateAppDataArgs[Math.min(size - i, + CREATE_APP_DATA_BATCH_SIZE)]; + for (int j = 0; j < args.length; j++) { + args[j] = mArgs.get(i + j); + } + final CreateAppDataResult[] results = installer.createAppDataBatched(args); + for (int j = 0; j < args.length; j++) { + final CreateAppDataResult result = results[j]; + final CompletableFuture future = mFutures.get(i + j); + if (result.exceptionCode == 0) { + future.complete(result.ceDataInode); + } else { + future.completeExceptionally( + new InstallerException(result.exceptionMessage)); + } + } } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 58a1648e51ad3..23443fd74eca2 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -418,6 +418,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -3481,6 +3482,7 @@ public class PackageManagerService extends IPackageManager.Stub return; } int count = 0; + final Installer.Batch batch = new Installer.Batch(); for (String pkgName : deferPackages) { AndroidPackage pkg = null; synchronized (mLock) { @@ -3490,13 +3492,14 @@ public class PackageManagerService extends IPackageManager.Stub } } if (pkg != null) { - synchronized (mInstallLock) { - prepareAppDataAndMigrateLIF(pkg, UserHandle.USER_SYSTEM, storageFlags, - true /* maybeMigrateAppData */); - } + prepareAppDataAndMigrate(batch, pkg, UserHandle.USER_SYSTEM, storageFlags, + true /* maybeMigrateAppData */); count++; } } + synchronized (mInstallLock) { + executeBatchLI(batch); + } traceLog.traceEnd(); Slog.i(TAG, "Deferred reconcileAppsData finished " + count + " packages"); }, "prepareAppData"); @@ -22736,6 +22739,14 @@ public class PackageManagerService extends IPackageManager.Stub } } + private void executeBatchLI(@NonNull Installer.Batch batch) { + try { + batch.execute(mInstaller); + } catch (InstallerException e) { + Slog.w(TAG, "Failed to execute pending operations", e); + } + } + /** * Examine all apps present on given mounted volume, and destroy apps that * aren't expected, either due to uninstallation or reinstallation on @@ -22875,6 +22886,8 @@ public class PackageManagerService extends IPackageManager.Stub // Ensure that data directories are ready to roll for all packages // installed for this volume and user + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "prepareAppDataAndMigrate"); + Installer.Batch batch = new Installer.Batch(); final List packages; synchronized (mLock) { packages = mSettings.getVolumePackagesLPr(volumeUuid); @@ -22895,10 +22908,12 @@ public class PackageManagerService extends IPackageManager.Stub } if (ps.getInstalled(userId)) { - prepareAppDataAndMigrateLIF(ps.pkg, userId, flags, migrateAppData); + prepareAppDataAndMigrate(batch, ps.pkg, userId, flags, migrateAppData); preparedCount++; } } + executeBatchLI(batch); + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); Slog.v(TAG, "reconcileAppsData finished " + preparedCount + " packages"); return result; @@ -22923,6 +22938,7 @@ public class PackageManagerService extends IPackageManager.Stub mSettings.writeKernelMappingLPr(ps); } + Installer.Batch batch = new Installer.Batch(); UserManagerInternal umInternal = mInjector.getUserManagerInternal(); StorageManagerInternal smInternal = mInjector.getStorageManagerInternal(); for (UserInfo user : mUserManager.getUsers(false /*excludeDying*/)) { @@ -22937,16 +22953,20 @@ public class PackageManagerService extends IPackageManager.Stub if (ps.getInstalled(user.id)) { // TODO: when user data is locked, mark that we're still dirty - prepareAppDataLIF(pkg, user.id, flags); - - if (umInternal.isUserUnlockingOrUnlocked(user.id)) { - // Prepare app data on external storage; currently this is used to - // setup any OBB dirs that were created by the installer correctly. - int uid = UserHandle.getUid(user.id, UserHandle.getAppId(pkg.getUid())); - smInternal.prepareAppDataAfterInstall(pkg.getPackageName(), uid); - } + prepareAppData(batch, pkg, user.id, flags).thenRun(() -> { + // Note: this code block is executed with the Installer lock + // already held, since it's invoked as a side-effect of + // executeBatchLI() + if (umInternal.isUserUnlockingOrUnlocked(user.id)) { + // Prepare app data on external storage; currently this is used to + // setup any OBB dirs that were created by the installer correctly. + int uid = UserHandle.getUid(user.id, UserHandle.getAppId(pkg.getUid())); + smInternal.prepareAppDataAfterInstall(pkg.getPackageName(), uid); + } + }); } } + executeBatchLI(batch); } /** @@ -22957,26 +22977,33 @@ public class PackageManagerService extends IPackageManager.Stub * will try recovering system apps by wiping data; third-party app data is * left intact. */ - private void prepareAppDataLIF(AndroidPackage pkg, int userId, int flags) { + private @NonNull CompletableFuture prepareAppData(@NonNull Installer.Batch batch, + @Nullable AndroidPackage pkg, int userId, int flags) { if (pkg == null) { Slog.wtf(TAG, "Package was null!", new Throwable()); - return; + return CompletableFuture.completedFuture(null); } - prepareAppDataLeafLIF(pkg, userId, flags); + return prepareAppDataLeaf(batch, pkg, userId, flags); } - private void prepareAppDataAndMigrateLIF(AndroidPackage pkg, int userId, int flags, - boolean maybeMigrateAppData) { - prepareAppDataLIF(pkg, userId, flags); - - if (maybeMigrateAppData && maybeMigrateAppDataLIF(pkg, userId)) { - // We may have just shuffled around app data directories, so - // prepare them one more time - prepareAppDataLIF(pkg, userId, flags); - } + private @NonNull CompletableFuture prepareAppDataAndMigrate(@NonNull Installer.Batch batch, + @NonNull AndroidPackage pkg, int userId, int flags, boolean maybeMigrateAppData) { + return prepareAppData(batch, pkg, userId, flags).thenRun(() -> { + // Note: this code block is executed with the Installer lock + // already held, since it's invoked as a side-effect of + // executeBatchLI() + if (maybeMigrateAppData && maybeMigrateAppDataLIF(pkg, userId)) { + // We may have just shuffled around app data directories, so + // prepare them one more time + final Installer.Batch batchInner = new Installer.Batch(); + prepareAppData(batchInner, pkg, userId, flags); + executeBatchLI(batchInner); + } + }); } - private void prepareAppDataLeafLIF(AndroidPackage pkg, int userId, int flags) { + private @NonNull CompletableFuture prepareAppDataLeaf(@NonNull Installer.Batch batch, + @NonNull AndroidPackage pkg, int userId, int flags) { if (DEBUG_APP_DATA) { Slog.v(TAG, "prepareAppData for " + pkg.getPackageName() + " u" + userId + " 0x" + Integer.toHexString(flags)); @@ -22996,57 +23023,67 @@ public class PackageManagerService extends IPackageManager.Stub Preconditions.checkNotNull(pkgSeInfo); final String seInfo = pkgSeInfo + (pkg.getSeInfoUser() != null ? pkg.getSeInfoUser() : ""); - long ceDataInode = -1; - try { - ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId, flags, - appId, seInfo, pkg.getTargetSdkVersion()); - } catch (InstallerException e) { - if (pkg.isSystem()) { - logCriticalInfo(Log.ERROR, "Failed to create app data for " + packageName - + ", but trying to recover: " + e); - destroyAppDataLeafLIF(pkg, userId, flags); - try { - ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId, flags, - appId, seInfo, pkg.getTargetSdkVersion()); - logCriticalInfo(Log.DEBUG, "Recovery succeeded!"); - } catch (InstallerException e2) { - logCriticalInfo(Log.DEBUG, "Recovery failed!"); - } - } else { - Slog.e(TAG, "Failed to create app data for " + packageName + ": " + e); - } - } - // Prepare the application profiles only for upgrades and first boot (so that we don't - // repeat the same operation at each boot). - // We only have to cover the upgrade and first boot here because for app installs we - // prepare the profiles before invoking dexopt (in installPackageLI). - // - // We also have to cover non system users because we do not call the usual install package - // methods for them. - // - // NOTE: in order to speed up first boot time we only create the current profile and do not - // update the content of the reference profile. A system image should already be configured - // with the right profile keys and the profiles for the speed-profile prebuilds should - // already be copied. That's done in #performDexOptUpgrade. - // - // TODO(calin, mathieuc): We should use .dm files for prebuilds profiles instead of - // manually copying them in #performDexOptUpgrade. When we do that we should have a more - // granular check here and only update the existing profiles. - if (mIsUpgrade || mFirstBoot || (userId != UserHandle.USER_SYSTEM)) { - mArtManagerService.prepareAppProfiles(pkg, userId, - /* updateReferenceProfileContent= */ false); - } + final int targetSdkVersion = pkg.getTargetSdkVersion(); - if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) { - // TODO: mark this structure as dirty so we persist it! - synchronized (mLock) { - if (ps != null) { - ps.setCeDataInode(ceDataInode, userId); - } - } - } + return batch.createAppData(volumeUuid, packageName, userId, flags, appId, seInfo, + targetSdkVersion).whenComplete((ceDataInode, e) -> { + // Note: this code block is executed with the Installer lock + // already held, since it's invoked as a side-effect of + // executeBatchLI() + if ((e != null) && pkg.isSystem()) { + logCriticalInfo(Log.ERROR, "Failed to create app data for " + packageName + + ", but trying to recover: " + e); + destroyAppDataLeafLIF(pkg, userId, flags); + try { + ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId, + flags, appId, seInfo, pkg.getTargetSdkVersion()); + logCriticalInfo(Log.DEBUG, "Recovery succeeded!"); + } catch (InstallerException e2) { + logCriticalInfo(Log.DEBUG, "Recovery failed!"); + } + } else if (e != null) { + Slog.e(TAG, "Failed to create app data for " + packageName + ": " + e); + } - prepareAppDataContentsLeafLIF(pkg, ps, userId, flags); + // Prepare the application profiles only for upgrades and + // first boot (so that we don't repeat the same operation at + // each boot). + // + // We only have to cover the upgrade and first boot here + // because for app installs we prepare the profiles before + // invoking dexopt (in installPackageLI). + // + // We also have to cover non system users because we do not + // call the usual install package methods for them. + // + // NOTE: in order to speed up first boot time we only create + // the current profile and do not update the content of the + // reference profile. A system image should already be + // configured with the right profile keys and the profiles + // for the speed-profile prebuilds should already be copied. + // That's done in #performDexOptUpgrade. + // + // TODO(calin, mathieuc): We should use .dm files for + // prebuilds profiles instead of manually copying them in + // #performDexOptUpgrade. When we do that we should have a + // more granular check here and only update the existing + // profiles. + if (mIsUpgrade || mFirstBoot || (userId != UserHandle.USER_SYSTEM)) { + mArtManagerService.prepareAppProfiles(pkg, userId, + /* updateReferenceProfileContent= */ false); + } + + if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) { + // TODO: mark this structure as dirty so we persist it! + synchronized (mLock) { + if (ps != null) { + ps.setCeDataInode(ceDataInode, userId); + } + } + } + + prepareAppDataContentsLeafLIF(pkg, ps, userId, flags); + }); } private void prepareAppDataContentsLIF(AndroidPackage pkg, @Nullable PackageSetting pkgSetting, diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 0061480c97f1b..826b082635f93 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -102,6 +102,7 @@ import com.android.internal.util.XmlUtils; import com.android.permission.persistence.RuntimePermissionsPersistence; import com.android.permission.persistence.RuntimePermissionsState; import com.android.server.LocalServices; +import com.android.server.pm.Installer.Batch; import com.android.server.pm.Installer.InstallerException; import com.android.server.pm.parsing.PackageInfoUtils; import com.android.server.pm.parsing.pkg.AndroidPackage; @@ -4148,24 +4149,12 @@ public final class Settings { final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG + "Timing", Trace.TRACE_TAG_PACKAGE_MANAGER); t.traceBegin("createNewUser-" + userHandle); - String[] volumeUuids; - String[] names; - int[] appIds; - String[] seinfos; - int[] targetSdkVersions; - int packagesCount; + Installer.Batch batch = new Installer.Batch(); final boolean skipPackageWhitelist = userTypeInstallablePackages == null; synchronized (mLock) { - Collection packages = mPackages.values(); - packagesCount = packages.size(); - volumeUuids = new String[packagesCount]; - names = new String[packagesCount]; - appIds = new int[packagesCount]; - seinfos = new String[packagesCount]; - targetSdkVersions = new int[packagesCount]; - Iterator packagesIterator = packages.iterator(); - for (int i = 0; i < packagesCount; i++) { - PackageSetting ps = packagesIterator.next(); + final int size = mPackages.size(); + for (int i = 0; i < size; i++) { + final PackageSetting ps = mPackages.valueAt(i); if (ps.pkg == null) { continue; } @@ -4187,18 +4176,15 @@ public final class Settings { } // Need to create a data directory for all apps under this user. Accumulate all // required args and call the installer after mPackages lock has been released - volumeUuids[i] = ps.volumeUuid; - names[i] = ps.name; - appIds[i] = ps.appId; - seinfos[i] = AndroidPackageUtils.getSeInfo(ps.pkg, ps); - targetSdkVersions[i] = ps.pkg.getTargetSdkVersion(); + final String seInfo = AndroidPackageUtils.getSeInfo(ps.pkg, ps); + batch.createAppData(ps.volumeUuid, ps.name, userHandle, + StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE, ps.appId, + seInfo, ps.pkg.getTargetSdkVersion()); } } t.traceBegin("createAppData"); - final int flags = StorageManager.FLAG_STORAGE_CE | StorageManager.FLAG_STORAGE_DE; try { - installer.createAppDataBatched(volumeUuids, names, userHandle, flags, appIds, seinfos, - targetSdkVersions); + batch.execute(installer); } catch (InstallerException e) { Slog.w(TAG, "Failed to prepare app data", e); }