BackgroundDexOptService.java revision 91d40f1baa2eeed2aa68d2492e20650d5e71dab5
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.AlarmManager;
22import android.app.job.JobInfo;
23import android.app.job.JobParameters;
24import android.app.job.JobScheduler;
25import android.app.job.JobService;
26import android.content.ComponentName;
27import android.content.Context;
28import android.content.Intent;
29import android.content.IntentFilter;
30import android.os.BatteryManager;
31import android.os.Environment;
32import android.os.ServiceManager;
33import android.os.SystemProperties;
34import android.os.storage.StorageManager;
35import android.util.ArraySet;
36import android.util.Log;
37
38import com.android.server.pm.dex.DexManager;
39
40import java.io.File;
41import java.util.concurrent.atomic.AtomicBoolean;
42import java.util.concurrent.TimeUnit;
43
44/**
45 * {@hide}
46 */
47public class BackgroundDexOptService extends JobService {
48    private static final String TAG = "BackgroundDexOptService";
49
50    private static final boolean DEBUG = false;
51
52    private static final int JOB_IDLE_OPTIMIZE = 800;
53    private static final int JOB_POST_BOOT_UPDATE = 801;
54
55    private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG
56            ? TimeUnit.MINUTES.toMillis(1)
57            : TimeUnit.DAYS.toMillis(1);
58
59    private static ComponentName sDexoptServiceName = new ComponentName(
60            "android",
61            BackgroundDexOptService.class.getName());
62
63    // Possible return codes of individual optimization steps.
64
65    // Optimizations finished. All packages were processed.
66    private static final int OPTIMIZE_PROCESSED = 0;
67    // Optimizations should continue. Issued after checking the scheduler, disk space or battery.
68    private static final int OPTIMIZE_CONTINUE = 1;
69    // Optimizations should be aborted. Job scheduler requested it.
70    private static final int OPTIMIZE_ABORT_BY_JOB_SCHEDULER = 2;
71    // Optimizations should be aborted. No space left on device.
72    private static final int OPTIMIZE_ABORT_NO_SPACE_LEFT = 3;
73
74    /**
75     * Set of failed packages remembered across job runs.
76     */
77    static final ArraySet<String> sFailedPackageNamesPrimary = new ArraySet<String>();
78    static final ArraySet<String> sFailedPackageNamesSecondary = new ArraySet<String>();
79
80    /**
81     * Atomics set to true if the JobScheduler requests an abort.
82     */
83    private final AtomicBoolean mAbortPostBootUpdate = new AtomicBoolean(false);
84    private final AtomicBoolean mAbortIdleOptimization = new AtomicBoolean(false);
85
86    /**
87     * Atomic set to true if one job should exit early because another job was started.
88     */
89    private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
90
91    private final File mDataDir = Environment.getDataDirectory();
92
93    public static void schedule(Context context) {
94        JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
95
96        // Schedule a one-off job which scans installed packages and updates
97        // out-of-date oat files.
98        js.schedule(new JobInfo.Builder(JOB_POST_BOOT_UPDATE, sDexoptServiceName)
99                    .setMinimumLatency(TimeUnit.MINUTES.toMillis(1))
100                    .setOverrideDeadline(TimeUnit.MINUTES.toMillis(1))
101                    .build());
102
103        // Schedule a daily job which scans installed packages and compiles
104        // those with fresh profiling data.
105        js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName)
106                    .setRequiresDeviceIdle(true)
107                    .setRequiresCharging(true)
108                    .setPeriodic(IDLE_OPTIMIZATION_PERIOD)
109                    .build());
110
111        if (DEBUG_DEXOPT) {
112            Log.i(TAG, "Jobs scheduled");
113        }
114    }
115
116    public static void notifyPackageChanged(String packageName) {
117        // The idle maintanance job skips packages which previously failed to
118        // compile. The given package has changed and may successfully compile
119        // now. Remove it from the list of known failing packages.
120        synchronized (sFailedPackageNamesPrimary) {
121            sFailedPackageNamesPrimary.remove(packageName);
122        }
123        synchronized (sFailedPackageNamesSecondary) {
124            sFailedPackageNamesSecondary.remove(packageName);
125        }
126    }
127
128    // Returns the current battery level as a 0-100 integer.
129    private int getBatteryLevel() {
130        IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
131        Intent intent = registerReceiver(null, filter);
132        int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
133        int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
134
135        if (level < 0 || scale <= 0) {
136            // Battery data unavailable. This should never happen, so assume the worst.
137            return 0;
138        }
139
140        return (100 * level / scale);
141    }
142
143    private long getLowStorageThreshold(Context context) {
144        @SuppressWarnings("deprecation")
145        final long lowThreshold = StorageManager.from(context).getStorageLowBytes(mDataDir);
146        if (lowThreshold == 0) {
147            Log.e(TAG, "Invalid low storage threshold");
148        }
149
150        return lowThreshold;
151    }
152
153    private boolean runPostBootUpdate(final JobParameters jobParams,
154            final PackageManagerService pm, final ArraySet<String> pkgs) {
155        if (mExitPostBootUpdate.get()) {
156            // This job has already been superseded. Do not start it.
157            return false;
158        }
159        new Thread("BackgroundDexOptService_PostBootUpdate") {
160            @Override
161            public void run() {
162                postBootUpdate(jobParams, pm, pkgs);
163            }
164
165        }.start();
166        return true;
167    }
168
169    private void postBootUpdate(JobParameters jobParams, PackageManagerService pm,
170            ArraySet<String> pkgs) {
171        // Load low battery threshold from the system config. This is a 0-100 integer.
172        final int lowBatteryThreshold = getResources().getInteger(
173                com.android.internal.R.integer.config_lowBatteryWarningLevel);
174        final long lowThreshold = getLowStorageThreshold(this);
175
176        mAbortPostBootUpdate.set(false);
177
178        for (String pkg : pkgs) {
179            if (mAbortPostBootUpdate.get()) {
180                // JobScheduler requested an early abort.
181                return;
182            }
183            if (mExitPostBootUpdate.get()) {
184                // Different job, which supersedes this one, is running.
185                break;
186            }
187            if (getBatteryLevel() < lowBatteryThreshold) {
188                // Rather bail than completely drain the battery.
189                break;
190            }
191            long usableSpace = mDataDir.getUsableSpace();
192            if (usableSpace < lowThreshold) {
193                // Rather bail than completely fill up the disk.
194                Log.w(TAG, "Aborting background dex opt job due to low storage: " +
195                        usableSpace);
196                break;
197            }
198
199            if (DEBUG_DEXOPT) {
200                Log.i(TAG, "Updating package " + pkg);
201            }
202
203            // Update package if needed. Note that there can be no race between concurrent
204            // jobs because PackageDexOptimizer.performDexOpt is synchronized.
205
206            // checkProfiles is false to avoid merging profiles during boot which
207            // might interfere with background compilation (b/28612421).
208            // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will
209            // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a
210            // trade-off worth doing to save boot time work.
211            pm.performDexOpt(pkg,
212                    /* checkProfiles */ false,
213                    PackageManagerService.REASON_BOOT,
214                    /* force */ false);
215        }
216        // Ran to completion, so we abandon our timeslice and do not reschedule.
217        jobFinished(jobParams, /* reschedule */ false);
218    }
219
220    private boolean runIdleOptimization(final JobParameters jobParams,
221            final PackageManagerService pm, final ArraySet<String> pkgs) {
222        new Thread("BackgroundDexOptService_IdleOptimization") {
223            @Override
224            public void run() {
225                int result = idleOptimization(pm, pkgs, BackgroundDexOptService.this);
226                if (result != OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
227                    Log.w(TAG, "Idle optimizations aborted because of space constraints.");
228                    // If we didn't abort we ran to completion (or stopped because of space).
229                    // Abandon our timeslice and do not reschedule.
230                    jobFinished(jobParams, /* reschedule */ false);
231                }
232            }
233        }.start();
234        return true;
235    }
236
237    // Optimize the given packages and return the optimization result (one of the OPTIMIZE_* codes).
238    private int idleOptimization(PackageManagerService pm, ArraySet<String> pkgs, Context context) {
239        Log.i(TAG, "Performing idle optimizations");
240        // If post-boot update is still running, request that it exits early.
241        mExitPostBootUpdate.set(true);
242        mAbortIdleOptimization.set(false);
243
244        long lowStorageThreshold = getLowStorageThreshold(context);
245        // Optimize primary apks.
246        int result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ true,
247                sFailedPackageNamesPrimary);
248
249        if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
250            return result;
251        }
252
253        if (SystemProperties.getBoolean("dalvik.vm.dexopt.secondary", false)) {
254            result = reconcileSecondaryDexFiles(pm.getDexManager());
255            if (result == OPTIMIZE_ABORT_BY_JOB_SCHEDULER) {
256                return result;
257            }
258
259            result = optimizePackages(pm, pkgs, lowStorageThreshold, /*is_for_primary_dex*/ false,
260                    sFailedPackageNamesSecondary);
261        }
262        return result;
263    }
264
265    private int optimizePackages(PackageManagerService pm, ArraySet<String> pkgs,
266            long lowStorageThreshold, boolean is_for_primary_dex,
267            ArraySet<String> failedPackageNames) {
268        for (String pkg : pkgs) {
269            int abort_code = abortIdleOptimizations(lowStorageThreshold);
270            if (abort_code != OPTIMIZE_CONTINUE) {
271                return abort_code;
272            }
273
274            synchronized (failedPackageNames) {
275                if (failedPackageNames.contains(pkg)) {
276                    // Skip previously failing package
277                    continue;
278                } else {
279                    // Conservatively add package to the list of failing ones in case performDexOpt
280                    // never returns.
281                    failedPackageNames.add(pkg);
282                }
283            }
284
285            // Optimize package if needed. Note that there can be no race between
286            // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
287            boolean success = is_for_primary_dex
288                    ? pm.performDexOpt(pkg,
289                            /* checkProfiles */ true,
290                            PackageManagerService.REASON_BACKGROUND_DEXOPT,
291                            /* force */ false)
292                    : pm.performDexOptSecondary(pkg,
293                            PackageManagerServiceCompilerMapping.getFullCompilerFilter(),
294                            /* force */ true);
295            if (success) {
296                // Dexopt succeeded, remove package from the list of failing ones.
297                synchronized (failedPackageNames) {
298                    failedPackageNames.remove(pkg);
299                }
300            }
301        }
302        return OPTIMIZE_PROCESSED;
303    }
304
305    private int reconcileSecondaryDexFiles(DexManager dm) {
306        // TODO(calin): should we blacklist packages for which we fail to reconcile?
307        for (String p : dm.getAllPackagesWithSecondaryDexFiles()) {
308            if (mAbortIdleOptimization.get()) {
309                return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
310            }
311            dm.reconcileSecondaryDexFiles(p);
312        }
313        return OPTIMIZE_PROCESSED;
314    }
315
316    // Evaluate whether or not idle optimizations should continue.
317    private int abortIdleOptimizations(long lowStorageThreshold) {
318        if (mAbortIdleOptimization.get()) {
319            // JobScheduler requested an early abort.
320            return OPTIMIZE_ABORT_BY_JOB_SCHEDULER;
321        }
322        long usableSpace = mDataDir.getUsableSpace();
323        if (usableSpace < lowStorageThreshold) {
324            // Rather bail than completely fill up the disk.
325            Log.w(TAG, "Aborting background dex opt job due to low storage: " + usableSpace);
326            return OPTIMIZE_ABORT_NO_SPACE_LEFT;
327        }
328
329        return OPTIMIZE_CONTINUE;
330    }
331
332    /**
333     * Execute the idle optimizations immediately.
334     */
335    public static boolean runIdleOptimizationsNow(PackageManagerService pm, Context context) {
336        // Create a new object to make sure we don't interfere with the scheduled jobs.
337        // Note that this may still run at the same time with the job scheduled by the
338        // JobScheduler but the scheduler will not be able to cancel it.
339        BackgroundDexOptService bdos = new BackgroundDexOptService();
340        int result = bdos.idleOptimization(pm, pm.getOptimizablePackages(), context);
341        return result == OPTIMIZE_PROCESSED;
342    }
343
344    @Override
345    public boolean onStartJob(JobParameters params) {
346        if (DEBUG_DEXOPT) {
347            Log.i(TAG, "onStartJob");
348        }
349
350        // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
351        // the checks above. This check is not "live" - the value is determined by a background
352        // restart with a period of ~1 minute.
353        PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
354        if (pm.isStorageLow()) {
355            if (DEBUG_DEXOPT) {
356                Log.i(TAG, "Low storage, skipping this run");
357            }
358            return false;
359        }
360
361        final ArraySet<String> pkgs = pm.getOptimizablePackages();
362        if (pkgs.isEmpty()) {
363            if (DEBUG_DEXOPT) {
364                Log.i(TAG, "No packages to optimize");
365            }
366            return false;
367        }
368
369        if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
370            return runPostBootUpdate(params, pm, pkgs);
371        } else {
372            return runIdleOptimization(params, pm, pkgs);
373        }
374    }
375
376    @Override
377    public boolean onStopJob(JobParameters params) {
378        if (DEBUG_DEXOPT) {
379            Log.i(TAG, "onStopJob");
380        }
381
382        if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
383            mAbortPostBootUpdate.set(true);
384        } else {
385            mAbortIdleOptimization.set(true);
386        }
387        return false;
388    }
389}
390