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 schedule(Context context)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 notifyPackageChanged(String packageName)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. getBatteryLevel()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 getLowStorageThreshold(Context context)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 runPostBootUpdate(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)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 postBootUpdate(JobParameters jobParams, PackageManagerService pm, ArraySet<String> pkgs)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 runIdleOptimization(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)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). idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context)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 optimizePackages(PackageManagerService pm, ArraySet<String> pkgs, long lowStorageThreshold, boolean is_for_primary_dex, ArraySet<String> failedPackageNames)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 reconcileSecondaryDexFiles(DexManager dm)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. abortIdleOptimizations(long lowStorageThreshold)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. shouldDowngrade(long lowStorageThresholdForDowngrade)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 */ runIdleOptimizationsNow(PackageManagerService pm, Context context, @Nullable List<String> packageNames)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 onStartJob(JobParameters params)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 onStopJob(JobParameters params)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 notifyPinService(ArraySet<String> updatedPackages)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 getDowngradeUnusedAppsThresholdInMillis()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 isBackgroundDexoptDisabled()506 private static boolean isBackgroundDexoptDisabled() { 507 return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */, 508 false /* default */); 509 } 510 } 511