• 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.SystemProperties;
34 import android.os.storage.StorageManager;
35 import android.util.ArraySet;
36 import android.util.Log;
37 
38 import com.android.server.pm.dex.DexManager;
39 import com.android.server.LocalServices;
40 import com.android.server.PinnerService;
41 
42 import java.io.File;
43 import java.util.concurrent.atomic.AtomicBoolean;
44 import java.util.concurrent.TimeUnit;
45 
46 /**
47  * {@hide}
48  */
49 public 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 
schedule(Context context)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 
notifyPackageChanged(String packageName)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.
getBatteryLevel()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 
getLowStorageThreshold(Context context)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 
runPostBootUpdate(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)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 
postBootUpdate(JobParameters jobParams, PackageManagerService pm, ArraySet<String> pkgs)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 
runIdleOptimization(final JobParameters jobParams, final PackageManagerService pm, final ArraySet<String> pkgs)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).
idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context)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 
optimizePackages(PackageManagerService pm, ArraySet<String> pkgs, long lowStorageThreshold, boolean is_for_primary_dex, ArraySet<String> failedPackageNames)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 
reconcileSecondaryDexFiles(DexManager dm)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.
abortIdleOptimizations(long lowStorageThreshold)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      */
runIdleOptimizationsNow(PackageManagerService pm, Context context)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
onStartJob(JobParameters params)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
onStopJob(JobParameters params)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 
notifyPinService(ArraySet<String> updatedPackages)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