Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f311e0b2 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

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
parent 9bd766de
Loading
Loading
Loading
Loading
+129 −21
Original line number Diff line number Diff line
@@ -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,39 +181,142 @@ 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);
        }
    }

    /**
     * Batched version of createAppData for use with multiple packages.
     */
    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 @NonNull CreateAppDataResult[] createAppDataBatched(@NonNull CreateAppDataArgs[] args)
            throws InstallerException {
        if (!checkBeforeRemote()) {
            final CreateAppDataResult[] results = new CreateAppDataResult[args.length];
            Arrays.fill(results, buildPlaceholderCreateAppDataResult());
            return results;
        }

        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));
            return mInstalld.createAppDataBatched(args);
        } catch (Exception e) {
            throw InstallerException.from(e);
        }
    }

    /**
     * Class that collects multiple {@code installd} operations together in an
     * attempt to more efficiently execute them in bulk.
     * <p>
     * Instead of returning results immediately, {@link CompletableFuture}
     * instances are returned which can be used to chain follow-up work for each
     * request.
     * <p>
     * The creator of this object <em>must</em> 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 static class Batch {
        private static final int CREATE_APP_DATA_BATCH_SIZE = 256;

        private boolean mExecuted;

        private final List<CreateAppDataArgs> mArgs = new ArrayList<>();
        private final List<CompletableFuture<Long>> mFutures = new ArrayList<>();

        /**
         * Enqueue the given {@code installd} operation to be executed in the
         * future when {@link #execute(Installer)} is invoked.
         * <p>
         * Callers of this method are not required to hold a monitor lock on an
         * {@link Installer} object.
         */
        public synchronized @NonNull CompletableFuture<Long> 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<Long> 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.
         * <p>
         * Callers of this method <em>must</em> 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<Long> future = mFutures.get(i + j);
                    if (result.exceptionCode == 0) {
                        future.complete(result.ceDataInode);
                    } else {
                        future.completeExceptionally(
                                new InstallerException(result.exceptionMessage));
                    }
                }
            }
        }
    }

    public void restoreconAppData(String uuid, String packageName, int userId, int flags, int appId,
+112 −75
Original line number Diff line number Diff line
@@ -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,
                        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<PackageSetting> 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);
                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);
    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
            prepareAppDataLIF(pkg, userId, flags);
                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,42 +23023,51 @@ 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()) {
        final int targetSdkVersion = pkg.getTargetSdkVersion();
        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());
                            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 {
                    } else if (e != null) {
                        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).
                    // 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.
                    // 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.
                    // 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.
                    // 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);
@@ -23047,6 +23083,7 @@ public class PackageManagerService extends IPackageManager.Stub
                    }
                    prepareAppDataContentsLeafLIF(pkg, ps, userId, flags);
                });
    }
    private void prepareAppDataContentsLIF(AndroidPackage pkg, @Nullable PackageSetting pkgSetting,
+10 −24
Original line number Diff line number Diff line
@@ -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<PackageSetting> 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<PackageSetting> 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);
        }