Home | History | Annotate | Download | only in pm
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.pm;
     18 
     19 import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT;
     20 
     21 import android.annotation.Nullable;
     22 import android.app.job.JobInfo;
     23 import android.app.job.JobParameters;
     24 import android.app.job.JobScheduler;
     25 import android.app.job.JobService;
     26 import android.content.ComponentName;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.os.BatteryManager;
     31 import android.os.Environment;
     32 import android.os.ServiceManager;
     33 import android.os.SystemProperties;
     34 import android.os.storage.StorageManager;
     35 import android.util.ArraySet;
     36 import android.util.Log;
     37 
     38 import com.android.server.pm.dex.DexManager;
     39 import com.android.server.LocalServices;
     40 import com.android.server.PinnerService;
     41 import com.android.server.pm.dex.DexoptOptions;
     42 
     43 import java.io.File;
     44 import java.util.List;
     45 import java.util.Set;
     46 import java.util.concurrent.atomic.AtomicBoolean;
     47 import java.util.concurrent.TimeUnit;
     48 
     49 /**
     50  * {@hide}
     51  */
     52 public class BackgroundDexOptService extends JobService {
     53     private static final String TAG = "BackgroundDexOptService";
     54 
     55     private static final boolean DEBUG = false;
     56 
     57     private static final int JOB_IDLE_OPTIMIZE = 800;
     58     private static final int JOB_POST_BOOT_UPDATE = 801;
     59 
     60     private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG
     61             ? TimeUnit.MINUTES.toMillis(1)
     62             : TimeUnit.DAYS.toMillis(1);
     63 
     64     private static ComponentName sDexoptServiceName = new ComponentName(
     65             "android",
     66             BackgroundDexOptService.class.getName());
     67 
     68     // Possible return codes of individual optimization steps.
     69 
     70     // Optimizations finished. All packages were processed.
     71     private static final int OPTIMIZE_PROCESSED = 0;
     72     // Optimizations should continue. Issued after checking the scheduler, disk space or battery.
     73     private static final int OPTIMIZE_CONTINUE = 1;
     74     // Optimizations should be aborted. Job scheduler requested it.
     75     private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2;
     76     // Optimizations should be aborted. No space left on device.
     77     private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3;
     78 
     79     // Used for calculating space threshold for downgrading unused apps.
     80     private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
     81 
     82     /**
     83      * Set of failed packages remembered across job runs.
     84      */
     85     static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>();
     86     static final ArraySet<String> sFailedPackageNamesSecondary = new ArraySet<String>();
     87 
     88     /**
     89      * Atomics set to true if the JobScheduler requests an abort.
     90      */
     91     private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false);
     92     private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false);
     93 
     94     /**
     95      * Atomic set to true if one job should exit early because another job was started.
     96      */
     97     private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
     98 
     99     private final File mDataDir = Environment.getDataDirectory();
    100 
    101     private static final long mDowngradeUnusedAppsThresholdInMillis =
    102             getDowngradeUnusedAppsThresholdInMillis();
    103 
    104     public static void schedule(Context context) {
    105         if (isBackgroundDexoptDisabled()) {
    106             return;
    107         }
    108 
    109         JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
    110 
    111         // Schedule a one-off job which scans installed packages and updates
    112         // out-of-date oat files.
    113         js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName)
    114                     .setMinimumLatency(TimeUnit.MINUTES.toMillis(1))
    115                     .setOverrideDeadline(TimeUnit.MINUTES.toMillis(1))
    116                     .build());
    117 
    118         // Schedule a daily job which scans installed packages and compiles
    119         // those with fresh profiling data.
    120         js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
    121                     .setRequiresDeviceIdle(true)
    122                     .setRequiresCharging(true)
    123                     .setPeriodic(IDLE_OPTIMIZATION_PERIOD)
    124                     .build());
    125 
    126         if (DEBUG_DEXOPT) {
    127             Log.i(TAG, "Jobs scheduled");
    128         }
    129     }
    130 
    131     public static void notifyPackageChanged(String packageName) {
    132         // The idle maintanance job skips packages which previously failed to
    133         // compile. The given package has changed and may successfully compile
    134         // now. Remove it from the list of known failing packages.
    135         synchronized (sFailedPackageNamesPrimary) {
    136             sFailedPackageNamesPrimary.remove(packageName);
    137         }
    138         synchronized (sFailedPackageNamesSecondary) {
    139             sFailedPackageNamesSecondary.remove(packageName);
    140         }
    141     }
    142 
    143     // Returns the current battery level as a 0-100 integer.
    144     private int getBatteryLevel() {
    145         IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    146         Intent intent = registerReceiver(null, filter);
    147         int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
    148         int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    149         boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
    150 
    151         if (!present) {
    152             // No battery, treat as if 100%, no possibility of draining battery.
    153             return 100;
    154         }
    155 
    156         if (level < 0 || scale <= 0) {
    157             // Battery data unavailable. This should never happen, so assume the worst.
    158             return 0;
    159         }
    160 
    161         return (100 * level / scale);
    162     }
    163 
    164     private long getLowStorageThreshold(Context context) {
    165         @SuppressWarnings("deprecation")
    166         final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir);
    167         if (lowThreshold == 0) {
    168             Log.e(TAG, "Invalid low storage threshold");
    169         }
    170 
    171         return lowThreshold;
    172     }
    173 
    174     private boolean runPostBootUpdate(final JobParameters jobParams,
    175             final PackageManagerService pm, final ArraySet<String> pkgs) {
    176         if (mExitPostBootUpdate.get()) {
    177             // This job has already been superseded. Do not start it.
    178             return false;
    179         }
    180         new Thread("BackgroundDexOptService_PostBootUpdate") {
    181             @Override
    182             public void run() {
    183                 postBootUpdate(jobParams, pm, pkgs);
    184             }
    185 
    186         }.start();
    187         return true;
    188     }
    189 
    190     private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
    191             ArraySet<String> pkgs) {
    192         // Load low battery threshold from the system config. This is a 0-100 integer.
    193         final int lowBatteryThreshold = getResources().getInteger(
    194                 com.android.internal.R.integer.config_lowBatteryWarningLevel);
    195         final long lowThreshold = getLowStorageThreshold(this);
    196 
    197         mAbortPostBootUpdate.set(false);
    198 
    199         ArraySet<String> updatedPackages = new ArraySet<>();
    200         for (String pkg : pkgs) {
    201             if (mAbortPostBootUpdate.get()) {
    202                 // JobScheduler requested an early abort.
    203                 return;
    204             }
    205             if (mExitPostBootUpdate.get()) {
    206                 // Different job, which supersedes this one, is running.
    207                 break;
    208             }
    209             if (getBatteryLevel() < lowBatteryThreshold) {
    210                 // Rather bail than completely drain the battery.
    211                 break;
    212             }
    213             long usableSpace = mDataDir.getUsableSpace();
    214             if (usableSpace < lowThreshold) {
    215                 // Rather bail than completely fill up the disk.
    216                 Log.w(TAG, "Aborting background dex opt job due to low storage: " +
    217                         usableSpace);
    218                 break;
    219             }
    220 
    221             if (DEBUG_DEXOPT) {
    222                 Log.i(TAG, "Updating package " + pkg);
    223             }
    224 
    225             // Update package if needed. Note that there can be no race between concurrent
    226             // jobs because PackageDexOptimizer.performDexOpt is synchronized.
    227 
    228             // checkProfiles is false to avoid merging profiles during boot which
    229             // might interfere with background compilation (b/28612421).
    230             // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
    231             // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
    232             // trade-off worth doing to save boot time work.
    233             int result = pm.performDexOptWithStatus(new DexoptOptions(
    234                     pkg,
    235                     PackageManagerService.REASON_BOOT,
    236                     DexoptOptions.DEXOPT_BOOT_COMPLETE));
    237             if (result == PackageDexOptimizer.DEX_OPT_PERFORMED)  {
    238                 updatedPackages.add(pkg);
    239             }
    240         }
    241         notifyPinService(updatedPackages);
    242         // Ran to completion, so we abandon our timeslice and do not reschedule.
    243         jobFinished(jobParams, /* reschedule */ false);
    244     }
    245 
    246     private boolean runIdleOptimization(final JobParameters jobParams,
    247             final PackageManagerService pm, final ArraySet<String> pkgs) {
    248         new Thread("BackgroundDexOptService_IdleOptimization") {
    249             @Override
    250             public void run() {
    251                 int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this);
    252                 if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
    253                     Log.w(TAG, "Idle optimizations aborted because of space constraints.");
    254                     // If we didn't abort we ran to completion (or stopped because of space).
    255                     // Abandon our timeslice and do not reschedule.
    256                     jobFinished(jobParams, /* reschedule */ false);
    257                 }
    258             }
    259         }.start();
    260         return true;
    261     }
    262 
    263     // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes).
    264     private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs,
    265             Context context) {
    266         Log.i(TAG, "Performing idle optimizations");
    267         // If post-boot update is still running, request that it exits early.
    268         mExitPostBootUpdate.set(true);
    269         mAbortIdleOptimization.set(false);
    270 
    271         long lowStorageThreshold = getLowStorageThreshold(context);
    272         // Optimize primary apks.
    273         int result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ true,
    274                 sFailedPackageNamesPrimary);
    275 
    276         if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
    277             return result;
    278         }
    279 
    280         if (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
    281             result = reconcileSecondaryDexFiles(pm.getDexManager());
    282             if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
    283                 return result;
    284             }
    285 
    286             result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false,
    287                     sFailedPackageNamesSecondary);
    288         }
    289         return result;
    290     }
    291 
    292     private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
    293             long lowStorageThreshold, boolean is_for_primary_dex,
    294             ArraySet<String> failedPackageNames) {
    295         ArraySet<String> updatedPackages = new ArraySet<>();
    296         Set<String> unusedPackages = pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
    297         // Only downgrade apps when space is low on device.
    298         // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean
    299         // up disk before user hits the actual lowStorageThreshold.
    300         final long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE *
    301                 lowStorageThreshold;
    302         boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
    303         for (String pkg : pkgs) {
    304             int abort_code = abortIdleOptimizations(lowStorageThreshold);
    305             if (abort_code == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
    306                 return abort_code;
    307             }
    308 
    309             synchronized (failedPackageNames) {
    310                 if (failedPackageNames.contains(pkg)) {
    311                     // Skip previously failing package
    312                     continue;
    313                 }
    314             }
    315 
    316             int reason;
    317             boolean downgrade;
    318             // Downgrade unused packages.
    319             if (unusedPackages.contains(pkg) && shouldDowngrade) {
    320                 // This applies for system apps or if packages location is not a directory, i.e.
    321                 // monolithic install.
    322                 if (is_for_primary_dex && !pm.canHaveOatDir(pkg)) {
    323                     // For apps that don't have the oat directory, instead of downgrading,
    324                     // remove their compiler artifacts from dalvik cache.
    325                     pm.deleteOatArtifactsOfPackage(pkg);
    326                     continue;
    327                 } else {
    328                     reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
    329                     downgrade = true;
    330                 }
    331             } else if (abort_code != OPTIMIZE_ABORT_NO_SPACE_LEFT) {
    332                 reason = PackageManagerService.REASON_BACKGROUND_DEXOPT;
    333                 downgrade = false;
    334             } else {
    335                 // can't dexopt because of low space.
    336                 continue;
    337             }
    338 
    339             synchronized (failedPackageNames) {
    340                 // Conservatively add package to the list of failing ones in case
    341                 // performDexOpt never returns.
    342                 failedPackageNames.add(pkg);
    343             }
    344 
    345             // Optimize package if needed. Note that there can be no race between
    346             // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
    347             boolean success;
    348             int dexoptFlags =
    349                     DexoptOptions.DEXOPT_CHECK_FOR_PROFILES_UPDATES |
    350                     DexoptOptions.DEXOPT_BOOT_COMPLETE |
    351                     (downgrade ? DexoptOptions.DEXOPT_DOWNGRADE : 0) |
    352                     DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB;
    353             if (is_for_primary_dex) {
    354                 int result = pm.performDexOptWithStatus(new DexoptOptions(pkg, reason,
    355                         dexoptFlags));
    356                 success = result != PackageDexOptimizer.DEX_OPT_FAILED;
    357                 if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) {
    358                     updatedPackages.add(pkg);
    359                 }
    360             } else {
    361                 success = pm.performDexOpt(new DexoptOptions(pkg,
    362                         reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX));
    363             }
    364             if (success) {
    365                 // Dexopt succeeded, remove package from the list of failing ones.
    366                 synchronized (failedPackageNames) {
    367                     failedPackageNames.remove(pkg);
    368                 }
    369             }
    370         }
    371         notifyPinService(updatedPackages);
    372         return OPTIMIZE_PROCESSED;
    373     }
    374 
    375     private int reconcileSecondaryDexFiles(DexManager dm) {
    376         // TODO(calin): should we blacklist packages for which we fail to reconcile?
    377         for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
    378             if (mAbortIdleOptimization.get()) {
    379                 return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
    380             }
    381             dm.reconcileSecondaryDexFiles(p);
    382         }
    383         return OPTIMIZE_PROCESSED;
    384     }
    385 
    386     // Evaluate whether or not idle optimizations should continue.
    387     private int abortIdleOptimizations(long lowStorageThreshold) {
    388         if (mAbortIdleOptimization.get()) {
    389             // JobScheduler requested an early abort.
    390             return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
    391         }
    392         long usableSpace = mDataDir.getUsableSpace();
    393         if (usableSpace < lowStorageThreshold) {
    394             // Rather bail than completely fill up the disk.
    395             Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
    396             return OPTIMIZE_ABORT_NO_SPACE_LEFT;
    397         }
    398 
    399         return OPTIMIZE_CONTINUE;
    400     }
    401 
    402     // Evaluate whether apps should be downgraded.
    403     private boolean shouldDowngrade(long lowStorageThresholdForDowngrade) {
    404         long usableSpace = mDataDir.getUsableSpace();
    405         if (usableSpace < lowStorageThresholdForDowngrade) {
    406             return true;
    407         }
    408 
    409         return false;
    410     }
    411 
    412     /**
    413      * Execute idle optimizations immediately on packages in packageNames. If packageNames is null,
    414      * then execute on all packages.
    415      */
    416     public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context,
    417             @Nullable List<String> packageNames) {
    418         // Create a new object to make sure we don't interfere with the scheduled jobs.
    419         // Note that this may still run at the same time with the job scheduled by the
    420         // JobScheduler but the scheduler will not be able to cancel it.
    421         BackgroundDexOptService bdos = new BackgroundDexOptService();
    422         ArraySet<String> packagesToOptimize;
    423         if (packageNames == null) {
    424             packagesToOptimize = pm.getOptimizablePackages();
    425         } else {
    426             packagesToOptimize = new ArraySet<>(packageNames);
    427         }
    428         int result = bdos.idleOptimization(pm, packagesToOptimize, context);
    429         return result == OPTIMIZE_PROCESSED;
    430     }
    431 
    432     @Override
    433     public boolean onStartJob(JobParameters params) {
    434         if (DEBUG_DEXOPT) {
    435             Log.i(TAG, "onStartJob");
    436         }
    437 
    438         // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
    439         // the checks above. This check is not "live" - the value is determined by a background
    440         // restart with a period of ~1 minute.
    441         PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
    442         if (pm.isStorageLow()) {
    443             if (DEBUG_DEXOPT) {
    444                 Log.i(TAG, "Low storage, skipping this run");
    445             }
    446             return false;
    447         }
    448 
    449         final ArraySet<String> pkgs = pm.getOptimizablePackages();
    450         if (pkgs.isEmpty()) {
    451             if (DEBUG_DEXOPT) {
    452                 Log.i(TAG, "No packages to optimize");
    453             }
    454             return false;
    455         }
    456 
    457         boolean result;
    458         if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
    459             result = runPostBootUpdate(params, pm, pkgs);
    460         } else {
    461             result = runIdleOptimization(params, pm, pkgs);
    462         }
    463 
    464         return result;
    465     }
    466 
    467     @Override
    468     public boolean onStopJob(JobParameters params) {
    469         if (DEBUG_DEXOPT) {
    470             Log.i(TAG, "onStopJob");
    471         }
    472 
    473         if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
    474             mAbortPostBootUpdate.set(true);
    475 
    476             // Do not reschedule.
    477             // TODO: We should reschedule if we didn't process all apps, yet.
    478             return false;
    479         } else {
    480             mAbortIdleOptimization.set(true);
    481 
    482             // Reschedule the run.
    483             // TODO: Should this be dependent on the stop reason?
    484             return true;
    485         }
    486     }
    487 
    488     private void notifyPinService(ArraySet<String> updatedPackages) {
    489         PinnerService pinnerService = LocalServices.getService(PinnerService.class);
    490         if (pinnerService != null) {
    491             Log.i(TAG, "Pinning optimized code " + updatedPackages);
    492             pinnerService.update(updatedPackages);
    493         }
    494     }
    495 
    496     private static long getDowngradeUnusedAppsThresholdInMillis() {
    497         final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days";
    498         String sysPropValue = SystemProperties.get(sysPropKey);
    499         if (sysPropValue == null || sysPropValue.isEmpty()) {
    500             Log.w(TAG, "SysProp " + sysPropKey + " not set");
    501             return Long.MAX_VALUE;
    502         }
    503         return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
    504     }
    505 
    506     private static boolean isBackgroundDexoptDisabled() {
    507         return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
    508                 false /* default */);
    509     }
    510 }
    511