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