diff options
| author | 2021-10-18 18:51:00 +0000 | |
|---|---|---|
| committer | 2021-10-18 18:51:00 +0000 | |
| commit | 0cc800c66ea45f98a0bc4246c26d3ff0859d6464 (patch) | |
| tree | 09e986232cf1e56cea9c03613d42b4d8764ec190 | |
| parent | 073f23ece58db2a95c40c22fd5dfda4574904758 (diff) | |
| parent | f992cb310195fe4b9cc942daca964ea229df9cb1 (diff) | |
Merge "add cancellation to background dexopt"
12 files changed, 703 insertions, 407 deletions
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 7ac385beffc3..46f179797e4a 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -599,12 +599,6 @@ interface IPackageManager { void forceDexOpt(String packageName); /** - * Execute the background dexopt job immediately on packages in packageNames. - * If null, then execute on all packages. - */ - boolean runBackgroundDexoptJob(in List<String> packageNames); - - /** * 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 * deleted, update the internal records and delete the generated oat files. diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index d0910fcfc836..5a5337449b79 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6311,7 +6311,7 @@ android:permission="android.permission.BIND_JOB_SERVICE" > </service> - <service android:name="com.android.server.pm.BackgroundDexOptService" + <service android:name="com.android.server.pm.BackgroundDexOptJobService" android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE"> </service> diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptJobService.java b/services/core/java/com/android/server/pm/BackgroundDexOptJobService.java new file mode 100644 index 000000000000..d9452742f99c --- /dev/null +++ b/services/core/java/com/android/server/pm/BackgroundDexOptJobService.java @@ -0,0 +1,37 @@ +/* + * 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.server.pm; + +import android.app.job.JobParameters; +import android.app.job.JobService; + +/** + * JobService to run background dex optimization. This is a thin wrapper and most logic exits in + * {@link BackgroundDexOptService}. + */ +public final class BackgroundDexOptJobService extends JobService { + + @Override + public boolean onStartJob(JobParameters params) { + return BackgroundDexOptService.getService().onStartJob(this, params); + } + + @Override + public boolean onStopJob(JobParameters params) { + return BackgroundDexOptService.getService().onStopJob(this, params); + } +} diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index 49a0a8827699..44e8e6646179 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -18,11 +18,11 @@ package com.android.server.pm; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; +import android.annotation.IntDef; import android.annotation.Nullable; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; -import android.app.job.JobService; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -30,63 +30,88 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageInfo; import android.os.BatteryManagerInternal; +import android.os.Binder; import android.os.Environment; import android.os.IThermalService; import android.os.PowerManager; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; +import android.os.Trace; import android.os.UserHandle; import android.os.storage.StorageManager; import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; +import com.android.internal.util.IndentingPrintWriter; import com.android.server.LocalServices; import com.android.server.PinnerService; +import com.android.server.pm.PackageDexOptimizer.DexOptResult; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.DexoptOptions; +import com.android.server.utils.TimingsTraceAndSlog; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; /** - * {@hide} + * Controls background dex optimization run as idle job or command line. */ -public class BackgroundDexOptService extends JobService { +public final class BackgroundDexOptService { private static final String TAG = "BackgroundDexOptService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final int JOB_IDLE_OPTIMIZE = 800; - private static final int JOB_POST_BOOT_UPDATE = 801; + @VisibleForTesting + static final int JOB_IDLE_OPTIMIZE = 800; + @VisibleForTesting + static final int JOB_POST_BOOT_UPDATE = 801; private static final long IDLE_OPTIMIZATION_PERIOD = TimeUnit.DAYS.toMillis(1); - private static ComponentName sDexoptServiceName = new ComponentName( - "android", - BackgroundDexOptService.class.getName()); + private static final long CANCELLATION_WAIT_CHECK_INTERVAL_MS = 200; - // Possible return codes of individual optimization steps. + private static ComponentName sDexoptServiceName = new ComponentName("android", + BackgroundDexOptJobService.class.getName()); - // 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; - // Optimizations should be aborted. Thermal throttling level too high. - private static final int OPTIMIZE_ABORT_THERMAL = 4; + // Possible return codes of individual optimization steps. + /** Ok status: Optimizations finished, All packages were processed, can continue */ + private static final int STATUS_OK = 0; + /** Optimizations should be aborted. Job scheduler requested it. */ + private static final int STATUS_ABORT_BY_CANCELLATION = 1; + /** Optimizations should be aborted. No space left on device. */ + private static final int STATUS_ABORT_NO_SPACE_LEFT = 2; + /** Optimizations should be aborted. Thermal throttling level too high. */ + private static final int STATUS_ABORT_THERMAL = 3; + /** Battery level too low */ + private static final int STATUS_ABORT_BATTERY = 4; + /** {@link PackageDexOptimizer#DEX_OPT_FAILED} case */ + private static final int STATUS_DEX_OPT_FAILED = 5; + + @IntDef(prefix = {"STATUS_"}, value = { + STATUS_OK, + STATUS_ABORT_BY_CANCELLATION, + STATUS_ABORT_NO_SPACE_LEFT, + STATUS_ABORT_THERMAL, + STATUS_ABORT_BATTERY, + STATUS_DEX_OPT_FAILED, + }) + @Retention(RetentionPolicy.SOURCE) + private @interface Status { + } // Used for calculating space threshold for downgrading unused apps. private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2; @@ -94,213 +119,407 @@ public class BackgroundDexOptService extends JobService { // Thermal cutoff value used if one isn't defined by a system property. private static final int THERMAL_CUTOFF_DEFAULT = PowerManager.THERMAL_STATUS_MODERATE; + private final Injector mInjector; + + private final Object mLock = new Object(); + + // Thread currently running dexopt. This will be null if dexopt is not running. + // The thread running dexopt make sure to set this into null when the pending dexopt is + // completed. + @GuardedBy("mLock") + @Nullable + private Thread mDexOptThread; + + // Thread currently cancelling dexopt. This thread is in blocked wait state until + // cancellation is done. Only this thread can change states for control. The other threads, if + // need to wait for cancellation, should just wait without doing any control. + @GuardedBy("mLock") + @Nullable + private Thread mDexOptCancellingThread; + + // Tells whether post boot update is completed or not. + @GuardedBy("mLock") + private boolean mFinishedPostBootUpdate; + + @GuardedBy("mLock") + @Status private int mLastExecutionStatus = STATUS_OK; + + // Keeps packages cancelled from PDO for last session. This is for debugging. + @GuardedBy("mLock") + private final ArraySet<String> mLastCancelledPackages = new ArraySet<String>(); + /** * Set of failed packages remembered across job runs. */ - static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>(); - static final ArraySet<String> sFailedPackageNamesSecondary = new ArraySet<String>(); + @GuardedBy("mLock") + private final ArraySet<String> mFailedPackageNamesPrimary = new ArraySet<String>(); + @GuardedBy("mLock") + private final ArraySet<String> mFailedPackageNamesSecondary = new ArraySet<String>(); - /** - * Atomics set to true if the JobScheduler requests an abort. - */ - private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false); - private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false); + private final long mDowngradeUnusedAppsThresholdInMillis; - /** - * Atomic set to true if one job should exit early because another job was started. - */ - private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false); + private List<PackagesUpdatedListener> mPackagesUpdatedListeners = new ArrayList<>(); - private final File mDataDir = Environment.getDataDirectory(); - private static final long mDowngradeUnusedAppsThresholdInMillis = - getDowngradeUnusedAppsThresholdInMillis(); + private int mThermalStatusCutoff = THERMAL_CUTOFF_DEFAULT; - private final IThermalService mThermalService = - IThermalService.Stub.asInterface( - ServiceManager.getService(Context.THERMAL_SERVICE)); + /** Listener for monitoring package change due to dexopt. */ + public interface PackagesUpdatedListener { + /** Called when the packages are updated through dexopt */ + void onPackagesUpdated(ArraySet<String> updatedPackages); + } - private static List<PackagesUpdatedListener> sPackagesUpdatedListeners = new ArrayList<>(); + public BackgroundDexOptService(Context context, DexManager dexManager) { + this(new Injector(context, dexManager)); + } - private int mThermalStatusCutoff = THERMAL_CUTOFF_DEFAULT; + @VisibleForTesting + public BackgroundDexOptService(Injector injector) { + mInjector = injector; + LocalServices.addService(BackgroundDexOptService.class, this); + mDowngradeUnusedAppsThresholdInMillis = mInjector.getDowngradeUnusedAppsThresholdInMillis(); + } - public static void schedule(Context context) { - if (isBackgroundDexoptDisabled()) { + /** Start scheduling job after boot completion */ + public void systemReady() { + if (mInjector.isBackgroundDexOptDisabled()) { return; } - final JobScheduler js = context.getSystemService(JobScheduler.class); - - // Schedule a one-off job which scans installed packages and updates - // out-of-date oat files. Schedule it 10 minutes after the boot complete event, - // so that we don't overload the boot with additional dex2oat compilations. - context.registerReceiver(new BroadcastReceiver() { + mInjector.getContext().registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName) - .setMinimumLatency(TimeUnit.MINUTES.toMillis(10)) - .setOverrideDeadline(TimeUnit.MINUTES.toMillis(60)) - .build()); - context.unregisterReceiver(this); + mInjector.getContext().unregisterReceiver(this); + // queue both job. JOB_IDLE_OPTIMIZE will not start until JOB_POST_BOOT_UPDATE is + // completed. + scheduleAJob(JOB_POST_BOOT_UPDATE); + scheduleAJob(JOB_IDLE_OPTIMIZE); if (DEBUG) { - Slog.i(TAG, "BootBgDexopt scheduled"); + Slog.d(TAG, "BootBgDexopt scheduled"); } } }, new IntentFilter(Intent.ACTION_BOOT_COMPLETED)); + } - // Schedule a daily job which scans installed packages and compiles - // those with fresh profiling data. - js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName) - .setRequiresDeviceIdle(true) - .setRequiresCharging(true) - .setPeriodic(IDLE_OPTIMIZATION_PERIOD) - .build()); - - if (DEBUG) { - Slog.d(TAG, "BgDexopt scheduled"); + /** Dump the current state */ + public void dump(IndentingPrintWriter writer) { + boolean disabled = mInjector.isBackgroundDexOptDisabled(); + writer.print("enabled:"); + writer.println(!disabled); + if (disabled) { + return; + } + synchronized (mLock) { + writer.print("mDexOptThread:"); + writer.println(mDexOptThread); + writer.print("mDexOptCancellingThread:"); + writer.println(mDexOptCancellingThread); + writer.print("mFinishedPostBootUpdate:"); + writer.print(mFinishedPostBootUpdate); + writer.print(",mLastExecutionStatus:"); + writer.println(mLastExecutionStatus); + writer.print("mLastCancelledPackages:"); + writer.println(String.join(",", mLastCancelledPackages)); + writer.print("mFailedPackageNamesPrimary:"); + writer.println(String.join(",", mFailedPackageNamesPrimary)); + writer.print("mFailedPackageNamesSecondary:"); + writer.println(String.join(",", mFailedPackageNamesSecondary)); } } - public static void notifyPackageChanged(String packageName) { - // 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 (sFailedPackageNamesPrimary) { - sFailedPackageNamesPrimary.remove(packageName); + /** Gets the instance of the service */ + public static BackgroundDexOptService getService() { + return LocalServices.getService(BackgroundDexOptService.class); + } + + /** + * Executes the background dexopt job immediately for selected packages or all packages. + * + * <p>This is only for shell command and only root or shell user can use this. + * + * @param packageNames dex optimize the passed packages or all packages if null + * + * @return true if dex optimization is complete. false if the task is cancelled or if there was + * an error. + */ + public boolean runBackgroundDexoptJob(@Nullable List<String> packageNames) { + enforceRootOrShell(); + long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + // Do not cancel and wait for completion if there is pending task. + waitForDexOptThreadToFinishLocked(); + resetStatesForNewDexOptRunLocked(Thread.currentThread()); + } + PackageManagerService pm = mInjector.getPackageManagerService(); + ArraySet<String> packagesToOptimize; + if (packageNames == null) { + packagesToOptimize = pm.getOptimizablePackages(); + } else { + packagesToOptimize = new ArraySet<>(packageNames); + } + return runIdleOptimization(pm, packagesToOptimize, /* isPostBootUpdate= */ false); + } finally { + Binder.restoreCallingIdentity(identity); + markDexOptCompleted(); } - synchronized (sFailedPackageNamesSecondary) { - sFailedPackageNamesSecondary.remove(packageName); + } + + /** + * Cancels currently running any idle optimization tasks started from JobScheduler + * or runIdleOptimizationsNow call. + * + * <p>This is only for shell command and only root or shell user can use this. + */ + public void cancelBackgroundDexoptJob() { + enforceRootOrShell(); + Binder.withCleanCallingIdentity(() -> cancelDexOptAndWaitForCompletion()); + } + + /** Adds listener for package update */ + public void addPackagesUpdatedListener(PackagesUpdatedListener listener) { + synchronized (mLock) { + mPackagesUpdatedListeners.add(listener); } } - private long getLowStorageThreshold(Context context) { - @SuppressWarnings("deprecation") - final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir); - if (lowThreshold == 0) { - Slog.e(TAG, "Invalid low storage threshold"); + /** Removes package update listener */ + public void removePackagesUpdatedListener(PackagesUpdatedListener listener) { + synchronized (mLock) { + mPackagesUpdatedListeners.remove(listener); } + } - return lowThreshold; + /** + * Notifies package change and removes the package from the failed package list so that + * the package can run dexopt again. + */ + public void notifyPackageChanged(String packageName) { + // The idle maintenance 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 (mLock) { + mFailedPackageNamesPrimary.remove(packageName); + mFailedPackageNamesSecondary.remove(packageName); + } } - private boolean runPostBootUpdate(final JobParameters jobParams, - final PackageManagerService pm, final ArraySet<String> pkgs) { - if (mExitPostBootUpdate.get()) { - // This job has already been superseded. Do not start it. + /** For BackgroundDexOptJobService to dispatch onStartJob event */ + /* package */ boolean onStartJob(BackgroundDexOptJobService job, JobParameters params) { + Slog.i(TAG, "onStartJob:" + params.getJobId()); + + boolean isPostBootUpdateJob = params.getJobId() == JOB_POST_BOOT_UPDATE; + // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from + // the checks above. This check is not "live" - the value is determined by a background + // restart with a period of ~1 minute. + PackageManagerService pm = mInjector.getPackageManagerService(); + if (pm.isStorageLow()) { + Slog.w(TAG, "Low storage, skipping this run"); + markPostBootUpdateCompleted(params); + return false; + } + + ArraySet<String> pkgs = pm.getOptimizablePackages(); + if (pkgs.isEmpty()) { + Slog.i(TAG, "No packages to optimize"); + markPostBootUpdateCompleted(params); return false; } - new Thread("BackgroundDexOptService_PostBootUpdate") { - @Override - public void run() { - postBootUpdate(jobParams, pm, pkgs); - } - }.start(); + mThermalStatusCutoff = mInjector.getDexOptThermalCutoff(); + + synchronized (mLock) { + if (mDexOptThread != null && mDexOptThread.isAlive()) { + // Other task is already running. + return false; + } + if (!isPostBootUpdateJob && !mFinishedPostBootUpdate) { + // Post boot job not finished yet. Run post boot job first. + return false; + } + resetStatesForNewDexOptRunLocked(mInjector.createAndStartThread( + "BackgroundDexOptService_" + (isPostBootUpdateJob ? "PostBoot" : "Idle"), + () -> { + TimingsTraceAndSlog tr = new TimingsTraceAndSlog(TAG, + Trace.TRACE_TAG_PACKAGE_MANAGER); + tr.traceBegin("jobExecution"); + boolean completed = false; + try { + completed = runIdleOptimization(pm, pkgs, + params.getJobId() == JOB_POST_BOOT_UPDATE); + } finally { // Those cleanup should be done always. + tr.traceEnd(); + Slog.i(TAG, "dexopt finishing. jobid:" + params.getJobId() + + " completed:" + completed); + + if (params.getJobId() == JOB_POST_BOOT_UPDATE) { + if (completed) { + markPostBootUpdateCompleted(params); + } + // Reschedule when cancelled + job.jobFinished(params, !completed); + } else { + // Periodic job + job.jobFinished(params, true); + } + markDexOptCompleted(); + } + })); + } return true; } - private void postBootUpdate(JobParameters jobParams, PackageManagerService pm, - ArraySet<String> pkgs) { - final BatteryManagerInternal batteryManagerInternal = - LocalServices.getService(BatteryManagerInternal.class); - final long lowThreshold = getLowStorageThreshold(this); - - mAbortPostBootUpdate.set(false); + /** For BackgroundDexOptJobService to dispatch onStopJob event */ + /* package */ boolean onStopJob(BackgroundDexOptJobService job, JobParameters params) { + Slog.i(TAG, "onStopJob:" + params.getJobId()); + // This cannot block as it is in main thread, thus dispatch to a newly created thread and + // cancel it from there. + // As this event does not happen often, creating a new thread is justified rather than + // having one thread kept permanently. + mInjector.createAndStartThread("DexOptCancel", this::cancelDexOptAndWaitForCompletion); + // Always reschedule for cancellation. + return true; + } - ArraySet<String> updatedPackages = new ArraySet<>(); - for (String pkg : pkgs) { - if (mAbortPostBootUpdate.get()) { - // JobScheduler requested an early abort. + /** + * Cancels pending dexopt and wait for completion of the cancellation. This can block the caller + * until cancellation is done. + */ + private void cancelDexOptAndWaitForCompletion() { + synchronized (mLock) { + if (mDexOptThread == null) { return; } - if (mExitPostBootUpdate.get()) { - // Different job, which supersedes this one, is running. - break; - } - if (batteryManagerInternal.getBatteryLevelLow()) { - // Rather bail than completely drain the battery. - break; + if (mDexOptCancellingThread != null && mDexOptCancellingThread.isAlive()) { + // No control, just wait + waitForDexOptThreadToFinishLocked(); + // Do not wait for other cancellation's complete. That will be handled by the next + // start flow. + return; } - long usableSpace = mDataDir.getUsableSpace(); - if (usableSpace < lowThreshold) { - // Rather bail than completely fill up the disk. - Slog.w(TAG, "Aborting background dex opt job due to low storage: " + - usableSpace); - break; + mDexOptCancellingThread = Thread.currentThread(); + // Take additional caution to make sure that we do not leave this call + // with controlDexOptBlockingLocked(true) state. + try { + controlDexOptBlockingLocked(true); + waitForDexOptThreadToFinishLocked(); + } finally { + // Reset to default states regardless of previous states + mDexOptCancellingThread = null; + mDexOptThread = null; + controlDexOptBlockingLocked(false); + mLock.notifyAll(); } - if (DEBUG) { - Slog.i(TAG, "Updating package " + pkg); + } + } + + @GuardedBy("mLock") + private void waitForDexOptThreadToFinishLocked() { + TimingsTraceAndSlog tr = new TimingsTraceAndSlog(TAG, Trace.TRACE_TAG_PACKAGE_MANAGER); + tr.traceBegin("waitForDexOptThreadToFinishLocked"); + try { + // Wait but check in regular internal to see if the thread is still alive. + while (mDexOptThread != null && mDexOptThread.isAlive()) { + mLock.wait(CANCELLATION_WAIT_CHECK_INTERVAL_MS); } + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupted while waiting for dexopt thread"); + Thread.currentThread().interrupt(); + } + tr.traceEnd(); + } - // Update package if needed. Note that there can be no race between concurrent - // jobs because PackageDexOptimizer.performDexOpt is synchronized. - - // checkProfiles is false to avoid merging profiles during boot which - // might interfere with background compilation (b/28612421). - // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will - // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a - // trade-off worth doing to save boot time work. - int result = pm.performDexOptWithStatus(new DexoptOptions( - pkg, - PackageManagerService.REASON_POST_BOOT, - DexoptOptions.DEXOPT_BOOT_COMPLETE)); - if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { - updatedPackages.add(pkg); + private void markDexOptCompleted() { + synchronized (mLock) { + if (mDexOptThread != Thread.currentThread()) { + throw new IllegalStateException( + "Only mDexOptThread can mark completion, mDexOptThread:" + mDexOptThread + + " current:" + Thread.currentThread()); } + mDexOptThread = null; + // Other threads may be waiting for completion. + mLock.notifyAll(); } - notifyPinService(updatedPackages); - notifyPackagesUpdated(updatedPackages); - // Ran to completion, so we abandon our timeslice and do not reschedule. - jobFinished(jobParams, /* reschedule */ false); } - private boolean runIdleOptimization(final JobParameters jobParams, - final PackageManagerService pm, final ArraySet<String> pkgs) { - new Thread("BackgroundDexOptService_IdleOptimization") { - @Override - public void run() { - int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this); - if (result == OPTIMIZE_PROCESSED) { - Slog.i(TAG, "Idle optimizations completed."); - } else if (result == OPTIMIZE_ABORT_NO_SPACE_LEFT) { - Slog.w(TAG, "Idle optimizations aborted because of space constraints."); - } else if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) { - Slog.w(TAG, "Idle optimizations aborted by job scheduler."); - } else if (result == OPTIMIZE_ABORT_THERMAL) { - Slog.w(TAG, "Idle optimizations aborted by thermal throttling."); - } else { - Slog.w(TAG, "Idle optimizations ended with unexpected code: " + result); - } + @GuardedBy("mLock") + private void resetStatesForNewDexOptRunLocked(Thread thread) { + mDexOptThread = thread; + mLastCancelledPackages.clear(); + controlDexOptBlockingLocked(false); + } - if (result == OPTIMIZE_ABORT_THERMAL) { - // Abandon our timeslice and reschedule - jobFinished(jobParams, /* wantsReschedule */ true); - } else if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) { - // Abandon our timeslice and do not reschedule. - jobFinished(jobParams, /* wantsReschedule */ false); - } - } - }.start(); - return true; + private void enforceRootOrShell() { + int uid = Binder.getCallingUid(); + if (uid != Process.ROOT_UID && uid != Process.SHELL_UID) { + throw new SecurityException("Should be shell or root user"); + } } - // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes). - private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, - Context context) { - Slog.i(TAG, "Performing idle optimizations"); - // If post-boot update is still running, request that it exits early. - mExitPostBootUpdate.set(true); - mAbortIdleOptimization.set(false); + @GuardedBy("mLock") + private void controlDexOptBlockingLocked(boolean block) { + mInjector.getPackageManagerService().controlDexOptBlocking(block); + } - long lowStorageThreshold = getLowStorageThreshold(context); - int result = idleOptimizePackages(pm, pkgs, lowStorageThreshold); - return result; + private void scheduleAJob(int jobId) { + JobScheduler js = mInjector.getJobScheduler(); + JobInfo.Builder builder = new JobInfo.Builder(jobId, sDexoptServiceName) + .setRequiresDeviceIdle(true); + if (jobId == JOB_IDLE_OPTIMIZE) { + builder.setRequiresCharging(true) + .setPeriodic(IDLE_OPTIMIZATION_PERIOD); + } + js.schedule(builder.build()); } - /** - * Get the size of the directory. It uses recursion to go over all files. - * @param f - * @return - */ + private long getLowStorageThreshold() { + long lowThreshold = mInjector.getDataDirStorageLowBytes(); + if (lowThreshold == 0) { + Slog.e(TAG, "Invalid low storage threshold"); + } + + return lowThreshold; + } + + private void logStatus(int status) { + switch (status) { + case STATUS_OK: + Slog.i(TAG, "Idle optimizations completed."); + break; + case STATUS_ABORT_NO_SPACE_LEFT: + Slog.w(TAG, "Idle optimizations aborted because of space constraints."); + break; + case STATUS_ABORT_BY_CANCELLATION: + Slog.w(TAG, "Idle optimizations aborted by cancellation."); + break; + case STATUS_ABORT_THERMAL: + Slog.w(TAG, "Idle optimizations aborted by thermal throttling."); + break; + case STATUS_ABORT_BATTERY: + Slog.w(TAG, "Idle optimizations aborted by low battery."); + break; + case STATUS_DEX_OPT_FAILED: + Slog.w(TAG, "Idle optimizations failed from dexopt."); + break; + default: + Slog.w(TAG, "Idle optimizations ended with unexpected code: " + status); + break; + } + } + + /** Returns true if completed */ + private boolean runIdleOptimization(PackageManagerService pm, ArraySet<String> pkgs, + boolean isPostBootUpdate) { + long lowStorageThreshold = getLowStorageThreshold(); + int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, isPostBootUpdate); + logStatus(status); + synchronized (mLock) { + mLastExecutionStatus = status; + } + + return status == STATUS_OK; + } + + /** Gets the size of the directory. It uses recursion to go over all files. */ private long getDirectorySize(File f) { long size = 0; if (f.isDirectory()) { @@ -313,10 +532,7 @@ public class BackgroundDexOptService extends JobService { return size; } - /** - * Get the size of a package. - * @param pkg - */ + /** Gets the size of a package. */ private long getPackageSize(PackageManagerService pm, String pkg) { PackageInfo info = pm.getPackageInfo(pkg, 0, UserHandle.USER_SYSTEM); long size = 0; @@ -340,17 +556,18 @@ public class BackgroundDexOptService extends JobService { return 0; } + @Status private int idleOptimizePackages(PackageManagerService pm, ArraySet<String> pkgs, - long lowStorageThreshold) { + long lowStorageThreshold, boolean isPostBootUpdate) { ArraySet<String> updatedPackages = new ArraySet<>(); ArraySet<String> updatedPackagesDueToSecondaryDex = new ArraySet<>(); try { - final boolean supportSecondaryDex = supportSecondaryDex(); + boolean supportSecondaryDex = mInjector.supportSecondaryDex(); if (supportSecondaryDex) { - int result = reconcileSecondaryDexFiles(pm.getDexManager()); - if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) { + @Status int result = reconcileSecondaryDexFiles(); + if (result != STATUS_OK) { return result; } } @@ -358,7 +575,7 @@ public class BackgroundDexOptService extends JobService { // Only downgrade apps when space is low on device. // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean // up disk before user hits the actual lowStorageThreshold. - final long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE + long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE * lowStorageThreshold; boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade); if (DEBUG) { @@ -368,21 +585,33 @@ public class BackgroundDexOptService extends JobService { Set<String> unusedPackages = pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis); if (DEBUG) { - Slog.d(TAG, "Unsused Packages " + String.join(",", unusedPackages)); + Slog.d(TAG, "Unsused Packages " + String.join(",", unusedPackages)); } if (!unusedPackages.isEmpty()) { for (String pkg : unusedPackages) { - int abortCode = abortIdleOptimizations(/*lowStorageThreshold*/ -1); - if (abortCode != OPTIMIZE_CONTINUE) { + @Status int abortCode = abortIdleOptimizations(/*lowStorageThreshold*/ -1); + if (abortCode != STATUS_OK) { // Should be aborted by the scheduler. return abortCode; } - if (downgradePackage(pm, pkg, /*isForPrimaryDex*/ true)) { + @DexOptResult int downgradeResult = downgradePackage(pm, pkg, + /* isForPrimaryDex= */ true, isPostBootUpdate); + if (downgradeResult == PackageDexOptimizer.DEX_OPT_PERFORMED) { updatedPackages.add(pkg); } + @Status int status = convertPackageDexOptimizerStatusToInternal( + downgradeResult); + if (status != STATUS_OK) { + return status; + } if (supportSecondaryDex) { - downgradePackage(pm, pkg, /*isForPrimaryDex*/ false); + downgradeResult = downgradePackage(pm, pkg, + /* isForPrimaryDex= */false, isPostBootUpdate); + status = convertPackageDexOptimizerStatusToInternal(downgradeResult); + if (status != STATUS_OK) { + return status; + } } } @@ -391,18 +620,19 @@ public class BackgroundDexOptService extends JobService { } } - int primaryResult = optimizePackages(pm, pkgs, lowStorageThreshold, - /*isForPrimaryDex*/ true, updatedPackages); - if (primaryResult != OPTIMIZE_PROCESSED) { + @Status int primaryResult = optimizePackages(pm, pkgs, lowStorageThreshold, + /*isForPrimaryDex=*/ true, updatedPackages, isPostBootUpdate); + if (primaryResult != STATUS_OK) { return primaryResult; } if (!supportSecondaryDex) { - return OPTIMIZE_PROCESSED; + return STATUS_OK; } - int secondaryResult = optimizePackages(pm, pkgs, lowStorageThreshold, - /*isForPrimaryDex*/ false, updatedPackagesDueToSecondaryDex); + @Status int secondaryResult = optimizePackages(pm, pkgs, lowStorageThreshold, + /*isForPrimaryDex*/ false, updatedPackagesDueToSecondaryDex, + isPostBootUpdate); return secondaryResult; } finally { // Always let the pinner service know about changes. @@ -414,21 +644,25 @@ public class BackgroundDexOptService extends JobService { } } + @Status private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs, - long lowStorageThreshold, boolean isForPrimaryDex, ArraySet<String> updatedPackages) { + long lowStorageThreshold, boolean isForPrimaryDex, ArraySet<String> updatedPackages, + boolean isPostBootUpdate) { for (String pkg : pkgs) { int abortCode = abortIdleOptimizations(lowStorageThreshold); - if (abortCode != OPTIMIZE_CONTINUE) { + if (abortCode != STATUS_OK) { // Either aborted by the scheduler or no space left. return abortCode; } - boolean dexOptPerformed = optimizePackage(pm, pkg, isForPrimaryDex); - if (dexOptPerformed) { + @DexOptResult int result = optimizePackage(pm, pkg, isForPrimaryDex, isPostBootUpdate); + if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { updatedPackages.add(pkg); + } else if (result != PackageDexOptimizer.DEX_OPT_SKIPPED) { + return convertPackageDexOptimizerStatusToInternal(result); } } - return OPTIMIZE_PROCESSED; + return STATUS_OK; } /** @@ -436,21 +670,25 @@ public class BackgroundDexOptService extends JobService { * eg. if the package is in speed-profile the package will be downgraded to verify. * @param pm PackageManagerService * @param pkg The package to be downgraded. - * @param isForPrimaryDex. Apps can have several dex file, primary and secondary. - * @return true if the package was downgraded. + * @param isForPrimaryDex Apps can have several dex file, primary and secondary. + * @return PackageDexOptimizer.DEX_* */ - private boolean downgradePackage(PackageManagerService pm, String pkg, - boolean isForPrimaryDex) { + @DexOptResult + private int downgradePackage(PackageManagerService pm, String pkg, + boolean isForPrimaryDex, boolean isPostBootUpdate) { if (DEBUG) { Slog.d(TAG, "Downgrading " + pkg); } - boolean dex_opt_performed = false; + if (isCancelling()) { + return PackageDexOptimizer.DEX_OPT_CANCELLED; + } int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE; - int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE - | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB - | DexoptOptions.DEXOPT_DOWNGRADE; + int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE | DexoptOptions.DEXOPT_DOWNGRADE; + if (!isPostBootUpdate) { + dexoptFlags |= DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB; + } long package_size_before = getPackageSize(pm, pkg); - + int result = PackageDexOptimizer.DEX_OPT_SKIPPED; if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) { // This applies for system apps or if packages location is not a directory, i.e. // monolithic install. @@ -459,32 +697,29 @@ public class BackgroundDexOptService extends JobService { // remove their compiler artifacts from dalvik cache. pm.deleteOatArtifactsOfPackage(pkg); } else { - dex_opt_performed = performDexOptPrimary(pm, pkg, reason, dexoptFlags); + result = performDexOptPrimary(pm, pkg, reason, dexoptFlags); } } else { - dex_opt_performed = performDexOptSecondary(pm, pkg, reason, dexoptFlags); + result = performDexOptSecondary(pm, pkg, reason, dexoptFlags); } - if (dex_opt_performed) { + if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before, getPackageSize(pm, pkg), /*aggressive=*/ false); } - return dex_opt_performed; - } - - private boolean supportSecondaryDex() { - return (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)); + return result; } - private int reconcileSecondaryDexFiles(DexManager dm) { + @Status + private int reconcileSecondaryDexFiles() { // TODO(calin): should we denylist packages for which we fail to reconcile? - for (String p : dm.getAllPackagesWithSecondaryDexFiles()) { - if (mAbortIdleOptimization.get()) { - return OPTIMIZE_ABORT_BY_JOB_SCHEDULER; + for (String p : mInjector.getDexManager().getAllPackagesWithSecondaryDexFiles()) { + if (isCancelling()) { + return STATUS_ABORT_BY_CANCELLATION; } - dm.reconcileSecondaryDexFiles(p); + mInjector.getDexManager().reconcileSecondaryDexFiles(p); } - return OPTIMIZE_PROCESSED; + return STATUS_OK; } /** @@ -493,39 +728,46 @@ public class BackgroundDexOptService extends JobService { * concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. * @param pm An instance of PackageManagerService * @param pkg The package to be downgraded. - * @param isForPrimaryDex. Apps can have several dex file, primary and secondary. - * @return true if the package was downgraded. + * @param isForPrimaryDex Apps can have several dex file, primary and secondary. + * @param isPostBootUpdate is post boot update or not. + * @return PackageDexOptimizer#DEX_OPT_* */ - private boolean optimizePackage(PackageManagerService pm, String pkg, - boolean isForPrimaryDex) { - int reason = PackageManagerService.REASON_BACKGROUND_DEXOPT; - int dexoptFlags = DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES - | DexoptOptions.DEXOPT_BOOT_COMPLETE - | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB; + @DexOptResult + private int optimizePackage(PackageManagerService pm, String pkg, + boolean isForPrimaryDex, boolean isPostBootUpdate) { + int reason = isPostBootUpdate ? PackageManagerService.REASON_POST_BOOT + : PackageManagerService.REASON_BACKGROUND_DEXOPT; + int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE; + if (!isPostBootUpdate) { + dexoptFlags |= DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES + | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB; + } // System server share the same code path as primary dex files. // PackageManagerService will select the right optimization path for it. - return (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) - ? performDexOptPrimary(pm, pkg, reason, dexoptFlags) - : performDexOptSecondary(pm, pkg, reason, dexoptFlags); + if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) { + return performDexOptPrimary(pm, pkg, reason, dexoptFlags); + } else { + return performDexOptSecondary(pm, pkg, reason, dexoptFlags); + } } - private boolean performDexOptPrimary(PackageManagerService pm, String pkg, int reason, + @DexOptResult + private int performDexOptPrimary(PackageManagerService pm, String pkg, int reason, int dexoptFlags) { - int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false, + return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true, () -> pm.performDexOptWithStatus(new DexoptOptions(pkg, reason, dexoptFlags))); - return result == PackageDexOptimizer.DEX_OPT_PERFORMED; } - private boolean performDexOptSecondary(PackageManagerService pm, String pkg, int reason, + @DexOptResult + private int performDexOptSecondary(PackageManagerService pm, String pkg, int reason, int dexoptFlags) { DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX); - int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true, + return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false, () -> pm.performDexOpt(dexoptOptions) ? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED ); - return result == PackageDexOptimizer.DEX_OPT_PERFORMED; } /** @@ -533,194 +775,212 @@ public class BackgroundDexOptService extends JobService { * the package is added to the list of failed packages. * Return one of following result: * {@link PackageDexOptimizer#DEX_OPT_SKIPPED} + * {@link PackageDexOptimizer#DEX_OPT_CANCELLED} * {@link PackageDexOptimizer#DEX_OPT_PERFORMED} * {@link PackageDexOptimizer#DEX_OPT_FAILED} */ + @DexOptResult private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex, Supplier<Integer> performDexOptWrapper) { - ArraySet<String> sFailedPackageNames = - isForPrimaryDex ? sFailedPackageNamesPrimary : sFailedPackageNamesSecondary; - synchronized (sFailedPackageNames) { - if (sFailedPackageNames.contains(pkg)) { + ArraySet<String> failedPackageNames; + synchronized (mLock) { + failedPackageNames = + isForPrimaryDex ? mFailedPackageNamesPrimary : mFailedPackageNamesSecondary; + if (failedPackageNames.contains(pkg)) { // Skip previously failing package return PackageDexOptimizer.DEX_OPT_SKIPPED; } - sFailedPackageNames.add(pkg); } int result = performDexOptWrapper.get(); - if (result != PackageDexOptimizer.DEX_OPT_FAILED) { - synchronized (sFailedPackageNames) { - sFailedPackageNames.remove(pkg); + if (result == PackageDexOptimizer.DEX_OPT_FAILED) { + synchronized (mLock) { + failedPackageNames.add(pkg); + } + } else if (result == PackageDexOptimizer.DEX_OPT_CANCELLED) { + synchronized (mLock) { + mLastCancelledPackages.add(pkg); } } return result; } - // Evaluate whether or not idle optimizations should continue. + @Status + private int convertPackageDexOptimizerStatusToInternal(@DexOptResult int pdoStatus) { + switch (pdoStatus) { + case PackageDexOptimizer.DEX_OPT_CANCELLED: + return STATUS_ABORT_BY_CANCELLATION; + case PackageDexOptimizer.DEX_OPT_FAILED: + return STATUS_DEX_OPT_FAILED; + case PackageDexOptimizer.DEX_OPT_PERFORMED: + case PackageDexOptimizer.DEX_OPT_SKIPPED: + return STATUS_OK; + default: + Slog.e(TAG, "Unkknown error code from PackageDexOptimizer:" + pdoStatus, + new RuntimeException()); + return STATUS_DEX_OPT_FAILED; + } + } + + /** Evaluate whether or not idle optimizations should continue. */ + @Status private int abortIdleOptimizations(long lowStorageThreshold) { - if (mAbortIdleOptimization.get()) { + if (isCancelling()) { // JobScheduler requested an early abort. - return OPTIMIZE_ABORT_BY_JOB_SCHEDULER; + return STATUS_ABORT_BY_CANCELLATION; } // Abort background dexopt if the device is in a moderate or stronger thermal throttling // state. - try { - final int thermalStatus = mThermalService.getCurrentThermalStatus(); - - if (DEBUG) { - Log.i(TAG, "Thermal throttling status during bgdexopt: " + thermalStatus); - } + int thermalStatus = mInjector.getCurrentThermalStatus(); + if (DEBUG) { + Log.d(TAG, "Thermal throttling status during bgdexopt: " + thermalStatus); + } + if (thermalStatus >= mThermalStatusCutoff) { + return STATUS_ABORT_THERMAL; + } - if (thermalStatus >= mThermalStatusCutoff) { - return OPTIMIZE_ABORT_THERMAL; - } - } catch (RemoteException ex) { - // Because this is a intra-process Binder call it is impossible for a RemoteException - // to be raised. + if (mInjector.isBatteryLevelLow()) { + return STATUS_ABORT_BATTERY; } - long usableSpace = mDataDir.getUsableSpace(); + long usableSpace = mInjector.getDataDirUsableSpace(); if (usableSpace < lowStorageThreshold) { // Rather bail than completely fill up the disk. Slog.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace); - return OPTIMIZE_ABORT_NO_SPACE_LEFT; + return STATUS_ABORT_NO_SPACE_LEFT; } - return OPTIMIZE_CONTINUE; + return STATUS_OK; } // Evaluate whether apps should be downgraded. private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) { - long usableSpace = mDataDir.getUsableSpace(); - if (usableSpace < lowStorageThresholdForDowngrade) { + if (mInjector.getDataDirUsableSpace() < lowStorageThresholdForDowngrade) { return true; } return false; } - /** - * Execute idle optimizations immediately on packages in packageNames. If packageNames is null, - * then execute on all packages. - */ - public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context, - @Nullable List<String> packageNames) { - // 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(); - ArraySet<String> packagesToOptimize; - if (packageNames == null) { - packagesToOptimize = pm.getOptimizablePackages(); - } else { - packagesToOptimize = new ArraySet<>(packageNames); + private boolean isCancelling() { + synchronized (mLock) { + return mDexOptCancellingThread != null; } - int result = bdos.idleOptimization(pm, packagesToOptimize, context); - return result == OPTIMIZE_PROCESSED; } - @Override - public boolean onStartJob(JobParameters params) { - if (DEBUG) { - Slog.i(TAG, "onStartJob"); + private void markPostBootUpdateCompleted(JobParameters params) { + if (params.getJobId() != JOB_POST_BOOT_UPDATE) { + return; } + synchronized (mLock) { + if (!mFinishedPostBootUpdate) { + mFinishedPostBootUpdate = true; + JobScheduler js = mInjector.getJobScheduler(); + js.cancel(JOB_POST_BOOT_UPDATE); + } + } + } - // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from - // the checks above. This check is not "live" - the value is determined by a background - // restart with a period of ~1 minute. - PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package"); - if (pm.isStorageLow()) { - Slog.i(TAG, "Low storage, skipping this run"); - return false; + private void notifyPinService(ArraySet<String> updatedPackages) { + PinnerService pinnerService = mInjector.getPinnerService(); + if (pinnerService != null) { + Slog.i(TAG, "Pinning optimized code " + updatedPackages); + pinnerService.update(updatedPackages, false /* force */); } + } - final ArraySet<String> pkgs = pm.getOptimizablePackages(); - if (pkgs.isEmpty()) { - Slog.i(TAG, "No packages to optimize"); - return false; + /** Notify all listeners (#addPackagesUpdatedListener) that packages have been updated. */ + private void notifyPackagesUpdated(ArraySet<String> updatedPackages) { + synchronized (mLock) { + for (PackagesUpdatedListener listener : mPackagesUpdatedListeners) { + listener.onPackagesUpdated(updatedPackages); + } } + } - mThermalStatusCutoff = - SystemProperties.getInt("dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT); + /** Injector pattern for testing purpose */ + @VisibleForTesting + static final class Injector { + private final Context mContext; + private final DexManager mDexManager; + private final File mDataDir = Environment.getDataDirectory(); - boolean result; - if (params.getJobId() == JOB_POST_BOOT_UPDATE) { - result = runPostBootUpdate(params, pm, pkgs); - } else { - result = runIdleOptimization(params, pm, pkgs); + Injector(Context context, DexManager dexManager) { + mContext = context; + mDexManager = dexManager; } - return result; - } + Context getContext() { + return mContext; + } - @Override - public boolean onStopJob(JobParameters params) { - if (DEBUG) { - Slog.d(TAG, "onStopJob"); + PackageManagerService getPackageManagerService() { + return (PackageManagerService) ServiceManager.getService("package"); } - if (params.getJobId() == JOB_POST_BOOT_UPDATE) { - mAbortPostBootUpdate.set(true); + JobScheduler getJobScheduler() { + return mContext.getSystemService(JobScheduler.class); + } - // Do not reschedule. - // TODO: We should reschedule if we didn't process all apps, yet. - return false; - } else { - mAbortIdleOptimization.set(true); + DexManager getDexManager() { + return mDexManager; + } - // Reschedule the run. - // TODO: Should this be dependent on the stop reason? - return true; + PinnerService getPinnerService() { + return LocalServices.getService(PinnerService.class); } - } - private void notifyPinService(ArraySet<String> updatedPackages) { - PinnerService pinnerService = LocalServices.getService(PinnerService.class); - if (pinnerService != null) { - Slog.i(TAG, "Pinning optimized code " + updatedPackages); - pinnerService.update(updatedPackages, false /* force */); + boolean isBackgroundDexOptDisabled() { + return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */, + false /* default */); } - } - public static interface PackagesUpdatedListener { - /** Callback when packages have been updated by the bg-dexopt service. */ - public void onPackagesUpdated(ArraySet<String> updatedPackages); - } + boolean isBatteryLevelLow() { + return LocalServices.getService(BatteryManagerInternal.class).getBatteryLevelLow(); + } - public static void addPackagesUpdatedListener(PackagesUpdatedListener listener) { - synchronized (sPackagesUpdatedListeners) { - sPackagesUpdatedListeners.add(listener); + long getDowngradeUnusedAppsThresholdInMillis() { + String sysPropKey = "pm.dexopt.downgrade_after_inactive_days"; + String sysPropValue = SystemProperties.get(sysPropKey); + if (sysPropValue == null || sysPropValue.isEmpty()) { + Slog.w(TAG, "SysProp " + sysPropKey + " not set"); + return Long.MAX_VALUE; + } + return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue)); } - } - public static void removePackagesUpdatedListener(PackagesUpdatedListener listener) { - synchronized (sPackagesUpdatedListeners) { - sPackagesUpdatedListeners.remove(listener); + boolean supportSecondaryDex() { + return (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)); } - } - /** Notify all listeners (#addPackagesUpdatedListener) that packages have been updated. */ - private void notifyPackagesUpdated(ArraySet<String> updatedPackages) { - synchronized (sPackagesUpdatedListeners) { - for (PackagesUpdatedListener listener : sPackagesUpdatedListeners) { - listener.onPackagesUpdated(updatedPackages); + long getDataDirUsableSpace() { + return mDataDir.getUsableSpace(); + } + + long getDataDirStorageLowBytes() { + return mContext.getSystemService(StorageManager.class).getStorageLowBytes(mDataDir); + } + + int getCurrentThermalStatus() { + IThermalService thermalService = IThermalService.Stub + .asInterface(ServiceManager.getService(Context.THERMAL_SERVICE)); + try { + return thermalService.getCurrentThermalStatus(); + } catch (RemoteException e) { + return STATUS_ABORT_THERMAL; } } - } - private static long getDowngradeUnusedAppsThresholdInMillis() { - final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days"; - String sysPropValue = SystemProperties.get(sysPropKey); - if (sysPropValue == null || sysPropValue.isEmpty()) { - Slog.w(TAG, "SysProp " + sysPropKey + " not set"); - return Long.MAX_VALUE; + int getDexOptThermalCutoff() { + return SystemProperties.getInt("dalvik.vm.dexopt.thermal-cutoff", + THERMAL_CUTOFF_DEFAULT); } - return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue)); - } - private static boolean isBackgroundDexoptDisabled() { - return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */, - false /* default */); + Thread createAndStartThread(String name, Runnable target) { + Thread thread = new Thread(target, name); + Slog.i(TAG, "Starting thread:" + name); + thread.start(); + return thread; + } } } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 34e942885ba4..9c6f3ebaeabd 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -179,6 +179,7 @@ public class ComputerEngine implements Computer { private final PackageDexOptimizer mPackageDexOptimizer; private final DexManager mDexManager; private final CompilerStats mCompilerStats; + private final BackgroundDexOptService mBackgroundDexOptService; // PackageManagerService attributes that are primitives are referenced through the // pms object directly. Primitives are the only attributes so referenced. @@ -228,6 +229,7 @@ public class ComputerEngine implements Computer { mPackageDexOptimizer = args.service.mPackageDexOptimizer; mDexManager = args.service.getDexManager(); mCompilerStats = args.service.mCompilerStats; + mBackgroundDexOptService = args.service.mBackgroundDexOptService; // Used to reference PMS attributes that are primitives and which are not // updated under control of the PMS lock. @@ -2929,6 +2931,10 @@ public class ComputerEngine implements Computer { mDexManager.getPackageUseInfoOrDefault(pkgName)); ipw.decreaseIndent(); } + ipw.println("BgDexopt state:"); + ipw.increaseIndent(); + mBackgroundDexOptService.dump(ipw); + ipw.decreaseIndent(); break; } diff --git a/services/core/java/com/android/server/pm/InstallParams.java b/services/core/java/com/android/server/pm/InstallParams.java index 5a18d4026607..676d12ec993f 100644 --- a/services/core/java/com/android/server/pm/InstallParams.java +++ b/services/core/java/com/android/server/pm/InstallParams.java @@ -1973,7 +1973,7 @@ final class InstallParams extends HandlerParams { // If this is an update of a package which used to fail to compile, // BackgroundDexOptService will remove it from its denylist. // TODO: Layering violation - BackgroundDexOptService.notifyPackageChanged(packageName); + BackgroundDexOptService.getService().notifyPackageChanged(packageName); notifyPackageChangeObserversOnUpdate(reconciledPkg); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 2bec8be395fb..7607fa44627e 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -899,6 +899,7 @@ public class PackageManagerService extends IPackageManager.Stub final ArtManagerService mArtManagerService; final PackageDexOptimizer mPackageDexOptimizer; + final BackgroundDexOptService mBackgroundDexOptService; // DexManager handles the usage of dex files (e.g. secondary files, whether or not a package // is used by other apps). private final DexManager mDexManager; @@ -1531,7 +1532,8 @@ public class PackageManagerService extends IPackageManager.Stub }, new DefaultSystemWrapper(), LocalServices::getService, - context::getSystemService); + context::getSystemService, + (i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager())); if (Build.VERSION.SDK_INT <= 0) { Slog.w(TAG, "**** ro.build.version.sdk not set!"); @@ -1676,6 +1678,7 @@ public class PackageManagerService extends IPackageManager.Stub mApexManager = testParams.apexManager; mArtManagerService = testParams.artManagerService; mAvailableFeatures = testParams.availableFeatures; + mBackgroundDexOptService = testParams.backgroundDexOptService; mDefParseFlags = testParams.defParseFlags; mDefaultAppProvider = testParams.defaultAppProvider; mLegacyPermissionManager = testParams.legacyPermissionManagerInternal; @@ -1852,6 +1855,7 @@ public class PackageManagerService extends IPackageManager.Stub mPackageDexOptimizer = injector.getPackageDexOptimizer(); mDexManager = injector.getDexManager(); + mBackgroundDexOptService = injector.getBackgroundDexOptService(); mArtManagerService = injector.getArtManagerService(); mMoveCallbacks = new MovePackageHelper.MoveCallbacks(FgThread.get().getLooper()); mViewCompiler = injector.getViewCompiler(); @@ -6140,6 +6144,10 @@ public class PackageManagerService extends IPackageManager.Stub } } + /*package*/ void controlDexOptBlocking(boolean block) { + mPackageDexOptimizer.controlDexOptBlocking(block); + } + /** * Perform dexopt on the given package and return one of following result: * {@link PackageDexOptimizer#DEX_OPT_SKIPPED} @@ -6276,23 +6284,6 @@ public class PackageManagerService extends IPackageManager.Stub return mDexManager; } - /** - * Execute the background dexopt job immediately. - */ - @Override - public boolean runBackgroundDexoptJob(@Nullable List<String> packageNames) { - if (getInstantAppPackageName(Binder.getCallingUid()) != null) { - return false; - } - enforceSystemOrRootOrShell("runBackgroundDexoptJob"); - final long identity = Binder.clearCallingIdentity(); - try { - return BackgroundDexOptService.runIdleOptimizationsNow(this, mContext, packageNames); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - private static List<SharedLibraryInfo> findSharedLibraries(PackageSetting pkgSetting) { if (!pkgSetting.getPkgState().getUsesLibraryInfos().isEmpty()) { ArrayList<SharedLibraryInfo> retValue = new ArrayList<>(); @@ -11468,6 +11459,8 @@ public class PackageManagerService extends IPackageManager.Stub mPerUidReadTimeoutsCache = null; } }); + + mBackgroundDexOptService.systemReady(); } public void waitForAppDataPrepared() { diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java index a63cc362dc0c..97a09ffc810f 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java @@ -134,6 +134,7 @@ public class PackageManagerServiceInjector { private final Singleton<DomainVerificationManagerInternal> mDomainVerificationManagerInternalProducer; private final Singleton<Handler> mHandlerProducer; + private final Singleton<BackgroundDexOptService> mBackgroundDexOptService; PackageManagerServiceInjector(Context context, PackageManagerTracedLock lock, Installer installer, Object installLock, PackageAbiHelper abiHelper, @@ -168,7 +169,8 @@ public class PackageManagerServiceInjector { Producer<Handler> handlerProducer, SystemWrapper systemWrapper, ServiceProducer getLocalServiceProducer, - ServiceProducer getSystemServiceProducer) { + ServiceProducer getSystemServiceProducer, + Producer<BackgroundDexOptService> backgroundDexOptService) { mContext = context; mLock = lock; mInstaller = installer; @@ -217,6 +219,7 @@ public class PackageManagerServiceInjector { new Singleton<>( domainVerificationManagerInternalProducer); mHandlerProducer = new Singleton<>(handlerProducer); + mBackgroundDexOptService = new Singleton<>(backgroundDexOptService); } /** @@ -377,6 +380,10 @@ public class PackageManagerServiceInjector { return getLocalService(ActivityManagerInternal.class); } + public BackgroundDexOptService getBackgroundDexOptService() { + return mBackgroundDexOptService.get(this, mPackageManager); + } + /** Provides an abstraction to static access to system state. */ public interface SystemWrapper { void disablePackageCaches(); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java index 2870c4526678..c1c32dd84c9e 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java @@ -100,5 +100,6 @@ public final class PackageManagerServiceTestParams { public boolean isEngBuild; public boolean isUserDebugBuild; public int sdkInt = Build.VERSION.SDK_INT; + public BackgroundDexOptService backgroundDexOptService; public final String incrementalVersion = Build.VERSION.INCREMENTAL; } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index e4f63987956b..3554cc02b9fc 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -226,6 +226,8 @@ class PackageManagerShellCommand extends ShellCommand { return runForceDexOpt(); case "bg-dexopt-job": return runDexoptJob(); + case "cancel-bg-dexopt-job": + return cancelBgDexOptJob(); case "dump-profiles": return runDumpProfiles(); case "snapshot-profile": @@ -1863,12 +1865,18 @@ class PackageManagerShellCommand extends ShellCommand { while ((arg = getNextArg()) != null) { packageNames.add(arg); } - boolean result = mInterface.runBackgroundDexoptJob(packageNames.isEmpty() ? null : - packageNames); + boolean result = BackgroundDexOptService.getService().runBackgroundDexoptJob( + packageNames.isEmpty() ? null : packageNames); getOutPrintWriter().println(result ? "Success" : "Failure"); return result ? 0 : -1; } + private int cancelBgDexOptJob() throws RemoteException { + BackgroundDexOptService.getService().cancelBackgroundDexoptJob(); + getOutPrintWriter().println("Success"); + return 0; + } + private int runDumpProfiles() throws RemoteException { String packageName = getNextArg(); mInterface.dumpProfiles(packageName); @@ -3940,6 +3948,11 @@ class PackageManagerShellCommand extends ShellCommand { 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(" cancel-bg-dexopt-job"); + pw.println(" Cancels currently running background optimizations immediately."); + pw.println(" This cancels optimizations run from bg-dexopt-job or from JobScjeduler."); + pw.println(" Note that cancelling currently running bg-dexopt-job command requires"); + pw.println(" running this command from separate adb shell."); pw.println(""); pw.println(" reconcile-secondary-dex-files TARGET-PACKAGE"); pw.println(" Reconciles the package secondary dex files with the generated oat files."); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 4a2b51ddcf58..f351a8c8922f 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -150,7 +150,6 @@ import com.android.server.os.DeviceIdentifiersPolicyService; import com.android.server.os.NativeTombstoneManagerService; import com.android.server.os.SchedulingPolicyService; import com.android.server.people.PeopleService; -import com.android.server.pm.BackgroundDexOptService; import com.android.server.pm.CrossProfileAppsService; import com.android.server.pm.DataLoaderManagerService; import com.android.server.pm.DynamicCodeLoggingService; @@ -2409,15 +2408,6 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(AuthService.class); t.traceEnd(); - - t.traceBegin("StartBackgroundDexOptService"); - try { - BackgroundDexOptService.schedule(context); - } catch (Throwable e) { - reportWtf("starting StartBackgroundDexOptService", e); - } - t.traceEnd(); - if (!isWatch) { // We don't run this on watches as there are no plans to use the data logged // on watch devices. diff --git a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java index 1a0e5269d51a..bd8f5a4d225b 100644 --- a/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java +++ b/startop/iorap/src/com/google/android/startop/iorap/IorapForwardingService.java @@ -21,16 +21,13 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.JobInfo; import android.app.job.JobParameters; -import android.app.job.JobService; import android.app.job.JobScheduler; +import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.os.IBinder; -import android.os.IBinder.DeathRecipient; import android.os.Handler; -import android.os.Parcel; +import android.os.IBinder.DeathRecipient; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; @@ -45,18 +42,15 @@ import com.android.server.SystemService; import com.android.server.pm.BackgroundDexOptService; import com.android.server.pm.PackageManagerService; import com.android.server.wm.ActivityMetricsLaunchObserver; -import com.android.server.wm.ActivityMetricsLaunchObserver.ActivityRecordProto; -import com.android.server.wm.ActivityMetricsLaunchObserver.Temperature; import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; import com.android.server.wm.ActivityTaskManagerInternal; import java.time.Duration; -import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BooleanSupplier; -import java.util.HashMap; -import java.util.List; /** * System-server-local proxy into the {@code IIorap} native service. @@ -347,7 +341,8 @@ public class IorapForwardingService extends SystemService { launchObserverRegistry.registerLaunchObserver(mAppLaunchObserver); launchObserverRegistry.registerLaunchObserver(mEventSequenceValidator); - BackgroundDexOptService.addPackagesUpdatedListener(mDexOptPackagesUpdated); + BackgroundDexOptService.getService().addPackagesUpdatedListener( + mDexOptPackagesUpdated); mRegisteredListeners = true; |