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