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