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