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