BackgroundDexOptService.java revision 31ce3a8a56e78ecc17d9befbc64a1e529b6b78e9
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;
39import com.android.server.LocalServices;
40import com.android.server.PinnerService;
41
42import java.io.File;
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    /**
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
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
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.
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
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
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
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
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).
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
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
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.
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     */
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
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
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
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