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