blob: ad9ac1232437808a938a4ac63ec7a90879a3170d [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.DEBUG_DEXOPT;
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.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.os.BatteryManager;
import android.os.Environment;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.util.ArraySet;
import android.util.Log;
import android.util.StatsLog;
import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
import com.android.server.PinnerService;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
import java.io.File;
import java.nio.file.Paths;
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}
*/
public class BackgroundDexOptService extends JobService {
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;
private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG
? TimeUnit.MINUTES.toMillis(1)
: TimeUnit.DAYS.toMillis(1);
private static ComponentName sDexoptServiceName = new ComponentName(
"android",
BackgroundDexOptService.class.getName());
// Possible return codes of individual optimization steps.
// Optimizations finished. All packages were processed.
private static final int OPTIMIZE_PROCESSED = 0;
// Optimizations should continue. Issued after checking the scheduler, disk space or battery.
private static final int OPTIMIZE_CONTINUE = 1;
// Optimizations should be aborted. Job scheduler requested it.
private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2;
// Optimizations should be aborted. No space left on device.
private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3;
// Used for calculating space threshold for downgrading unused apps.
private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
/**
* Set of failed packages remembered across job runs.
*/
static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>();
static final ArraySet<String> sFailedPackageNamesSecondary = 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);
/**
* Atomic set to true if one job should exit early because another job was started.
*/
private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
private final File mDataDir = Environment.getDataDirectory();
private static final long mDowngradeUnusedAppsThresholdInMillis =
getDowngradeUnusedAppsThresholdInMillis();
public static void schedule(Context context) {
if (isBackgroundDexoptDisabled()) {
return;
}
JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
// Schedule a one-off job which scans installed packages and updates
// out-of-date oat files.
js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName)
.setMinimumLatency(TimeUnit.MINUTES.toMillis(1))
.setOverrideDeadline(TimeUnit.MINUTES.toMillis(1))
.build());
// 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_DEXOPT) {
Log.i(TAG, "Jobs scheduled");
}
}
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);
}
synchronized (sFailedPackageNamesSecondary) {
sFailedPackageNamesSecondary.remove(packageName);
}
}
// Returns the current battery level as a 0-100 integer.
private int getBatteryLevel() {
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent intent = registerReceiver(null, filter);
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
if (!present) {
// No battery, treat as if 100%, no possibility of draining battery.
return 100;
}
if (level < 0 || scale <= 0) {
// Battery data unavailable. This should never happen, so assume the worst.
return 0;
}
return (100 * level / scale);
}
private long getLowStorageThreshold(Context context) {
@SuppressWarnings("deprecation")
final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir);
if (lowThreshold == 0) {
Log.e(TAG, "Invalid low storage threshold");
}
return lowThreshold;
}
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.
return false;
}
new Thread("BackgroundDexOptService_PostBootUpdate") {
@Override
public void run() {
postBootUpdate(jobParams, pm, pkgs);
}
}.start();
return true;
}
private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
ArraySet<String> pkgs) {
// Load low battery threshold from the system config. This is a 0-100 integer.
final int lowBatteryThreshold = getResources().getInteger(
com.android.internal.R.integer.config_lowBatteryWarningLevel);
final long lowThreshold = getLowStorageThreshold(this);
mAbortPostBootUpdate.set(false);
ArraySet<String> updatedPackages = new ArraySet<>();
for (String pkg : pkgs) {
if (mAbortPostBootUpdate.get()) {
// JobScheduler requested an early abort.
return;
}
if (mExitPostBootUpdate.get()) {
// Different job, which supersedes this one, is running.
break;
}
if (getBatteryLevel() < lowBatteryThreshold) {
// Rather bail than completely drain the battery.
break;
}
long usableSpace = mDataDir.getUsableSpace();
if (usableSpace < lowThreshold) {
// Rather bail than completely fill up the disk.
Log.w(TAG, "Aborting background dex opt job due to low storage: " +
usableSpace);
break;
}
if (DEBUG_DEXOPT) {
Log.i(TAG, "Updating package " + pkg);
}
// 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_BOOT,
DexoptOptions.DEXOPT_BOOT_COMPLETE));
if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
updatedPackages.add(pkg);
}
}
notifyPinService(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_ABORT_BY_JOB_SCHEDULER) {
Log.w(TAG, "Idle optimizations aborted because of space constraints.");
// If we didn't abort we ran to completion (or stopped because of space).
// Abandon our timeslice and do not reschedule.
jobFinished(jobParams, /* reschedule */ false);
}
}
}.start();
return true;
}
// Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes).
private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs,
Context context) {
Log.i(TAG, "Performing idle optimizations");
// If post-boot update is still running, request that it exits early.
mExitPostBootUpdate.set(true);
mAbortIdleOptimization.set(false);
long lowStorageThreshold = getLowStorageThreshold(context);
// Optimize primary apks.
int result = optimizePackages(pm, pkgs, lowStorageThreshold,
/*isForPrimaryDex=*/ true);
if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
return result;
}
if (supportSecondaryDex()) {
result = reconcileSecondaryDexFiles(pm.getDexManager());
if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
return result;
}
result = optimizePackages(pm, pkgs, lowStorageThreshold,
/*isForPrimaryDex=*/ false);
}
return result;
}
/**
* Get the size of the directory. It uses recursion to go over all files.
* @param f
* @return
*/
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;
}
/**
* Get the size of a package.
* @param pkg
*/
private long getPackageSize(PackageManagerService pm, String pkg) {
PackageInfo info = pm.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;
}
private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
long lowStorageThreshold, boolean isForPrimaryDex) {
ArraySet<String> updatedPackages = new ArraySet<>();
Set<String> unusedPackages = pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
Log.d(TAG, "Unsused Packages " + String.join(",", unusedPackages));
// 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 *
lowStorageThreshold;
boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
Log.d(TAG, "Should Downgrade " + shouldDowngrade);
boolean dex_opt_performed = false;
for (String pkg : pkgs) {
int abort_code = abortIdleOptimizations(lowStorageThreshold);
if (abort_code == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
return abort_code;
}
// Downgrade unused packages.
if (unusedPackages.contains(pkg) && shouldDowngrade) {
dex_opt_performed = downgradePackage(pm, pkg, isForPrimaryDex);
} else {
if (abort_code == OPTIMIZE_ABORT_NO_SPACE_LEFT) {
// can't dexopt because of low space.
continue;
}
dex_opt_performed = optimizePackage(pm, pkg, isForPrimaryDex);
}
if (dex_opt_performed) {
updatedPackages.add(pkg);
}
}
notifyPinService(updatedPackages);
return OPTIMIZE_PROCESSED;
}
/**
* 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 true if the package was downgraded.
*/
private boolean downgradePackage(PackageManagerService pm, String pkg,
boolean isForPrimaryDex) {
Log.d(TAG, "Downgrading " + pkg);
boolean dex_opt_performed = false;
int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
| DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB
| DexoptOptions.DEXOPT_DOWNGRADE;
long package_size_before = getPackageSize(pm, pkg);
if (isForPrimaryDex) {
// This applies for system apps or if packages location is not a directory, i.e.
// monolithic install.
if (!pm.canHaveOatDir(pkg)) {
// For apps that don't have the oat directory, instead of downgrading,
// remove their compiler artifacts from dalvik cache.
pm.deleteOatArtifactsOfPackage(pkg);
} else {
dex_opt_performed = performDexOptPrimary(pm, pkg, reason, dexoptFlags);
}
} else {
dex_opt_performed = performDexOptSecondary(pm, pkg, reason, dexoptFlags);
}
if (dex_opt_performed) {
StatsLog.write(StatsLog.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));
}
private int reconcileSecondaryDexFiles(DexManager dm) {
// TODO(calin): should we blacklist packages for which we fail to reconcile?
for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
if (mAbortIdleOptimization.get()) {
return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
}
dm.reconcileSecondaryDexFiles(p);
}
return OPTIMIZE_PROCESSED;
}
/**
*
* Optimize package if needed. Note that there can be no race between
* 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.
*/
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;
return isForPrimaryDex
? performDexOptPrimary(pm, pkg, reason, dexoptFlags)
: performDexOptSecondary(pm, pkg, reason, dexoptFlags);
}
private boolean performDexOptPrimary(PackageManagerService pm, String pkg, int reason,
int dexoptFlags) {
int result = trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
() -> pm.performDexOptWithStatus(new DexoptOptions(pkg, reason, dexoptFlags)));
return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
}
private boolean 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,
() -> pm.performDexOpt(dexoptOptions)
? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
);
return result == PackageDexOptimizer.DEX_OPT_PERFORMED;
}
/**
* 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_PERFORMED}
* {@link PackageDexOptimizer#DEX_OPT_FAILED}
*/
private int trackPerformDexOpt(String pkg, boolean isForPrimaryDex,
Supplier<Integer> performDexOptWrapper) {
ArraySet<String> sFailedPackageNames =
isForPrimaryDex ? sFailedPackageNamesPrimary : sFailedPackageNamesSecondary;
synchronized (sFailedPackageNames) {
if (sFailedPackageNames.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);
}
}
return result;
}
// Evaluate whether or not idle optimizations should continue.
private int abortIdleOptimizations(long lowStorageThreshold) {
if (mAbortIdleOptimization.get()) {
// JobScheduler requested an early abort.
return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
}
long usableSpace = mDataDir.getUsableSpace();
if (usableSpace < lowStorageThreshold) {
// Rather bail than completely fill up the disk.
Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
return OPTIMIZE_ABORT_NO_SPACE_LEFT;
}
return OPTIMIZE_CONTINUE;
}
// Evaluate whether apps should be downgraded.
private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) {
long usableSpace = mDataDir.getUsableSpace();
if (usableSpace < 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);
}
int result = bdos.idleOptimization(pm, packagesToOptimize, context);
return result == OPTIMIZE_PROCESSED;
}
@Override
public boolean onStartJob(JobParameters params) {
if (DEBUG_DEXOPT) {
Log.i(TAG, "onStartJob");
}
// 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()) {
if (DEBUG_DEXOPT) {
Log.i(TAG, "Low storage, skipping this run");
}
return false;
}
final ArraySet<String> pkgs = pm.getOptimizablePackages();
if (pkgs.isEmpty()) {
if (DEBUG_DEXOPT) {
Log.i(TAG, "No packages to optimize");
}
return false;
}
boolean result;
if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
result = runPostBootUpdate(params, pm, pkgs);
} else {
result = runIdleOptimization(params, pm, pkgs);
}
return result;
}
@Override
public boolean onStopJob(JobParameters params) {
if (DEBUG_DEXOPT) {
Log.i(TAG, "onStopJob");
}
if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
mAbortPostBootUpdate.set(true);
// Do not reschedule.
// TODO: We should reschedule if we didn't process all apps, yet.
return false;
} else {
mAbortIdleOptimization.set(true);
// Reschedule the run.
// TODO: Should this be dependent on the stop reason?
return true;
}
}
private void notifyPinService(ArraySet<String> updatedPackages) {
PinnerService pinnerService = LocalServices.getService(PinnerService.class);
if (pinnerService != null) {
Log.i(TAG, "Pinning optimized code " + updatedPackages);
pinnerService.update(updatedPackages, false /* force */);
}
}
private static long getDowngradeUnusedAppsThresholdInMillis() {
final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days";
String sysPropValue = SystemProperties.get(sysPropKey);
if (sysPropValue == null || sysPropValue.isEmpty()) {
Log.w(TAG, "SysProp " + sysPropKey + " not set");
return Long.MAX_VALUE;
}
return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
}
private static boolean isBackgroundDexoptDisabled() {
return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
false /* default */);
}
}