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.AlarmManager; 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.ServiceManager; 32 import android.util.ArraySet; 33 import android.util.Log; 34 35 import java.util.concurrent.atomic.AtomicBoolean; 36 import java.util.concurrent.TimeUnit; 37 38 /** 39 * {@hide} 40 */ 41 public class BackgroundDexOptService extends JobService { 42 static final String TAG = "BackgroundDexOptService"; 43 44 static final long RETRY_LATENCY = 4 * AlarmManager.INTERVAL_HOUR; 45 46 static final int JOB_IDLE_OPTIMIZE = 800; 47 static final int JOB_POST_BOOT_UPDATE = 801; 48 49 private static ComponentName sDexoptServiceName = new ComponentName( 50 "android", 51 BackgroundDexOptService.class.getName()); 52 53 /** 54 * Set of failed packages remembered across job runs. 55 */ 56 static final ArraySet<String> sFailedPackageNames = new ArraySet<String>(); 57 58 /** 59 * Atomics set to true if the JobScheduler requests an abort. 60 */ 61 final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false); 62 final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false); 63 64 /** 65 * Atomic set to true if one job should exit early because another job was started. 66 */ 67 final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false); 68 schedule(Context context)69 public static void schedule(Context context) { 70 JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); 71 72 // Schedule a one-off job which scans installed packages and updates 73 // out-of-date oat files. 74 js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName) 75 .setMinimumLatency(TimeUnit.MINUTES.toMillis(1)) 76 .setOverrideDeadline(TimeUnit.MINUTES.toMillis(1)) 77 .build()); 78 79 // Schedule a daily job which scans installed packages and compiles 80 // those with fresh profiling data. 81 js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName) 82 .setRequiresDeviceIdle(true) 83 .setRequiresCharging(true) 84 .setPeriodic(TimeUnit.DAYS.toMillis(1)) 85 .build()); 86 87 if (DEBUG_DEXOPT) { 88 Log.i(TAG, "Jobs scheduled"); 89 } 90 } 91 notifyPackageChanged(String packageName)92 public static void notifyPackageChanged(String packageName) { 93 // The idle maintanance job skips packages which previously failed to 94 // compile. The given package has changed and may successfully compile 95 // now. Remove it from the list of known failing packages. 96 synchronized (sFailedPackageNames) { 97 sFailedPackageNames.remove(packageName); 98 } 99 } 100 101 // Returns the current battery level as a 0-100 integer. getBatteryLevel()102 private int getBatteryLevel() { 103 IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); 104 Intent intent = registerReceiver(null, filter); 105 int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); 106 int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); 107 108 if (level < 0 || scale <= 0) { 109 // Battery data unavailable. This should never happen, so assume the worst. 110 return 0; 111 } 112 113 return (100 * level / scale); 114 } 115 runPostBootUpdate(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)116 private boolean runPostBootUpdate(final JobParameters jobParams, 117 final PackageManagerService pm, final ArraySet<String> pkgs) { 118 if (mExitPostBootUpdate.get()) { 119 // This job has already been superseded. Do not start it. 120 return false; 121 } 122 123 // Load low battery threshold from the system config. This is a 0-100 integer. 124 final int lowBatteryThreshold = getResources().getInteger( 125 com.android.internal.R.integer.config_lowBatteryWarningLevel); 126 127 mAbortPostBootUpdate.set(false); 128 new Thread("BackgroundDexOptService_PostBootUpdate") { 129 @Override 130 public void run() { 131 for (String pkg : pkgs) { 132 if (mAbortPostBootUpdate.get()) { 133 // JobScheduler requested an early abort. 134 return; 135 } 136 if (mExitPostBootUpdate.get()) { 137 // Different job, which supersedes this one, is running. 138 break; 139 } 140 if (getBatteryLevel() < lowBatteryThreshold) { 141 // Rather bail than completely drain the battery. 142 break; 143 } 144 if (DEBUG_DEXOPT) { 145 Log.i(TAG, "Updating package " + pkg); 146 } 147 148 // Update package if needed. Note that there can be no race between concurrent 149 // jobs because PackageDexOptimizer.performDexOpt is synchronized. 150 151 // checkProfiles is false to avoid merging profiles during boot which 152 // might interfere with background compilation (b/28612421). 153 // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will 154 // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a 155 // trade-off worth doing to save boot time work. 156 pm.performDexOpt(pkg, 157 /* checkProfiles */ false, 158 PackageManagerService.REASON_BOOT, 159 /* force */ false); 160 } 161 // Ran to completion, so we abandon our timeslice and do not reschedule. 162 jobFinished(jobParams, /* reschedule */ false); 163 } 164 }.start(); 165 return true; 166 } 167 runIdleOptimization(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)168 private boolean runIdleOptimization(final JobParameters jobParams, 169 final PackageManagerService pm, final ArraySet<String> pkgs) { 170 // If post-boot update is still running, request that it exits early. 171 mExitPostBootUpdate.set(true); 172 173 mAbortIdleOptimization.set(false); 174 new Thread("BackgroundDexOptService_IdleOptimization") { 175 @Override 176 public void run() { 177 for (String pkg : pkgs) { 178 if (mAbortIdleOptimization.get()) { 179 // JobScheduler requested an early abort. 180 return; 181 } 182 if (sFailedPackageNames.contains(pkg)) { 183 // Skip previously failing package 184 continue; 185 } 186 // Conservatively add package to the list of failing ones in case performDexOpt 187 // never returns. 188 synchronized (sFailedPackageNames) { 189 sFailedPackageNames.add(pkg); 190 } 191 // Optimize package if needed. Note that there can be no race between 192 // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. 193 if (pm.performDexOpt(pkg, 194 /* checkProfiles */ true, 195 PackageManagerService.REASON_BACKGROUND_DEXOPT, 196 /* force */ false)) { 197 // Dexopt succeeded, remove package from the list of failing ones. 198 synchronized (sFailedPackageNames) { 199 sFailedPackageNames.remove(pkg); 200 } 201 } 202 } 203 // Ran to completion, so we abandon our timeslice and do not reschedule. 204 jobFinished(jobParams, /* reschedule */ false); 205 } 206 }.start(); 207 return true; 208 } 209 210 @Override onStartJob(JobParameters params)211 public boolean onStartJob(JobParameters params) { 212 if (DEBUG_DEXOPT) { 213 Log.i(TAG, "onStartJob"); 214 } 215 216 PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package"); 217 if (pm.isStorageLow()) { 218 if (DEBUG_DEXOPT) { 219 Log.i(TAG, "Low storage, skipping this run"); 220 } 221 return false; 222 } 223 224 final ArraySet<String> pkgs = pm.getOptimizablePackages(); 225 if (pkgs == null || pkgs.isEmpty()) { 226 if (DEBUG_DEXOPT) { 227 Log.i(TAG, "No packages to optimize"); 228 } 229 return false; 230 } 231 232 if (params.getJobId() == JOB_POST_BOOT_UPDATE) { 233 return runPostBootUpdate(params, pm, pkgs); 234 } else { 235 return runIdleOptimization(params, pm, pkgs); 236 } 237 } 238 239 @Override onStopJob(JobParameters params)240 public boolean onStopJob(JobParameters params) { 241 if (DEBUG_DEXOPT) { 242 Log.i(TAG, "onStopJob"); 243 } 244 245 if (params.getJobId() == JOB_POST_BOOT_UPDATE) { 246 mAbortPostBootUpdate.set(true); 247 } else { 248 mAbortIdleOptimization.set(true); 249 } 250 return false; 251 } 252 } 253