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