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 17package com.android.server.pm; 18 19import static com.android.server.pm.PackageManagerService.DEBUG_DEXOPT; 20 21import android.app.job.JobInfo; 22import android.app.job.JobParameters; 23import android.app.job.JobScheduler; 24import android.app.job.JobService; 25import android.content.ComponentName; 26import android.content.Context; 27import android.content.Intent; 28import android.content.IntentFilter; 29import android.os.BatteryManager; 30import android.os.Environment; 31import android.os.ServiceManager; 32import android.os.SystemProperties; 33import android.os.storage.StorageManager; 34import android.util.ArraySet; 35import android.util.Log; 36 37import com.android.server.pm.dex.DexManager; 38import com.android.server.LocalServices; 39import com.android.server.PinnerService; 40import com.android.server.pm.dex.DexoptOptions; 41 42import java.io.File; 43import java.util.Set; 44import java.util.concurrent.atomic.AtomicBoolean; 45import java.util.concurrent.TimeUnit; 46 47/** 48 * {@hide} 49 */ 50public 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