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