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