BackgroundDexOptService.java revision 3a2b7f7d59da9f54d9de4afbbfeebcfb4a3866f2
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() {
128        @SuppressWarnings("deprecation")
129        final long lowThreshold = StorageManager.from(this).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();
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(jobParams, pm, pkgs);
210            }
211        }.start();
212        return true;
213    }
214
215    private void idleOptimization(JobParameters jobParams, PackageManagerService pm,
216            ArraySet<String> pkgs) {
217        Log.i(TAG, "Performing idle optimizations");
218        // If post-boot update is still running, request that it exits early.
219        mExitPostBootUpdate.set(true);
220
221        mAbortIdleOptimization.set(false);
222
223        final long lowThreshold = getLowStorageThreshold();
224        for (String pkg : pkgs) {
225            if (mAbortIdleOptimization.get()) {
226                // JobScheduler requested an early abort.
227                return;
228            }
229
230            synchronized (sFailedPackageNames) {
231                if (sFailedPackageNames.contains(pkg)) {
232                    // Skip previously failing package
233                    continue;
234                }
235            }
236
237            long usableSpace = mDataDir.getUsableSpace();
238            if (usableSpace < lowThreshold) {
239                // Rather bail than completely fill up the disk.
240                Log.w(TAG, "Aborting background dex opt job due to low storage: " +
241                        usableSpace);
242                break;
243            }
244
245            // Conservatively add package to the list of failing ones in case performDexOpt
246            // never returns.
247            synchronized (sFailedPackageNames) {
248                sFailedPackageNames.add(pkg);
249            }
250            // Optimize package if needed. Note that there can be no race between
251            // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
252            if (pm.performDexOpt(pkg,
253                    /* checkProfiles */ true,
254                    PackageManagerService.REASON_BACKGROUND_DEXOPT,
255                    /* force */ false)) {
256                // Dexopt succeeded, remove package from the list of failing ones.
257                synchronized (sFailedPackageNames) {
258                    sFailedPackageNames.remove(pkg);
259                }
260            }
261        }
262        // Ran to completion, so we abandon our timeslice and do not reschedule.
263        jobFinished(jobParams, /* reschedule */ false);
264    }
265
266    @Override
267    public boolean onStartJob(JobParameters params) {
268        if (DEBUG_DEXOPT) {
269            Log.i(TAG, "onStartJob");
270        }
271
272        // NOTE: PackageManagerService.isStorageLow uses a different set of criteria from
273        // the checks above. This check is not "live" - the value is determined by a background
274        // restart with a period of ~1 minute.
275        PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
276        if (pm.isStorageLow()) {
277            if (DEBUG_DEXOPT) {
278                Log.i(TAG, "Low storage, skipping this run");
279            }
280            return false;
281        }
282
283        final ArraySet<String> pkgs = pm.getOptimizablePackages();
284        if (pkgs == null || pkgs.isEmpty()) {
285            if (DEBUG_DEXOPT) {
286                Log.i(TAG, "No packages to optimize");
287            }
288            return false;
289        }
290
291        if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
292            return runPostBootUpdate(params, pm, pkgs);
293        } else {
294            return runIdleOptimization(params, pm, pkgs);
295        }
296    }
297
298    @Override
299    public boolean onStopJob(JobParameters params) {
300        if (DEBUG_DEXOPT) {
301            Log.i(TAG, "onStopJob");
302        }
303
304        if (params.getJobId() == JOB_POST_BOOT_UPDATE) {
305            mAbortPostBootUpdate.set(true);
306        } else {
307            mAbortIdleOptimization.set(true);
308        }
309        return false;
310    }
311}
312