diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index ebeb4abcce507..4cf65ab5092fc 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -517,6 +517,11 @@ interface IPackageManager { void forceDexOpt(String packageName); + /** + * Execute the background dexopt job immediately. + */ + boolean runBackgroundDexoptJob(); + /** * Reconcile the information we have about the secondary dex files belonging to * {@code packagName} and the actual dex files. For all dex files that were diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index 601a2194e8f37..66977d66e0a5a 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -30,10 +30,13 @@ import android.content.IntentFilter; import android.os.BatteryManager; import android.os.Environment; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.storage.StorageManager; import android.util.ArraySet; import android.util.Log; +import com.android.server.pm.dex.DexManager; + import java.io.File; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.TimeUnit; @@ -59,21 +62,33 @@ public class BackgroundDexOptService extends JobService { "android", BackgroundDexOptService.class.getName()); + // Possible return codes of individual optimization steps. + + // Optimizations finished. All packages were processed. + private static final int OPTIMIZE_PROCESSED = 0; + // Optimizations should continue. Issued after checking the scheduler, disk space or battery. + private static final int OPTIMIZE_CONTINUE = 1; + // Optimizations should be aborted. Job scheduler requested it. + private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2; + // Optimizations should be aborted. No space left on device. + private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3; + /** * Set of failed packages remembered across job runs. */ - static final ArraySet sFailedPackageNames = new ArraySet(); + static final ArraySet sFailedPackageNamesPrimary = new ArraySet(); + static final ArraySet sFailedPackageNamesSecondary = new ArraySet(); /** * Atomics set to true if the JobScheduler requests an abort. */ - final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false); - final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false); + private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false); + private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false); /** * Atomic set to true if one job should exit early because another job was started. */ - final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false); + private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false); private final File mDataDir = Environment.getDataDirectory(); @@ -104,8 +119,11 @@ public class BackgroundDexOptService extends JobService { // The idle maintanance job skips packages which previously failed to // compile. The given package has changed and may successfully compile // now. Remove it from the list of known failing packages. - synchronized (sFailedPackageNames) { - sFailedPackageNames.remove(packageName); + synchronized (sFailedPackageNamesPrimary) { + sFailedPackageNamesPrimary.remove(packageName); + } + synchronized (sFailedPackageNamesSecondary) { + sFailedPackageNamesSecondary.remove(packageName); } } @@ -124,9 +142,9 @@ public class BackgroundDexOptService extends JobService { return (100 * level / scale); } - private long getLowStorageThreshold() { + private long getLowStorageThreshold(Context context) { @SuppressWarnings("deprecation") - final long lowThreshold = StorageManager.from(this).getStorageLowBytes(mDataDir); + final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir); if (lowThreshold == 0) { Log.e(TAG, "Invalid low storage threshold"); } @@ -155,7 +173,7 @@ public class BackgroundDexOptService extends JobService { // Load low battery threshold from the system config. This is a 0-100 integer. final int lowBatteryThreshold = getResources().getInteger( com.android.internal.R.integer.config_lowBatteryWarningLevel); - final long lowThreshold = getLowStorageThreshold(); + final long lowThreshold = getLowStorageThreshold(this); mAbortPostBootUpdate.set(false); @@ -206,61 +224,123 @@ public class BackgroundDexOptService extends JobService { new Thread("BackgroundDexOptService_IdleOptimization") { @Override public void run() { - idleOptimization(jobParams, pm, pkgs); + int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this); + if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) { + Log.w(TAG, "Idle optimizations aborted because of space constraints."); + // If we didn't abort we ran to completion (or stopped because of space). + // Abandon our timeslice and do not reschedule. + jobFinished(jobParams, /* reschedule */ false); + } } }.start(); return true; } - private void idleOptimization(JobParameters jobParams, PackageManagerService pm, - ArraySet pkgs) { + // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes). + private int idleOptimization(PackageManagerService pm, ArraySet pkgs, Context context) { Log.i(TAG, "Performing idle optimizations"); // If post-boot update is still running, request that it exits early. mExitPostBootUpdate.set(true); - mAbortIdleOptimization.set(false); - final long lowThreshold = getLowStorageThreshold(); - for (String pkg : pkgs) { - if (mAbortIdleOptimization.get()) { - // JobScheduler requested an early abort. - return; + long lowStorageThreshold = getLowStorageThreshold(context); + // Optimize primary apks. + int result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ true, + sFailedPackageNamesPrimary); + + if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) { + return result; + } + + if (SystemProperties.getBoolean("dalvik.vm.deopt.secondary", false)) { + result = reconcileSecondaryDexFiles(pm.getDexManager()); + if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) { + return result; } - synchronized (sFailedPackageNames) { - if (sFailedPackageNames.contains(pkg)) { + result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false, + sFailedPackageNamesSecondary); + } + return result; + } + + private int optimizePackages(PackageManagerService pm, ArraySet pkgs, + long lowStorageThreshold, boolean is_for_primary_dex, + ArraySet failedPackageNames) { + for (String pkg : pkgs) { + int abort_code = abortIdleOptimizations(lowStorageThreshold); + if (abort_code != OPTIMIZE_CONTINUE) { + return abort_code; + } + + synchronized (failedPackageNames) { + if (failedPackageNames.contains(pkg)) { // Skip previously failing package continue; + } else { + // Conservatively add package to the list of failing ones in case performDexOpt + // never returns. + failedPackageNames.add(pkg); } } - long usableSpace = mDataDir.getUsableSpace(); - if (usableSpace < lowThreshold) { - // Rather bail than completely fill up the disk. - Log.w(TAG, "Aborting background dex opt job due to low storage: " + - usableSpace); - break; - } - - // Conservatively add package to the list of failing ones in case performDexOpt - // never returns. - synchronized (sFailedPackageNames) { - sFailedPackageNames.add(pkg); - } // Optimize package if needed. Note that there can be no race between // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. - if (pm.performDexOpt(pkg, - /* checkProfiles */ true, - PackageManagerService.REASON_BACKGROUND_DEXOPT, - /* force */ false)) { + boolean success = is_for_primary_dex + ? pm.performDexOpt(pkg, + /* checkProfiles */ true, + PackageManagerService.REASON_BACKGROUND_DEXOPT, + /* force */ false) + : pm.performDexOptSecondary(pkg, + PackageManagerServiceCompilerMapping.getFullCompilerFilter(), + /* force */ true); + if (success) { // Dexopt succeeded, remove package from the list of failing ones. - synchronized (sFailedPackageNames) { - sFailedPackageNames.remove(pkg); + synchronized (failedPackageNames) { + failedPackageNames.remove(pkg); } } } - // Ran to completion, so we abandon our timeslice and do not reschedule. - jobFinished(jobParams, /* reschedule */ false); + return OPTIMIZE_PROCESSED; + } + + private int reconcileSecondaryDexFiles(DexManager dm) { + // TODO(calin): should we blacklist packages for which we fail to reconcile? + for (String p : dm.getAllPackagesWithSecondaryDexFiles()) { + if (mAbortIdleOptimization.get()) { + return OPTIMIZE_ABORT_BY_JOB_SCHEDULER; + } + dm.reconcileSecondaryDexFiles(p); + } + return OPTIMIZE_PROCESSED; + } + + // Evaluate whether or not idle optimizations should continue. + private int abortIdleOptimizations(long lowStorageThreshold) { + if (mAbortIdleOptimization.get()) { + // JobScheduler requested an early abort. + return OPTIMIZE_ABORT_BY_JOB_SCHEDULER; + } + long usableSpace = mDataDir.getUsableSpace(); + if (usableSpace < lowStorageThreshold) { + // Rather bail than completely fill up the disk. + Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace); + return OPTIMIZE_ABORT_NO_SPACE_LEFT; + } + + return OPTIMIZE_CONTINUE; + } + + /** + * Execute the idle optimizations immediately. + */ + public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context) { + // Create a new object to make sure we don't interfere with the scheduled jobs. + // Note that this may still run at the same time with the job scheduled by the + // JobScheduler but the scheduler will not be able to cancel it. + BackgroundDexOptService bdos = new BackgroundDexOptService(); + int result = bdos.idleOptimization(pm, pm.getOptimizablePackages(), context); + return result == OPTIMIZE_PROCESSED; } @Override @@ -281,7 +361,7 @@ public class BackgroundDexOptService extends JobService { } final ArraySet pkgs = pm.getOptimizablePackages(); - if (pkgs == null || pkgs.isEmpty()) { + if (pkgs.isEmpty()) { if (DEBUG_DEXOPT) { Log.i(TAG, "No packages to optimize"); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 6180c3ba2ea19..4a426bd6539a5 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -8245,6 +8245,20 @@ public class PackageManagerService extends IPackageManager.Stub { mDexManager.reconcileSecondaryDexFiles(packageName); } + // TODO(calin): this is only needed for BackgroundDexOptService. Find a cleaner way to inject + // a reference there. + /*package*/ DexManager getDexManager() { + return mDexManager; + } + + /** + * Execute the background dexopt job immediately. + */ + @Override + public boolean runBackgroundDexoptJob() { + return BackgroundDexOptService.runIdleOptimizationsNow(this, mContext); + } + List findSharedNonSystemLibraries(PackageParser.Package p) { if (p.usesLibraries != null || p.usesOptionalLibraries != null || p.usesStaticLibraries != null) { diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index a179a6dc3f49b..1203e4d58c004 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -120,6 +120,8 @@ class PackageManagerShellCommand extends ShellCommand { return runCompile(); case "reconcile-secondary-dex-files": return runreconcileSecondaryDexFiles(); + case "bg-dexopt-job": + return runDexoptJob(); case "dump-profiles": return runDumpProfiles(); case "list": @@ -449,6 +451,11 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + private int runDexoptJob() throws RemoteException { + boolean result = mInterface.runBackgroundDexoptJob(); + return result ? 0 : -1; + } + private int runDumpProfiles() throws RemoteException { String packageName = getNextArg(); mInterface.dumpProfiles(packageName); @@ -1530,7 +1537,13 @@ class PackageManagerShellCommand extends ShellCommand { } pw.println(" --reset: restore package to its post-install state"); pw.println(" --check-prof (true | false): look at profiles when doing dexopt?"); - pw.println(" --secondary-dex: copmile app secondary dex files"); + pw.println(" --secondary-dex: compile app secondary dex files"); + pw.println(" bg-dexopt-job"); + pw.println(" Execute the background optimizations immediately."); + pw.println(" Note that the command only runs the background optimizer logic. It may"); + pw.println(" overlap with the actual job but the job scheduler will not be able to"); + pw.println(" cancel it. It will also run even if the device is not in the idle"); + pw.println(" maintenance mode."); pw.println(" list features"); pw.println(" Prints all features of the system."); pw.println(" list instrumentation [-f] [TARGET-PACKAGE]"); diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index 28bf58fdfbef8..00f3711c70387 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -291,6 +291,7 @@ public class DexManager { return; } Set dexFilesToRemove = new HashSet<>(); + boolean updated = false; for (Map.Entry entry : useInfo.getDexUseInfoMap().entrySet()) { String dexPath = entry.getKey(); DexUseInfo dexUseInfo = entry.getValue(); @@ -311,7 +312,8 @@ public class DexManager { Slog.d(TAG, "Could not find package when compiling secondary dex " + packageName + " for user " + dexUseInfo.getOwnerUserId()); // Update the usage and continue, another user might still have the package. - mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId()); + updated = mPackageDexUsage.removeUserPackage( + packageName, dexUseInfo.getOwnerUserId()) || updated; continue; } ApplicationInfo info = pkg.applicationInfo; @@ -322,7 +324,8 @@ public class DexManager { flags |= StorageManager.FLAG_STORAGE_CE; } else { Slog.e(TAG, "Could not infer CE/DE storage for package " + info.packageName); - mPackageDexUsage.removeUserPackage(packageName, dexUseInfo.getOwnerUserId()); + updated = mPackageDexUsage.removeUserPackage( + packageName, dexUseInfo.getOwnerUserId()) || updated; continue; } @@ -338,9 +341,21 @@ public class DexManager { } } if (!dexStillExists) { - mPackageDexUsage.removeDexFile(packageName, dexPath, dexUseInfo.getOwnerUserId()); + updated = mPackageDexUsage.removeDexFile( + packageName, dexPath, dexUseInfo.getOwnerUserId()) || updated; } + } + if (updated) { + mPackageDexUsage.maybeWriteAsync(); + } + } + + /** + * Return all packages that contain records of secondary dex files. + */ + public Set getAllPackagesWithSecondaryDexFiles() { + return mPackageDexUsage.getAllPackagesWithSecondaryDexFiles(); } /** diff --git a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java index 253d0e97f51e4..3693bce04eb10 100644 --- a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java +++ b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java @@ -378,6 +378,8 @@ public class PackageDexUsage extends AbstractStatsBase { /** * Remove all the records about package {@code packageName} belonging to user {@code userId}. + * @return true if the record was found and actually deleted, + * false if the record doesn't exist */ public boolean removeUserPackage(String packageName, int userId) { synchronized (mPackageUseInfoMap) { @@ -402,6 +404,8 @@ public class PackageDexUsage extends AbstractStatsBase { /** * Remove the secondary dex file record belonging to the package {@code packageName} * and user {@code userId}. + * @return true if the record was found and actually deleted, + * false if the record doesn't exist */ public boolean removeDexFile(String packageName, String dexFile, int userId) { synchronized (mPackageUseInfoMap) { @@ -437,6 +441,21 @@ public class PackageDexUsage extends AbstractStatsBase { } } + /** + * Return all packages that contain records of secondary dex files. + */ + public Set getAllPackagesWithSecondaryDexFiles() { + Set packages = new HashSet<>(); + synchronized (mPackageUseInfoMap) { + for (Map.Entry entry : mPackageUseInfoMap.entrySet()) { + if (!entry.getValue().mDexUseInfoMap.isEmpty()) { + packages.add(entry.getKey()); + } + } + } + return packages; + } + public void clear() { synchronized (mPackageUseInfoMap) { mPackageUseInfoMap.clear();