blob: e7412c5e52ec405a994ec43f5ba6dd5294f1a06c [file] [log] [blame]
/*
* Copyright (C) 2014 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 static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import static com.android.server.pm.dex.ArtStatsLogUtils.BackgroundDexoptJobStatsLogger;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
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.SystemClock;
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.function.Supplier;
/**
* Controls background dex optimization run as idle job or command line.
*/
public final class BackgroundDexOptService {
private static final String TAG = "BackgroundDexOptService";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@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 final long CANCELLATION_WAIT_CHECK_INTERVAL_MS = 200;
private static ComponentName sDexoptServiceName =
new ComponentName("android", BackgroundDexOptJobService.class.getName());
// Possible return codes of individual optimization steps.
/** Ok status: Optimizations finished, All packages were processed, can continue */
public static final int STATUS_OK = 0;
/** Optimizations should be aborted. Job scheduler requested it. */
public static final int STATUS_ABORT_BY_CANCELLATION = 1;
/** Optimizations should be aborted. No space left on device. */
public static final int STATUS_ABORT_NO_SPACE_LEFT = 2;
/** Optimizations should be aborted. Thermal throttling level too high. */
public static final int STATUS_ABORT_THERMAL = 3;
/** Battery level too low */
public static final int STATUS_ABORT_BATTERY = 4;
/**
* {@link PackageDexOptimizer#DEX_OPT_FAILED} case. This state means some packages have failed
* compilation during the job. Note that the failure will not be permanent as the next dexopt
* job will exclude those failed packages.
*/
public 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)
public @interface Status {}
// Used for calculating space threshold for downgrading unused apps.
private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
// 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 DexOptHelper mDexOptHelper;
private final BackgroundDexoptJobStatsLogger mStatsLogger =
new BackgroundDexoptJobStatsLogger();
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;
// True if JobScheduler invocations of dexopt have been disabled.
@GuardedBy("mLock") private boolean mDisableJobSchedulerJobs;
@GuardedBy("mLock") @Status private int mLastExecutionStatus = STATUS_OK;
@GuardedBy("mLock") private long mLastExecutionStartTimeMs;
@GuardedBy("mLock") private long mLastExecutionDurationIncludingSleepMs;
@GuardedBy("mLock") private long mLastExecutionStartUptimeMs;
@GuardedBy("mLock") private long mLastExecutionDurationMs;
// 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.
*/
@GuardedBy("mLock")
private final ArraySet<String> mFailedPackageNamesPrimary = new ArraySet<String>();
@GuardedBy("mLock")
private final ArraySet<String> mFailedPackageNamesSecondary = new ArraySet<String>();
private final long mDowngradeUnusedAppsThresholdInMillis;
private List<PackagesUpdatedListener> mPackagesUpdatedListeners = new ArrayList<>();
private int mThermalStatusCutoff = THERMAL_CUTOFF_DEFAULT;
/** Listener for monitoring package change due to dexopt. */
public interface PackagesUpdatedListener {
/** Called when the packages are updated through dexopt */
void onPackagesUpdated(ArraySet<String> updatedPackages);
}
public BackgroundDexOptService(
Context context, DexManager dexManager, PackageManagerService pm) {
this(new Injector(context, dexManager, pm));
}
@VisibleForTesting
public BackgroundDexOptService(Injector injector) {
mInjector = injector;
mDexOptHelper = mInjector.getDexOptHelper();
LocalServices.addService(BackgroundDexOptService.class, this);
mDowngradeUnusedAppsThresholdInMillis = mInjector.getDowngradeUnusedAppsThresholdInMillis();
}
/** Start scheduling job after boot completion */
public void systemReady() {
if (mInjector.isBackgroundDexOptDisabled()) {
return;
}
mInjector.getContext().registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
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.d(TAG, "BootBgDexopt scheduled");
}
}
}, new IntentFilter(Intent.ACTION_BOOT_COMPLETED));
}
/** 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.println(mFinishedPostBootUpdate);
writer.print("mDisableJobSchedulerJobs:");
writer.println(mDisableJobSchedulerJobs);
writer.print("mLastExecutionStatus:");
writer.println(mLastExecutionStatus);
writer.print("mLastExecutionStartTimeMs:");
writer.println(mLastExecutionStartTimeMs);
writer.print("mLastExecutionDurationIncludingSleepMs:");
writer.println(mLastExecutionDurationIncludingSleepMs);
writer.print("mLastExecutionStartUptimeMs:");
writer.println(mLastExecutionStartUptimeMs);
writer.print("mLastExecutionDurationMs:");
writer.println(mLastExecutionDurationMs);
writer.print("now:");
writer.println(SystemClock.elapsedRealtime());
writer.print("mLastCancelledPackages:");
writer.println(String.join(",", mLastCancelledPackages));
writer.print("mFailedPackageNamesPrimary:");
writer.println(String.join(",", mFailedPackageNamesPrimary));
writer.print("mFailedPackageNamesSecondary:");
writer.println(String.join(",", mFailedPackageNamesSecondary));
}
}
/** 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 in the given order, or all packages in
* the default order 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();
List<String> packagesToOptimize;
if (packageNames == null) {
packagesToOptimize = mDexOptHelper.getOptimizablePackages(pm.snapshotComputer());
} else {
packagesToOptimize = packageNames;
}
return runIdleOptimization(pm, packagesToOptimize, /* isPostBootUpdate= */ false);
} finally {
Binder.restoreCallingIdentity(identity);
markDexOptCompleted();
}
}
/**
* Cancels currently running any idle optimization tasks started from JobScheduler
* or runIdleOptimization call.
*
* <p>This is only for shell command and only root or shell user can use this.
*/
public void cancelBackgroundDexoptJob() {
enforceRootOrShell();
Binder.withCleanCallingIdentity(() -> cancelDexOptAndWaitForCompletion());
}
/**
* Sets a flag that disables jobs from being started from JobScheduler.
*
* This state is not persistent and is only retained in this service instance.
*
* This is intended for shell command use and only root or shell users can call it.
*
* @param disable True if JobScheduler invocations should be disabled, false otherwise.
*/
public void setDisableJobSchedulerJobs(boolean disable) {
enforceRootOrShell();
synchronized (mLock) {
mDisableJobSchedulerJobs = disable;
}
}
/** Adds listener for package update */
public void addPackagesUpdatedListener(PackagesUpdatedListener listener) {
synchronized (mLock) {
mPackagesUpdatedListeners.add(listener);
}
}
/** Removes package update listener */
public void removePackagesUpdatedListener(PackagesUpdatedListener listener) {
synchronized (mLock) {
mPackagesUpdatedListeners.remove(listener);
}
}
/**
* 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);
}
}
/** 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;
}
List<String> pkgs = mDexOptHelper.getOptimizablePackages(pm.snapshotComputer());
if (pkgs.isEmpty()) {
Slog.i(TAG, "No packages to optimize");
markPostBootUpdateCompleted(params);
return false;
}
mThermalStatusCutoff = mInjector.getDexOptThermalCutoff();
synchronized (mLock) {
if (mDisableJobSchedulerJobs) {
Slog.i(TAG, "JobScheduler invocations disabled");
return false;
}
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);
writeStatsLog(params);
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;
}
/** 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;
}
/**
* 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 (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;
}
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();
}
}
}
@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();
}
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();
}
}
@GuardedBy("mLock")
private void resetStatesForNewDexOptRunLocked(Thread thread) {
mDexOptThread = thread;
mLastCancelledPackages.clear();
controlDexOptBlockingLocked(false);
}
private void enforceRootOrShell() {
int uid = mInjector.getCallingUid();
if (uid != Process.ROOT_UID && uid != Process.SHELL_UID) {
throw new SecurityException("Should be shell or root user");
}
}
@GuardedBy("mLock")
private void controlDexOptBlockingLocked(boolean block) {
PackageManagerService pm = mInjector.getPackageManagerService();
mDexOptHelper.controlDexOptBlocking(block);
}
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());
}
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 whether we've successfully run the job. Note that it will return true even if some
* packages may have failed compiling.
*/
private boolean runIdleOptimization(
PackageManagerService pm, List<String> pkgs, boolean isPostBootUpdate) {
synchronized (mLock) {
mLastExecutionStartTimeMs = SystemClock.elapsedRealtime();
mLastExecutionDurationIncludingSleepMs = -1;
mLastExecutionStartUptimeMs = SystemClock.uptimeMillis();
mLastExecutionDurationMs = -1;
}
long lowStorageThreshold = getLowStorageThreshold();
int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, isPostBootUpdate);
logStatus(status);
synchronized (mLock) {
mLastExecutionStatus = status;
mLastExecutionDurationIncludingSleepMs =
SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs;
mLastExecutionDurationMs = SystemClock.uptimeMillis() - mLastExecutionStartUptimeMs;
}
return status == STATUS_OK || status == STATUS_DEX_OPT_FAILED;
}
/** 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()) {
for (File file : f.listFiles()) {
size += getDirectorySize(file);
}
} else {
size = f.length();
}
return size;
}
/** Gets the size of a package. */
private long getPackageSize(@NonNull Computer snapshot, String pkg) {
PackageInfo info = snapshot.getPackageInfo(pkg, 0, UserHandle.USER_SYSTEM);
long size = 0;
if (info != null && info.applicationInfo != null) {
File path = Paths.get(info.applicationInfo.sourceDir).toFile();
if (path.isFile()) {
path = path.getParentFile();
}
size += getDirectorySize(path);
if (!ArrayUtils.isEmpty(info.applicationInfo.splitSourceDirs)) {
for (String splitSourceDir : info.applicationInfo.splitSourceDirs) {
path = Paths.get(splitSourceDir).toFile();
if (path.isFile()) {
path = path.getParentFile();
}
size += getDirectorySize(path);
}
}
return size;
}
return 0;
}
@Status
private int idleOptimizePackages(PackageManagerService pm, List<String> pkgs,
long lowStorageThreshold, boolean isPostBootUpdate) {
ArraySet<String> updatedPackages = new ArraySet<>();
try {
boolean supportSecondaryDex = mInjector.supportSecondaryDex();
if (supportSecondaryDex) {
@Status int result = reconcileSecondaryDexFiles();
if (result != STATUS_OK) {
return result;
}
}
// 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.
long lowStorageThresholdForDowngrade =
LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE * lowStorageThreshold;
boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
if (DEBUG) {
Slog.d(TAG, "Should Downgrade " + shouldDowngrade);
}
if (shouldDowngrade) {
final Computer snapshot = pm.snapshotComputer();
Set<String> unusedPackages =
snapshot.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
if (DEBUG) {
Slog.d(TAG, "Unsused Packages " + String.join(",", unusedPackages));
}
if (!unusedPackages.isEmpty()) {
for (String pkg : unusedPackages) {
@Status int abortCode = abortIdleOptimizations(/*lowStorageThreshold*/ -1);
if (abortCode != STATUS_OK) {
// Should be aborted by the scheduler.
return abortCode;
}
@DexOptResult
int downgradeResult = downgradePackage(snapshot, 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) {
downgradeResult = downgradePackage(snapshot, pm, pkg,
/* isForPrimaryDex= */ false, isPostBootUpdate);
status = convertPackageDexOptimizerStatusToInternal(downgradeResult);
if (status != STATUS_OK) {
return status;
}
}
}
pkgs = new ArrayList<>(pkgs);
pkgs.removeAll(unusedPackages);
}
}
return optimizePackages(pkgs, lowStorageThreshold, updatedPackages, isPostBootUpdate);
} finally {
// Always let the pinner service know about changes.
notifyPinService(updatedPackages);
// Only notify IORap the primary dex opt, because we don't want to
// invalidate traces unnecessary due to b/161633001 and that it's
// better to have a trace than no trace at all.
notifyPackagesUpdated(updatedPackages);
}
}
@Status
private int optimizePackages(List<String> pkgs, long lowStorageThreshold,
ArraySet<String> updatedPackages, boolean isPostBootUpdate) {
boolean supportSecondaryDex = mInjector.supportSecondaryDex();
// Keep the error if there is any error from any package.
@Status int status = STATUS_OK;
// Other than cancellation, all packages will be processed even if an error happens
// in a package.
for (String pkg : pkgs) {
int abortCode = abortIdleOptimizations(lowStorageThreshold);
if (abortCode != STATUS_OK) {
// Either aborted by the scheduler or no space left.
return abortCode;
}
@DexOptResult
int primaryResult = optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate);
if (primaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) {
return STATUS_ABORT_BY_CANCELLATION;
}
if (primaryResult == PackageDexOptimizer.DEX_OPT_PERFORMED) {
updatedPackages.add(pkg);
} else if (primaryResult == PackageDexOptimizer.DEX_OPT_FAILED) {
status = convertPackageDexOptimizerStatusToInternal(primaryResult);
}
if (!supportSecondaryDex) {
continue;
}
@DexOptResult
int secondaryResult =
optimizePackage(pkg, false /* isForPrimaryDex */, isPostBootUpdate);
if (secondaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) {
return STATUS_ABORT_BY_CANCELLATION;
}
if (secondaryResult == PackageDexOptimizer.DEX_OPT_FAILED) {
status = convertPackageDexOptimizerStatusToInternal(secondaryResult);
}
}
return status;
}
/**
* Try to downgrade the package to a smaller compilation filter.
* 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 PackageDexOptimizer.DEX_*
*/
@DexOptResult
private int downgradePackage(@NonNull Computer snapshot, PackageManagerService pm, String pkg,
boolean isForPrimaryDex, boolean isPostBootUpdate) {
if (DEBUG) {
Slog.d(TAG, "Downgrading " + pkg);
}
if (isCancelling()) {
return PackageDexOptimizer.DEX_OPT_CANCELLED;
}
int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE | DexoptOptions.DEXOPT_DOWNGRADE;
if (!isPostBootUpdate) {
dexoptFlags |= DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB;
}
long package_size_before = getPackageSize(snapshot, 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.
if (!pm.canHaveOatDir(snapshot, pkg)) {
// For apps that don't have the oat directory, instead of downgrading,
// remove their compiler artifacts from dalvik cache.
pm.deleteOatArtifactsOfPackage(snapshot, pkg);
} else {
result = performDexOptPrimary(pkg, reason, dexoptFlags);
}
} else {
result = performDexOptSecondary(pkg, reason, dexoptFlags);
}
if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
final Computer newSnapshot = pm.snapshotComputer();
FrameworkStatsLog.write(FrameworkStatsLog.APP_DOWNGRADED, pkg, package_size_before,
getPackageSize(newSnapshot, pkg), /*aggressive=*/false);
}
return result;
}
@Status
private int reconcileSecondaryDexFiles() {
// TODO(calin): should we denylist packages for which we fail to reconcile?
for (String p : mInjector.getDexManager().getAllPackagesWithSecondaryDexFiles()) {
if (isCancelling()) {
return STATUS_ABORT_BY_CANCELLATION;
}
mInjector.getDexManager().reconcileSecondaryDexFiles(p);
}
return STATUS_OK;
}
/**
*
* Optimize package if needed. Note that there can be no race between
* concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
* @param pkg The package to be downgraded.
* @param isForPrimaryDex Apps can have several dex file, primary and secondary.
* @param isPostBootUpdate is post boot update or not.
* @return PackageDexOptimizer#DEX_OPT_*
*/
@DexOptResult
private int optimizePackage(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.
if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) {
return performDexOptPrimary(pkg, reason, dexoptFlags);
} else {
return performDexOptSecondary(pkg, reason, dexoptFlags);
}
}
@DexOptResult
private int performDexOptPrimary(String pkg, int reason, int dexoptFlags) {
DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, dexoptFlags);
return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/true,
() -> mDexOptHelper.performDexOptWithStatus(dexoptOptions));
}
@DexOptResult
private int performDexOptSecondary(String pkg, int reason, int dexoptFlags) {
DexoptOptions dexoptOptions = new DexoptOptions(
pkg, reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/false,
()
-> mDexOptHelper.performDexOpt(dexoptOptions)
? PackageDexOptimizer.DEX_OPT_PERFORMED
: PackageDexOptimizer.DEX_OPT_FAILED);
}
/**
* Execute the dexopt wrapper and make sure that if performDexOpt wrapper fails
* 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> failedPackageNames;
synchronized (mLock) {
failedPackageNames =
isForPrimaryDex ? mFailedPackageNamesPrimary : mFailedPackageNamesSecondary;
if (failedPackageNames.contains(pkg)) {
// Skip previously failing package
return PackageDexOptimizer.DEX_OPT_SKIPPED;
}
}
int result = performDexOptWrapper.get();
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;
}
@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 (isCancelling()) {
// JobScheduler requested an early abort.
return STATUS_ABORT_BY_CANCELLATION;
}
// Abort background dexopt if the device is in a moderate or stronger thermal throttling
// state.
int thermalStatus = mInjector.getCurrentThermalStatus();
if (DEBUG) {
Log.d(TAG, "Thermal throttling status during bgdexopt: " + thermalStatus);
}
if (thermalStatus >= mThermalStatusCutoff) {
return STATUS_ABORT_THERMAL;
}
if (mInjector.isBatteryLevelLow()) {
return STATUS_ABORT_BATTERY;
}
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 STATUS_ABORT_NO_SPACE_LEFT;
}
return STATUS_OK;
}
// Evaluate whether apps should be downgraded.
private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) {
if (mInjector.getDataDirUsableSpace() < lowStorageThresholdForDowngrade) {
return true;
}
return false;
}
private boolean isCancelling() {
synchronized (mLock) {
return mDexOptCancellingThread != null;
}
}
private void markPostBootUpdateCompleted(JobParameters params) {
if (params.getJobId() != JOB_POST_BOOT_UPDATE) {
return;
}
synchronized (mLock) {
if (!mFinishedPostBootUpdate) {
mFinishedPostBootUpdate = true;
}
}
// Safe to do this outside lock.
mInjector.getJobScheduler().cancel(JOB_POST_BOOT_UPDATE);
}
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 */);
}
}
/** Notify all listeners (#addPackagesUpdatedListener) that packages have been updated. */
private void notifyPackagesUpdated(ArraySet<String> updatedPackages) {
synchronized (mLock) {
for (PackagesUpdatedListener listener : mPackagesUpdatedListeners) {
listener.onPackagesUpdated(updatedPackages);
}
}
}
private void writeStatsLog(JobParameters params) {
@Status int status;
long durationMs;
long durationIncludingSleepMs;
synchronized (mLock) {
status = mLastExecutionStatus;
durationMs = mLastExecutionDurationMs;
durationIncludingSleepMs = mLastExecutionDurationIncludingSleepMs;
}
mStatsLogger.write(status, params.getStopReason(), durationMs, durationIncludingSleepMs);
}
/** Injector pattern for testing purpose */
@VisibleForTesting
static final class Injector {
private final Context mContext;
private final DexManager mDexManager;
private final PackageManagerService mPackageManagerService;
private final File mDataDir = Environment.getDataDirectory();
Injector(Context context, DexManager dexManager, PackageManagerService pm) {
mContext = context;
mDexManager = dexManager;
mPackageManagerService = pm;
}
int getCallingUid() {
return Binder.getCallingUid();
}
Context getContext() {
return mContext;
}
PackageManagerService getPackageManagerService() {
return mPackageManagerService;
}
DexOptHelper getDexOptHelper() {
return new DexOptHelper(getPackageManagerService());
}
JobScheduler getJobScheduler() {
return mContext.getSystemService(JobScheduler.class);
}
DexManager getDexManager() {
return mDexManager;
}
PinnerService getPinnerService() {
return LocalServices.getService(PinnerService.class);
}
boolean isBackgroundDexOptDisabled() {
return SystemProperties.getBoolean(
"pm.dexopt.disable_bg_dexopt" /* key */, false /* default */);
}
boolean isBatteryLevelLow() {
return LocalServices.getService(BatteryManagerInternal.class).getBatteryLevelLow();
}
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));
}
boolean supportSecondaryDex() {
return (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false));
}
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;
}
}
int getDexOptThermalCutoff() {
return SystemProperties.getInt(
"dalvik.vm.dexopt.thermal-cutoff", THERMAL_CUTOFF_DEFAULT);
}
Thread createAndStartThread(String name, Runnable target) {
Thread thread = new Thread(target, name);
Slog.i(TAG, "Starting thread:" + name);
thread.start();
return thread;
}
}
}