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
This commit is contained in:
Jeff Sharkey
2020-08-13 14:16:58 -06:00
parent 9bd766deaf
commit f311e0b26d
3 changed files with 250 additions and 119 deletions

View File

@@ -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.
* <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 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<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));
}
}
}
}
}

View File

@@ -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<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);
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,

View File

@@ -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);
}