OtaDexoptService.java revision c98c7bccdccbff1ceeda501e6a6f8c21d61649ca
1/*
2 * Copyright (C) 2016 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.Installer.DEXOPT_OTA;
20import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
21import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
22import static com.android.server.pm.PackageManagerServiceCompilerMapping.getCompilerFilterForReason;
23
24import android.annotation.Nullable;
25import android.content.Context;
26import android.content.pm.IOtaDexopt;
27import android.content.pm.PackageParser;
28import android.os.Environment;
29import android.os.RemoteException;
30import android.os.ResultReceiver;
31import android.os.ServiceManager;
32import android.os.storage.StorageManager;
33import android.text.TextUtils;
34import android.util.Log;
35import android.util.Slog;
36
37import com.android.internal.logging.MetricsLogger;
38import com.android.internal.os.InstallerConnection.InstallerException;
39
40import java.io.File;
41import java.io.FileDescriptor;
42import java.util.ArrayList;
43import java.util.Arrays;
44import java.util.Collection;
45import java.util.List;
46import java.util.concurrent.TimeUnit;
47
48/**
49 * A service for A/B OTA dexopting.
50 *
51 * {@hide}
52 */
53public class OtaDexoptService extends IOtaDexopt.Stub {
54    private final static String TAG = "OTADexopt";
55    private final static boolean DEBUG_DEXOPT = true;
56
57    // The synthetic library dependencies denoting "no checks."
58    private final static String[] NO_LIBRARIES = new String[] { "&" };
59
60    // The amount of "available" (free - low threshold) space necessary at the start of an OTA to
61    // not bulk-delete unused apps' odex files.
62    private final static long BULK_DELETE_THRESHOLD = 1024 * 1024 * 1024;  // 1GB.
63
64    private final Context mContext;
65    private final PackageManagerService mPackageManagerService;
66
67    // TODO: Evaluate the need for WeakReferences here.
68
69    /**
70     * The list of dexopt invocations for all work.
71     */
72    private List<String> mDexoptCommands;
73
74    private int completeSize;
75
76    // MetricsLogger properties.
77
78    // Space before and after.
79    private long availableSpaceBefore;
80    private long availableSpaceAfterBulkDelete;
81    private long availableSpaceAfterDexopt;
82
83    // Packages.
84    private int importantPackageCount;
85    private int otherPackageCount;
86
87    // Number of dexopt commands. This may be different from the count of packages.
88    private int dexoptCommandCountTotal;
89    private int dexoptCommandCountExecuted;
90
91    // For spent time.
92    private long otaDexoptTimeStart;
93
94
95    public OtaDexoptService(Context context, PackageManagerService packageManagerService) {
96        this.mContext = context;
97        this.mPackageManagerService = packageManagerService;
98
99        // Now it's time to check whether we need to move any A/B artifacts.
100        moveAbArtifacts(packageManagerService.mInstaller);
101    }
102
103    public static OtaDexoptService main(Context context,
104            PackageManagerService packageManagerService) {
105        OtaDexoptService ota = new OtaDexoptService(context, packageManagerService);
106        ServiceManager.addService("otadexopt", ota);
107
108        return ota;
109    }
110
111    @Override
112    public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
113            String[] args, ResultReceiver resultReceiver) throws RemoteException {
114        (new OtaDexoptShellCommand(this)).exec(
115                this, in, out, err, args, resultReceiver);
116    }
117
118    @Override
119    public synchronized void prepare() throws RemoteException {
120        if (mDexoptCommands != null) {
121            throw new IllegalStateException("already called prepare()");
122        }
123        final List<PackageParser.Package> important;
124        final List<PackageParser.Package> others;
125        synchronized (mPackageManagerService.mPackages) {
126            // Important: the packages we need to run with ab-ota compiler-reason.
127            important = PackageManagerServiceUtils.getPackagesForDexopt(
128                    mPackageManagerService.mPackages.values(), mPackageManagerService);
129            // Others: we should optimize this with the (first-)boot compiler-reason.
130            others = new ArrayList<>(mPackageManagerService.mPackages.values());
131            others.removeAll(important);
132
133            // Pre-size the array list by over-allocating by a factor of 1.5.
134            mDexoptCommands = new ArrayList<>(3 * mPackageManagerService.mPackages.size() / 2);
135        }
136
137        for (PackageParser.Package p : important) {
138            // Make sure that core apps are optimized according to their own "reason".
139            // If the core apps are not preopted in the B OTA, and REASON_AB_OTA is not speed
140            // (by default is speed-profile) they will be interepreted/JITed. This in itself is
141            // not a problem as we will end up doing profile guided compilation. However, some
142            // core apps may be loaded by system server which doesn't JIT and we need to make
143            // sure we don't interpret-only
144            int compilationReason = p.coreApp
145                    ? PackageManagerService.REASON_CORE_APP
146                    : PackageManagerService.REASON_AB_OTA;
147            mDexoptCommands.addAll(generatePackageDexopts(p, compilationReason));
148        }
149        for (PackageParser.Package p : others) {
150            // We assume here that there are no core apps left.
151            if (p.coreApp) {
152                throw new IllegalStateException("Found a core app that's not important");
153            }
154            mDexoptCommands.addAll(
155                    generatePackageDexopts(p, PackageManagerService.REASON_FIRST_BOOT));
156        }
157        completeSize = mDexoptCommands.size();
158
159        long spaceAvailable = getAvailableSpace();
160        if (spaceAvailable < BULK_DELETE_THRESHOLD) {
161            Log.i(TAG, "Low on space, deleting oat files in an attempt to free up space: "
162                    + PackageManagerServiceUtils.packagesToString(others));
163            for (PackageParser.Package pkg : others) {
164                deleteOatArtifactsOfPackage(pkg);
165            }
166        }
167        long spaceAvailableNow = getAvailableSpace();
168
169        prepareMetricsLogging(important.size(), others.size(), spaceAvailable, spaceAvailableNow);
170    }
171
172    @Override
173    public synchronized void cleanup() throws RemoteException {
174        if (DEBUG_DEXOPT) {
175            Log.i(TAG, "Cleaning up OTA Dexopt state.");
176        }
177        mDexoptCommands = null;
178        availableSpaceAfterDexopt = getAvailableSpace();
179
180        performMetricsLogging();
181    }
182
183    @Override
184    public synchronized boolean isDone() throws RemoteException {
185        if (mDexoptCommands == null) {
186            throw new IllegalStateException("done() called before prepare()");
187        }
188
189        return mDexoptCommands.isEmpty();
190    }
191
192    @Override
193    public synchronized float getProgress() throws RemoteException {
194        // Approximate the progress by the amount of already completed commands.
195        if (completeSize == 0) {
196            return 1f;
197        }
198        int commandsLeft = mDexoptCommands.size();
199        return (completeSize - commandsLeft) / ((float)completeSize);
200    }
201
202    @Override
203    public synchronized String nextDexoptCommand() throws RemoteException {
204        if (mDexoptCommands == null) {
205            throw new IllegalStateException("dexoptNextPackage() called before prepare()");
206        }
207
208        if (mDexoptCommands.isEmpty()) {
209            return "(all done)";
210        }
211
212        String next = mDexoptCommands.remove(0);
213
214        if (getAvailableSpace() > 0) {
215            dexoptCommandCountExecuted++;
216
217            return next;
218        } else {
219            if (DEBUG_DEXOPT) {
220                Log.w(TAG, "Not enough space for OTA dexopt, stopping with "
221                        + (mDexoptCommands.size() + 1) + " commands left.");
222            }
223            mDexoptCommands.clear();
224            return "(no free space)";
225        }
226    }
227
228    private long getMainLowSpaceThreshold() {
229        File dataDir = Environment.getDataDirectory();
230        @SuppressWarnings("deprecation")
231        long lowThreshold = StorageManager.from(mContext).getStorageLowBytes(dataDir);
232        if (lowThreshold == 0) {
233            throw new IllegalStateException("Invalid low memory threshold");
234        }
235        return lowThreshold;
236    }
237
238    /**
239     * Returns the difference of free space to the low-storage-space threshold. Positive values
240     * indicate free bytes.
241     */
242    private long getAvailableSpace() {
243        // TODO: If apps are not installed in the internal /data partition, we should compare
244        //       against that storage's free capacity.
245        long lowThreshold = getMainLowSpaceThreshold();
246
247        File dataDir = Environment.getDataDirectory();
248        long usableSpace = dataDir.getUsableSpace();
249
250        return usableSpace - lowThreshold;
251    }
252
253    private static String getOatDir(PackageParser.Package pkg) {
254        if (!pkg.canHaveOatDir()) {
255            return null;
256        }
257        File codePath = new File(pkg.codePath);
258        if (codePath.isDirectory()) {
259            return PackageDexOptimizer.getOatDir(codePath).getAbsolutePath();
260        }
261        return null;
262    }
263
264    private void deleteOatArtifactsOfPackage(PackageParser.Package pkg) {
265        String[] instructionSets = getAppDexInstructionSets(pkg.applicationInfo);
266        for (String codePath : pkg.getAllCodePaths()) {
267            for (String isa : instructionSets) {
268                try {
269                    mPackageManagerService.mInstaller.deleteOdex(codePath, isa, getOatDir(pkg));
270                } catch (InstallerException e) {
271                    Log.e(TAG, "Failed deleting oat files for " + codePath, e);
272                }
273            }
274        }
275    }
276
277    /**
278     * Generate all dexopt commands for the given package.
279     */
280    private synchronized List<String> generatePackageDexopts(PackageParser.Package pkg,
281            int compilationReason) {
282        // Intercept and collect dexopt requests
283        final List<String> commands = new ArrayList<String>();
284        final Installer collectingInstaller = new Installer(mContext, true) {
285            @Override
286            public void dexopt(String apkPath, int uid, @Nullable String pkgName,
287                    String instructionSet, int dexoptNeeded, @Nullable String outputPath,
288                    int dexFlags, String compilerFilter, @Nullable String volumeUuid,
289                    @Nullable String sharedLibraries) throws InstallerException {
290                commands.add(buildCommand("dexopt",
291                        apkPath,
292                        uid,
293                        pkgName,
294                        instructionSet,
295                        dexoptNeeded,
296                        outputPath,
297                        dexFlags,
298                        compilerFilter,
299                        volumeUuid,
300                        sharedLibraries));
301            }
302        };
303
304        // Use the package manager install and install lock here for the OTA dex optimizer.
305        PackageDexOptimizer optimizer = new OTADexoptPackageDexOptimizer(
306                collectingInstaller, mPackageManagerService.mInstallLock, mContext);
307
308        String[] libraryDependencies = pkg.usesLibraryFiles;
309        if (pkg.isSystemApp()) {
310            // For system apps, we want to avoid classpaths checks.
311            libraryDependencies = NO_LIBRARIES;
312        }
313
314        optimizer.performDexOpt(pkg, libraryDependencies,
315                null /* ISAs */, false /* checkProfiles */,
316                getCompilerFilterForReason(compilationReason),
317                null /* CompilerStats.PackageStats */);
318
319        return commands;
320    }
321
322    @Override
323    public synchronized void dexoptNextPackage() throws RemoteException {
324        throw new UnsupportedOperationException();
325    }
326
327    private void moveAbArtifacts(Installer installer) {
328        if (mDexoptCommands != null) {
329            throw new IllegalStateException("Should not be ota-dexopting when trying to move.");
330        }
331
332        // Look into all packages.
333        Collection<PackageParser.Package> pkgs = mPackageManagerService.getPackages();
334        for (PackageParser.Package pkg : pkgs) {
335            if (pkg == null) {
336                continue;
337            }
338
339            // Does the package have code? If not, there won't be any artifacts.
340            if (!PackageDexOptimizer.canOptimizePackage(pkg)) {
341                continue;
342            }
343            if (pkg.codePath == null) {
344                Slog.w(TAG, "Package " + pkg + " can be optimized but has null codePath");
345                continue;
346            }
347
348            // If the path is in /system or /vendor, ignore. It will have been ota-dexopted into
349            // /data/ota and moved into the dalvik-cache already.
350            if (pkg.codePath.startsWith("/system") || pkg.codePath.startsWith("/vendor")) {
351                continue;
352            }
353
354            final String[] instructionSets = getAppDexInstructionSets(pkg.applicationInfo);
355            final List<String> paths = pkg.getAllCodePathsExcludingResourceOnly();
356            final String[] dexCodeInstructionSets = getDexCodeInstructionSets(instructionSets);
357            for (String dexCodeInstructionSet : dexCodeInstructionSets) {
358                for (String path : paths) {
359                    String oatDir = PackageDexOptimizer.getOatDir(new File(pkg.codePath)).
360                            getAbsolutePath();
361
362                    // TODO: Check first whether there is an artifact, to save the roundtrip time.
363
364                    try {
365                        installer.moveAb(path, dexCodeInstructionSet, oatDir);
366                    } catch (InstallerException e) {
367                    }
368                }
369            }
370        }
371    }
372
373    /**
374     * Initialize logging fields.
375     */
376    private void prepareMetricsLogging(int important, int others, long spaceBegin, long spaceBulk) {
377        availableSpaceBefore = spaceBegin;
378        availableSpaceAfterBulkDelete = spaceBulk;
379        availableSpaceAfterDexopt = 0;
380
381        importantPackageCount = important;
382        otherPackageCount = others;
383
384        dexoptCommandCountTotal = mDexoptCommands.size();
385        dexoptCommandCountExecuted = 0;
386
387        otaDexoptTimeStart = System.nanoTime();
388    }
389
390    private static int inMegabytes(long value) {
391        long in_mega_bytes = value / (1024 * 1024);
392        if (in_mega_bytes > Integer.MAX_VALUE) {
393            Log.w(TAG, "Recording " + in_mega_bytes + "MB of free space, overflowing range");
394            return Integer.MAX_VALUE;
395        }
396        return (int)in_mega_bytes;
397    }
398
399    private void performMetricsLogging() {
400        long finalTime = System.nanoTime();
401
402        MetricsLogger.histogram(mContext, "ota_dexopt_available_space_before_mb",
403                inMegabytes(availableSpaceBefore));
404        MetricsLogger.histogram(mContext, "ota_dexopt_available_space_after_bulk_delete_mb",
405                inMegabytes(availableSpaceAfterBulkDelete));
406        MetricsLogger.histogram(mContext, "ota_dexopt_available_space_after_dexopt_mb",
407                inMegabytes(availableSpaceAfterDexopt));
408
409        MetricsLogger.histogram(mContext, "ota_dexopt_num_important_packages",
410                importantPackageCount);
411        MetricsLogger.histogram(mContext, "ota_dexopt_num_other_packages", otherPackageCount);
412
413        MetricsLogger.histogram(mContext, "ota_dexopt_num_commands", dexoptCommandCountTotal);
414        MetricsLogger.histogram(mContext, "ota_dexopt_num_commands_executed",
415                dexoptCommandCountExecuted);
416
417        final int elapsedTimeSeconds =
418                (int) TimeUnit.NANOSECONDS.toSeconds(finalTime - otaDexoptTimeStart);
419        MetricsLogger.histogram(mContext, "ota_dexopt_time_s", elapsedTimeSeconds);
420    }
421
422    private static class OTADexoptPackageDexOptimizer extends
423            PackageDexOptimizer.ForcedUpdatePackageDexOptimizer {
424
425        public OTADexoptPackageDexOptimizer(Installer installer, Object installLock,
426                Context context) {
427            super(installer, installLock, context, "*otadexopt*");
428        }
429
430        @Override
431        protected int adjustDexoptFlags(int dexoptFlags) {
432            // Add the OTA flag.
433            return dexoptFlags | DEXOPT_OTA;
434        }
435
436    }
437
438    /**
439     * Cook up argument list in the format that {@code installd} expects.
440     */
441    private static String buildCommand(Object... args) {
442        final StringBuilder builder = new StringBuilder();
443        for (Object arg : args) {
444            String escaped;
445            if (arg == null) {
446                escaped = "";
447            } else {
448                escaped = String.valueOf(arg);
449            }
450            if (escaped.indexOf('\0') != -1 || escaped.indexOf(' ') != -1 || "!".equals(escaped)) {
451                throw new IllegalArgumentException(
452                        "Invalid argument while executing " + Arrays.toString(args));
453            }
454            if (TextUtils.isEmpty(escaped)) {
455                escaped = "!";
456            }
457            builder.append(' ').append(escaped);
458        }
459        return builder.toString();
460    }
461}
462