From 6ea5ace7b3a8ddf19a7389592eb9df3b33c673f3 Mon Sep 17 00:00:00 2001 From: Calin Juravle Date: Thu, 1 Dec 2016 17:53:07 +0000 Subject: [PATCH 1/6] Add logic for recording dex files use on disk Add PackageDexUsage to handle the I/O operations of dex usage data. It is responsible to encode, save and load dex Test: runtest -x .../PackageDexUsageTests.java Bug: 32871170 (cherry picked from commit 0318162abcbd07a0472989df43e00e353fac731b) Change-Id: I6d483d480d62aa14cb1663560fd88092469a2835 --- .../android/server/pm/AbstractStatsBase.java | 6 +- .../server/pm/PackageManagerServiceUtils.java | 15 + .../server/pm/dex/PackageDexUsage.java | 512 ++++++++++++++++++ .../server/pm/dex/PackageDexUsageTests.java | 318 +++++++++++ 4 files changed, 848 insertions(+), 3 deletions(-) create mode 100644 services/core/java/com/android/server/pm/dex/PackageDexUsage.java create mode 100644 services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java diff --git a/services/core/java/com/android/server/pm/AbstractStatsBase.java b/services/core/java/com/android/server/pm/AbstractStatsBase.java index 612c4767ccaa3..0053f588621a9 100644 --- a/services/core/java/com/android/server/pm/AbstractStatsBase.java +++ b/services/core/java/com/android/server/pm/AbstractStatsBase.java @@ -60,12 +60,12 @@ public abstract class AbstractStatsBase { return new AtomicFile(fname); } - void writeNow(final T data) { + protected void writeNow(final T data) { writeImpl(data); mLastTimeWritten.set(SystemClock.elapsedRealtime()); } - boolean maybeWriteAsync(final T data) { + protected boolean maybeWriteAsync(final T data) { if (SystemClock.elapsedRealtime() - mLastTimeWritten.get() < WRITE_INTERVAL_MS && !PackageManagerService.DEBUG_DEXOPT) { return false; @@ -105,7 +105,7 @@ public abstract class AbstractStatsBase { protected abstract void writeInternal(T data); - void read(T data) { + protected void read(T data) { if (mLock) { synchronized (data) { synchronized (mFileLock) { diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index cfd0af7635e82..45887e1c8a3fd 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -24,11 +24,13 @@ import android.app.AppGlobals; import android.content.Intent; import android.content.pm.PackageParser; import android.content.pm.ResolveInfo; +import android.os.Build; import android.os.RemoteException; import android.os.UserHandle; import android.system.ErrnoException; import android.util.ArraySet; import android.util.Log; +import dalvik.system.VMRuntime; import libcore.io.Libcore; import java.io.File; @@ -197,4 +199,17 @@ public class PackageManagerServiceUtils { } return sb.toString(); } + + /** + * Verifies that the given string {@code isa} is a valid supported isa on + * the running device. + */ + public static boolean checkISA(String isa) { + for (String abi : Build.SUPPORTED_ABIS) { + if (VMRuntime.getInstructionSet(abi).equals(isa)) { + return true; + } + } + return false; + } } diff --git a/services/core/java/com/android/server/pm/dex/PackageDexUsage.java b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java new file mode 100644 index 0000000000000..10384a2749e8c --- /dev/null +++ b/services/core/java/com/android/server/pm/dex/PackageDexUsage.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.pm.dex; + +import android.util.AtomicFile; +import android.util.Slog; +import android.os.Build; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.FastPrintWriter; +import com.android.server.pm.AbstractStatsBase; +import com.android.server.pm.PackageManagerServiceUtils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Iterator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import dalvik.system.VMRuntime; +import libcore.io.IoUtils; + +/** + * Stat file which store usage information about dex files. + */ +public class PackageDexUsage extends AbstractStatsBase { + private final static String TAG = "PackageDexUsage"; + + private final static int PACKAGE_DEX_USAGE_VERSION = 1; + private final static String PACKAGE_DEX_USAGE_VERSION_HEADER = + "PACKAGE_MANAGER__PACKAGE_DEX_USAGE__"; + + private final static String SPLIT_CHAR = ","; + private final static String DEX_LINE_CHAR = "#"; + + // Map which structures the information we have on a package. + // Maps package name to package data (which stores info about UsedByOtherApps and + // secondary dex files.). + // Access to this map needs synchronized. + @GuardedBy("mPackageUseInfoMap") + private Map mPackageUseInfoMap; + + public PackageDexUsage() { + super("package-dex-usage.list", "PackageDexUsage_DiskWriter", /*lock*/ false); + mPackageUseInfoMap = new HashMap<>(); + } + + /** + * Record a dex file load. + * + * Note this is called when apps load dex files and as such it should return + * as fast as possible. + * + * @param loadingPackage the package performing the load + * @param dexPath the path of the dex files being loaded + * @param ownerUserId the user id which runs the code loading the dex files + * @param loaderIsa the ISA of the app loading the dex files + * @param isUsedByOtherApps whether or not this dex file was not loaded by its owning package + * @param primaryOrSplit whether or not the dex file is a primary/split dex. True indicates + * the file is either primary or a split. False indicates the file is secondary dex. + * @return true if the dex load constitutes new information, or false if this information + * has been seen before. + */ + public boolean record(String owningPackageName, String dexPath, int ownerUserId, + String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) { + if (!PackageManagerServiceUtils.checkISA(loaderIsa)) { + throw new IllegalArgumentException("loaderIsa " + loaderIsa + " is unsupported"); + } + synchronized (mPackageUseInfoMap) { + PackageUseInfo packageUseInfo = mPackageUseInfoMap.get(owningPackageName); + if (packageUseInfo == null) { + // This is the first time we see the package. + packageUseInfo = new PackageUseInfo(); + if (primaryOrSplit) { + // If we have a primary or a split apk, set isUsedByOtherApps. + // We do not need to record the loaderIsa or the owner because we compile + // primaries for all users and all ISAs. + packageUseInfo.mIsUsedByOtherApps = isUsedByOtherApps; + } else { + // For secondary dex files record the loaderISA and the owner. We'll need + // to know under which user to compile and for what ISA. + packageUseInfo.mDexUseInfoMap.put( + dexPath, new DexUseInfo(isUsedByOtherApps, ownerUserId, loaderIsa)); + } + mPackageUseInfoMap.put(owningPackageName, packageUseInfo); + return true; + } else { + // We already have data on this package. Amend it. + if (primaryOrSplit) { + // We have a possible update on the primary apk usage. Merge + // isUsedByOtherApps information and return if there was an update. + return packageUseInfo.merge(isUsedByOtherApps); + } else { + DexUseInfo newData = new DexUseInfo( + isUsedByOtherApps, ownerUserId, loaderIsa); + DexUseInfo existingData = packageUseInfo.mDexUseInfoMap.get(dexPath); + if (existingData == null) { + // It's the first time we see this dex file. + packageUseInfo.mDexUseInfoMap.put(dexPath, newData); + return true; + } else { + if (ownerUserId != existingData.mOwnerUserId) { + // Oups, this should never happen, the DexManager who calls this should + // do the proper checks and not call record if the user does not own the + // dex path. + // Secondary dex files are stored in the app user directory. A change in + // owningUser for the same path means that something went wrong at some + // higher level, and the loaderUser was allowed to cross + // user-boundaries and access data from what we know to be the owner + // user. + throw new IllegalArgumentException("Trying to change ownerUserId for " + + " dex path " + dexPath + " from " + existingData.mOwnerUserId + + " to " + ownerUserId); + } + // Merge the information into the existing data. + // Returns true if there was an update. + return existingData.merge(newData); + } + } + } + } + } + + /** + * Convenience method for sync reads which does not force the user to pass a useless + * (Void) null. + */ + public void read() { + read((Void) null); + } + + /** + * Convenience method for async writes which does not force the user to pass a useless + * (Void) null. + */ + public void maybeWriteAsync() { + maybeWriteAsync((Void) null); + } + + @Override + protected void writeInternal(Void data) { + AtomicFile file = getFile(); + FileOutputStream f = null; + + try { + f = file.startWrite(); + OutputStreamWriter osw = new OutputStreamWriter(f); + write(osw); + osw.flush(); + file.finishWrite(f); + } catch (IOException e) { + if (f != null) { + file.failWrite(f); + } + Slog.e(TAG, "Failed to write usage for dex files", e); + } + } + + /** + * File format: + * + * file_magic_version + * package_name_1 + * #dex_file_path_1_1 + * user_1_1, used_by_other_app_1_1, user_isa_1_1_1, user_isa_1_1_2 + * #dex_file_path_1_2 + * user_1_2, used_by_other_app_1_2, user_isa_1_2_1, user_isa_1_2_2 + * ... + * package_name_2 + * #dex_file_path_2_1 + * user_2_1, used_by_other_app_2_1, user_isa_2_1_1, user_isa_2_1_2 + * #dex_file_path_2_2, + * user_2_2, used_by_other_app_2_2, user_isa_2_2_1, user_isa_2_2_2 + * ... + */ + /* package */ void write(Writer out) { + // Make a clone to avoid locking while writing to disk. + Map packageUseInfoMapClone = clonePackageUseInfoMap(); + + FastPrintWriter fpw = new FastPrintWriter(out); + + // Write the header. + fpw.print(PACKAGE_DEX_USAGE_VERSION_HEADER); + fpw.println(PACKAGE_DEX_USAGE_VERSION); + + for (Map.Entry pEntry : packageUseInfoMapClone.entrySet()) { + // Write the package line. + String packageName = pEntry.getKey(); + PackageUseInfo packageUseInfo = pEntry.getValue(); + + fpw.println(String.join(SPLIT_CHAR, packageName, + writeBoolean(packageUseInfo.mIsUsedByOtherApps))); + + // Write dex file lines. + for (Map.Entry dEntry : packageUseInfo.mDexUseInfoMap.entrySet()) { + String dexPath = dEntry.getKey(); + DexUseInfo dexUseInfo = dEntry.getValue(); + fpw.println(DEX_LINE_CHAR + dexPath); + fpw.print(String.join(SPLIT_CHAR, Integer.toString(dexUseInfo.mOwnerUserId), + writeBoolean(dexUseInfo.mIsUsedByOtherApps))); + for (String isa : dexUseInfo.mLoaderIsas) { + fpw.print(SPLIT_CHAR + isa); + } + fpw.println(); + } + } + fpw.flush(); + } + + @Override + protected void readInternal(Void data) { + AtomicFile file = getFile(); + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader(file.openRead())); + read(in); + } catch (FileNotFoundException expected) { + // The file may not be there. E.g. When we first take the OTA with this feature. + } catch (IOException e) { + Slog.w(TAG, "Failed to parse package dex usage.", e); + } finally { + IoUtils.closeQuietly(in); + } + } + + /* package */ void read(Reader reader) throws IOException { + Map data = new HashMap<>(); + BufferedReader in = new BufferedReader(reader); + // Read header, do version check. + String versionLine = in.readLine(); + if (versionLine == null) { + throw new IllegalStateException("No version line found."); + } else { + if (!versionLine.startsWith(PACKAGE_DEX_USAGE_VERSION_HEADER)) { + // TODO(calin): the caller is responsible to clear the file. + throw new IllegalStateException("Invalid version line: " + versionLine); + } + int version = Integer.parseInt( + versionLine.substring(PACKAGE_DEX_USAGE_VERSION_HEADER.length())); + if (version != PACKAGE_DEX_USAGE_VERSION) { + throw new IllegalStateException("Unexpected version: " + version); + } + } + + String s = null; + String currentPakage = null; + PackageUseInfo currentPakageData = null; + + Set supportedIsas = new HashSet<>(); + for (String abi : Build.SUPPORTED_ABIS) { + supportedIsas.add(VMRuntime.getInstructionSet(abi)); + } + while ((s = in.readLine()) != null) { + if (s.startsWith(DEX_LINE_CHAR)) { + // This is the start of the the dex lines. + // We expect two lines for each dex entry: + // #dexPaths + // onwerUserId,isUsedByOtherApps,isa1,isa2 + if (currentPakage == null) { + throw new IllegalStateException( + "Malformed PackageDexUsage file. Expected package line before dex line."); + } + + // First line is the dex path. + String dexPath = s.substring(DEX_LINE_CHAR.length()); + // Next line is the dex data. + s = in.readLine(); + if (s == null) { + throw new IllegalStateException("Could not fine dexUseInfo for line: " + s); + } + + // We expect at least 3 elements (isUsedByOtherApps, userId, isa). + String[] elems = s.split(SPLIT_CHAR); + if (elems.length < 3) { + throw new IllegalStateException("Invalid PackageDexUsage line: " + s); + } + int ownerUserId = Integer.parseInt(elems[0]); + boolean isUsedByOtherApps = readBoolean(elems[1]); + DexUseInfo dexUseInfo = new DexUseInfo(isUsedByOtherApps, ownerUserId); + for (int i = 2; i < elems.length; i++) { + String isa = elems[i]; + if (supportedIsas.contains(isa)) { + dexUseInfo.mLoaderIsas.add(elems[i]); + } else { + // Should never happen unless someone crafts the file manually. + // In theory it could if we drop a supported ISA after an OTA but we don't + // do that. + Slog.wtf(TAG, "Unsupported ISA when parsing PackageDexUsage: " + isa); + } + } + if (supportedIsas.isEmpty()) { + Slog.wtf(TAG, "Ignore dexPath when parsing PackageDexUsage because of " + + "unsupported isas. dexPath=" + dexPath); + continue; + } + currentPakageData.mDexUseInfoMap.put(dexPath, dexUseInfo); + } else { + // This is a package line. + // We expect it to be: `packageName,isUsedByOtherApps`. + String[] elems = s.split(SPLIT_CHAR); + if (elems.length != 2) { + throw new IllegalStateException("Invalid PackageDexUsage line: " + s); + } + currentPakage = elems[0]; + currentPakageData = new PackageUseInfo(); + currentPakageData.mIsUsedByOtherApps = readBoolean(elems[1]); + data.put(currentPakage, currentPakageData); + } + } + + synchronized (mPackageUseInfoMap) { + mPackageUseInfoMap.clear(); + mPackageUseInfoMap.putAll(data); + } + } + + /** + * Syncs the existing data with the set of available packages by removing obsolete entries. + */ + public void syncData(Map> packageToUsersMap) { + synchronized (mPackageUseInfoMap) { + Iterator> pIt = + mPackageUseInfoMap.entrySet().iterator(); + while (pIt.hasNext()) { + Map.Entry pEntry = pIt.next(); + String packageName = pEntry.getKey(); + PackageUseInfo packageUseInfo = pEntry.getValue(); + Set users = packageToUsersMap.get(packageName); + if (users == null) { + // The package doesn't exist anymore, remove the record. + pIt.remove(); + } else { + // The package exists but we can prune the entries associated with non existing + // users. + Iterator> dIt = + packageUseInfo.mDexUseInfoMap.entrySet().iterator(); + while (dIt.hasNext()) { + DexUseInfo dexUseInfo = dIt.next().getValue(); + if (!users.contains(dexUseInfo.mOwnerUserId)) { + // User was probably removed. Delete its dex usage info. + dIt.remove(); + } + } + if (!packageUseInfo.mIsUsedByOtherApps + && packageUseInfo.mDexUseInfoMap.isEmpty()) { + // The package is not used by other apps and we removed all its dex files + // records. Remove the entire package record as well. + pIt.remove(); + } + } + } + } + } + + public PackageUseInfo getPackageUseInfo(String packageName) { + synchronized (mPackageUseInfoMap) { + return mPackageUseInfoMap.get(packageName); + } + } + + public void clear() { + synchronized (mPackageUseInfoMap) { + mPackageUseInfoMap.clear(); + } + } + // Creates a deep copy of the class' mPackageUseInfoMap. + private Map clonePackageUseInfoMap() { + Map clone = new HashMap<>(); + synchronized (mPackageUseInfoMap) { + for (Map.Entry e : mPackageUseInfoMap.entrySet()) { + clone.put(e.getKey(), new PackageUseInfo(e.getValue())); + } + } + return clone; + } + + private String writeBoolean(boolean bool) { + return bool ? "1" : "0"; + } + + private boolean readBoolean(String bool) { + if ("0".equals(bool)) return false; + if ("1".equals(bool)) return true; + throw new IllegalArgumentException("Unknown bool encoding: " + bool); + } + + private boolean contains(int[] array, int elem) { + for (int i = 0; i < array.length; i++) { + if (elem == array[i]) { + return true; + } + } + return false; + } + + public String dump() { + StringWriter sw = new StringWriter(); + write(sw); + return sw.toString(); + } + + /** + * Stores data on how a package and its dex files are used. + */ + public static class PackageUseInfo { + // This flag is for the primary and split apks. It is set to true whenever one of them + // is loaded by another app. + private boolean mIsUsedByOtherApps; + // Map dex paths to their data (isUsedByOtherApps, owner id, loader isa). + private final Map mDexUseInfoMap; + + public PackageUseInfo() { + mIsUsedByOtherApps = false; + mDexUseInfoMap = new HashMap<>(); + } + + // Creates a deep copy of the `other`. + public PackageUseInfo(PackageUseInfo other) { + mIsUsedByOtherApps = other.mIsUsedByOtherApps; + mDexUseInfoMap = new HashMap<>(); + for (Map.Entry e : other.mDexUseInfoMap.entrySet()) { + mDexUseInfoMap.put(e.getKey(), new DexUseInfo(e.getValue())); + } + } + + private boolean merge(boolean isUsedByOtherApps) { + boolean oldIsUsedByOtherApps = mIsUsedByOtherApps; + mIsUsedByOtherApps = mIsUsedByOtherApps || isUsedByOtherApps; + return oldIsUsedByOtherApps != this.mIsUsedByOtherApps; + } + + public boolean isUsedByOtherApps() { + return mIsUsedByOtherApps; + } + + public Map getDexUseInfoMap() { + return mDexUseInfoMap; + } + } + + /** + * Stores data about a loaded dex files. + */ + public static class DexUseInfo { + private boolean mIsUsedByOtherApps; + private final int mOwnerUserId; + private final Set mLoaderIsas; + + public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId) { + this(isUsedByOtherApps, ownerUserId, null); + } + + public DexUseInfo(boolean isUsedByOtherApps, int ownerUserId, String loaderIsa) { + mIsUsedByOtherApps = isUsedByOtherApps; + mOwnerUserId = ownerUserId; + mLoaderIsas = new HashSet<>(); + if (loaderIsa != null) { + mLoaderIsas.add(loaderIsa); + } + } + + // Creates a deep copy of the `other`. + public DexUseInfo(DexUseInfo other) { + mIsUsedByOtherApps = other.mIsUsedByOtherApps; + mOwnerUserId = other.mOwnerUserId; + mLoaderIsas = new HashSet<>(other.mLoaderIsas); + } + + private boolean merge(DexUseInfo dexUseInfo) { + boolean oldIsUsedByOtherApps = mIsUsedByOtherApps; + mIsUsedByOtherApps = mIsUsedByOtherApps || dexUseInfo.mIsUsedByOtherApps; + boolean updateIsas = mLoaderIsas.addAll(dexUseInfo.mLoaderIsas); + return updateIsas || (oldIsUsedByOtherApps != mIsUsedByOtherApps); + } + + public boolean isUsedByOtherApps() { + return mIsUsedByOtherApps; + } + + public int getOwnerUserId() { + return mOwnerUserId; + } + + public Set getLoaderIsas() { + return mLoaderIsas; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java new file mode 100644 index 0000000000000..5a428414a9708 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/dex/PackageDexUsageTests.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm.dex; + +import android.os.Build; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import dalvik.system.VMRuntime; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; +import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PackageDexUsageTests { + private PackageDexUsage mPackageDexUsage; + + private TestData mFooBaseUser0; + private TestData mFooSplit1User0; + private TestData mFooSplit2UsedByOtherApps0; + private TestData mFooSecondary1User0; + private TestData mFooSecondary1User1; + private TestData mFooSecondary2UsedByOtherApps0; + private TestData mInvalidIsa; + + private TestData mBarBaseUser0; + private TestData mBarSecondary1User0; + private TestData mBarSecondary2User1; + + @Before + public void setup() { + mPackageDexUsage = new PackageDexUsage(); + + String fooPackageName = "com.google.foo"; + String fooCodeDir = "/data/app/com.google.foo/"; + String fooDataDir = "/data/user/0/com.google.foo/"; + + String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]); + + mFooBaseUser0 = new TestData(fooPackageName, + fooCodeDir + "base.apk", 0, isa, false, true); + + mFooSplit1User0 = new TestData(fooPackageName, + fooCodeDir + "split-1.apk", 0, isa, false, true); + + mFooSplit2UsedByOtherApps0 = new TestData(fooPackageName, + fooCodeDir + "split-2.apk", 0, isa, true, true); + + mFooSecondary1User0 = new TestData(fooPackageName, + fooDataDir + "sec-1.dex", 0, isa, false, false); + + mFooSecondary1User1 = new TestData(fooPackageName, + fooDataDir + "sec-1.dex", 1, isa, false, false); + + mFooSecondary2UsedByOtherApps0 = new TestData(fooPackageName, + fooDataDir + "sec-2.dex", 0, isa, true, false); + + mInvalidIsa = new TestData(fooPackageName, + fooCodeDir + "base.apk", 0, "INVALID_ISA", false, true); + + String barPackageName = "com.google.bar"; + String barCodeDir = "/data/app/com.google.bar/"; + String barDataDir = "/data/user/0/com.google.bar/"; + String barDataDir1 = "/data/user/1/com.google.bar/"; + + mBarBaseUser0 = new TestData(barPackageName, + barCodeDir + "base.apk", 0, isa, false, true); + mBarSecondary1User0 = new TestData(barPackageName, + barDataDir + "sec-1.dex", 0, isa, false, false); + mBarSecondary2User1 = new TestData(barPackageName, + barDataDir1 + "sec-2.dex", 1, isa, false, false); + } + + @Test + public void testRecordPrimary() { + // Assert new information. + assertTrue(record(mFooBaseUser0)); + + assertPackageDexUsage(mFooBaseUser0); + writeAndReadBack(); + assertPackageDexUsage(mFooBaseUser0); + } + + @Test + public void testRecordSplit() { + // Assert new information. + assertTrue(record(mFooSplit1User0)); + + assertPackageDexUsage(mFooSplit1User0); + writeAndReadBack(); + assertPackageDexUsage(mFooSplit1User0); + } + + @Test + public void testRecordSplitPrimarySequence() { + // Assert new information. + assertTrue(record(mFooBaseUser0)); + // Assert no new information. + assertFalse(record(mFooSplit1User0)); + + assertPackageDexUsage(mFooBaseUser0); + writeAndReadBack(); + assertPackageDexUsage(mFooBaseUser0); + + // Write Split2 which is used by other apps. + // Assert new information. + assertTrue(record(mFooSplit2UsedByOtherApps0)); + assertPackageDexUsage(mFooSplit2UsedByOtherApps0); + writeAndReadBack(); + assertPackageDexUsage(mFooSplit2UsedByOtherApps0); + } + + @Test + public void testRecordSecondary() { + assertTrue(record(mFooSecondary1User0)); + + assertPackageDexUsage(null, mFooSecondary1User0); + writeAndReadBack(); + assertPackageDexUsage(null, mFooSecondary1User0); + + // Recording again does not add more data. + assertFalse(record(mFooSecondary1User0)); + assertPackageDexUsage(null, mFooSecondary1User0); + } + + @Test + public void testRecordBaseAndSecondarySequence() { + // Write split. + assertTrue(record(mFooSplit2UsedByOtherApps0)); + // Write secondary. + assertTrue(record(mFooSecondary1User0)); + + // Check. + assertPackageDexUsage(mFooSplit2UsedByOtherApps0, mFooSecondary1User0); + writeAndReadBack(); + assertPackageDexUsage(mFooSplit2UsedByOtherApps0, mFooSecondary1User0); + + // Write another secondary. + assertTrue(record(mFooSecondary2UsedByOtherApps0)); + + // Check. + assertPackageDexUsage( + mFooSplit2UsedByOtherApps0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0); + writeAndReadBack(); + assertPackageDexUsage( + mFooSplit2UsedByOtherApps0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0); + } + + @Test + public void testMultiplePackages() { + assertTrue(record(mFooBaseUser0)); + assertTrue(record(mFooSecondary1User0)); + assertTrue(record(mFooSecondary2UsedByOtherApps0)); + assertTrue(record(mBarBaseUser0)); + assertTrue(record(mBarSecondary1User0)); + assertTrue(record(mBarSecondary2User1)); + + assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0); + assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1); + writeAndReadBack(); + assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0); + assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1); + } + + @Test + public void testPackageNotFound() { + assertNull(mPackageDexUsage.getPackageUseInfo("missing.package")); + } + + @Test + public void testAttemptToChangeOwner() { + assertTrue(record(mFooSecondary1User0)); + try { + record(mFooSecondary1User1); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testInvalidIsa() { + try { + record(mInvalidIsa); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testReadWriteEmtpy() { + // Expect no exceptions when writing/reading without data. + writeAndReadBack(); + } + + @Test + public void testSyncData() { + // Write some records. + assertTrue(record(mFooBaseUser0)); + assertTrue(record(mFooSecondary1User0)); + assertTrue(record(mFooSecondary2UsedByOtherApps0)); + assertTrue(record(mBarBaseUser0)); + assertTrue(record(mBarSecondary1User0)); + assertTrue(record(mBarSecondary2User1)); + + // Verify all is good. + assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0); + assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1); + writeAndReadBack(); + assertPackageDexUsage(mFooBaseUser0, mFooSecondary1User0, mFooSecondary2UsedByOtherApps0); + assertPackageDexUsage(mBarBaseUser0, mBarSecondary1User0, mBarSecondary2User1); + + // Simulate that only user 1 is available. + Map> packageToUsersMap = new HashMap<>(); + packageToUsersMap.put(mBarSecondary2User1.mPackageName, + new HashSet<>(Arrays.asList(mBarSecondary2User1.mOwnerUserId))); + mPackageDexUsage.syncData(packageToUsersMap); + + // Assert that only user 1 files are there. + assertPackageDexUsage(mBarBaseUser0, mBarSecondary2User1); + assertNull(mPackageDexUsage.getPackageUseInfo(mFooBaseUser0.mPackageName)); + } + + private void assertPackageDexUsage(TestData primary, TestData... secondaries) { + String packageName = primary == null ? secondaries[0].mPackageName : primary.mPackageName; + boolean primaryUsedByOtherApps = primary == null ? false : primary.mUsedByOtherApps; + PackageUseInfo pInfo = mPackageDexUsage.getPackageUseInfo(packageName); + + // Check package use info + assertNotNull(pInfo); + assertEquals(primaryUsedByOtherApps, pInfo.isUsedByOtherApps()); + Map dexUseInfoMap = pInfo.getDexUseInfoMap(); + assertEquals(secondaries.length, dexUseInfoMap.size()); + + // Check dex use info + for (TestData testData : secondaries) { + DexUseInfo dInfo = dexUseInfoMap.get(testData.mDexFile); + assertNotNull(dInfo); + assertEquals(testData.mUsedByOtherApps, dInfo.isUsedByOtherApps()); + assertEquals(testData.mOwnerUserId, dInfo.getOwnerUserId()); + assertEquals(1, dInfo.getLoaderIsas().size()); + assertTrue(dInfo.getLoaderIsas().contains(testData.mLoaderIsa)); + } + } + + private boolean record(TestData testData) { + return mPackageDexUsage.record(testData.mPackageName, testData.mDexFile, + testData.mOwnerUserId, testData.mLoaderIsa, testData.mUsedByOtherApps, + testData.mPrimaryOrSplit); + } + + private void writeAndReadBack() { + try { + StringWriter writer = new StringWriter(); + mPackageDexUsage.write(writer); + + mPackageDexUsage = new PackageDexUsage(); + mPackageDexUsage.read(new StringReader(writer.toString())); + } catch (IOException e) { + fail("Unexpected IOException: " + e.getMessage()); + } + } + + private static class TestData { + private final String mPackageName; + private final String mDexFile; + private final int mOwnerUserId; + private final String mLoaderIsa; + private final boolean mUsedByOtherApps; + private final boolean mPrimaryOrSplit; + + private TestData(String packageName, String dexFile, int ownerUserId, + String loaderIsa, boolean isUsedByOtherApps, boolean primaryOrSplit) { + mPackageName = packageName; + mDexFile = dexFile; + mOwnerUserId = ownerUserId; + mLoaderIsa = loaderIsa; + mUsedByOtherApps = isUsedByOtherApps; + mPrimaryOrSplit = primaryOrSplit; + } + + } +} From 5a9094c1b6e5797327674d4f12fa1ac4ade7275c Mon Sep 17 00:00:00 2001 From: Calin Juravle Date: Fri, 16 Dec 2016 16:22:00 +0000 Subject: [PATCH 2/6] Record data about dex files use on disk Add DexManager to keep track of how dex files are used. Every time a dex file is loaded, PackageManager will notify DexManager which will process the load. The DexManager will look up what package owns the dex file and record its use in package-dex-usage.list (through PackageDexUsage). Test: device boots, package-dex-usage.list is created and contains valid data, after device reboot the list is succesfully read from disk. runtest -x .../PackageDexUsageTests.java runtest -x .../DexManagerTests.java Bug: 32871170 (cherry picked from commit b8976d8f22fecaa9ed39276d9d8ded17d35b51a6) Change-Id: I9d779b2d39814d7c54fc7a888df93d403a001df5 --- .../server/pm/PackageManagerService.java | 28 +- .../com/android/server/pm/dex/DexManager.java | 324 ++++++++++++++++++ .../server/pm/dex/DexManagerTests.java | 282 +++++++++++++++ 3 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 services/core/java/com/android/server/pm/dex/DexManager.java create mode 100644 services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 614230c273dda..a87204254dfcd 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -253,6 +253,7 @@ import com.android.server.pm.Installer.InstallerException; import com.android.server.pm.PermissionsState.PermissionState; import com.android.server.pm.Settings.DatabaseVersion; import com.android.server.pm.Settings.VersionInfo; +import com.android.server.pm.dex.DexManager; import com.android.server.storage.DeviceStorageMonitorInternal; import dalvik.system.CloseGuard; @@ -295,6 +296,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -712,6 +714,9 @@ public class PackageManagerService extends IPackageManager.Stub { final PackageInstallerService mInstallerService; private final PackageDexOptimizer mPackageDexOptimizer; + // DexManager handles the usage of dex files (e.g. secondary files, whether or not a package + // is used by other apps). + private final DexManager mDexManager; private AtomicInteger mNextMoveId = new AtomicInteger(); private final MoveCallbacks mMoveCallbacks; @@ -2116,6 +2121,7 @@ public class PackageManagerService extends IPackageManager.Stub { mInstaller = installer; mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context, "*dexopt*"); + mDexManager = new DexManager(); mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper()); mOnPermissionChangeListeners = new OnPermissionChangeListeners( @@ -2544,6 +2550,19 @@ public class PackageManagerService extends IPackageManager.Stub { mPackageUsage.read(mPackages); mCompilerStats.read(); + // Read and update the usage of dex files. + // At this point we know the code paths of the packages, so we can validate + // the disk file and build the internal cache. + // The usage file is expected to be small so loading and verifying it + // should take a fairly small time compare to the other activities (e.g. package + // scanning). + final Map> userPackages = new HashMap<>(); + final int[] currentUserIds = UserManagerService.getInstance().getUserIds(); + for (int userId : currentUserIds) { + userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList()); + } + mDexManager.load(userPackages); + EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END, SystemClock.uptimeMillis()); Slog.i(TAG, "Time to scan packages: " @@ -7365,7 +7384,14 @@ public class PackageManagerService extends IPackageManager.Stub { @Override public void notifyDexLoad(String loadingPackageName, List dexPaths, String loaderIsa) { - // TODO(calin): b/32871170 + int userId = UserHandle.getCallingUserId(); + ApplicationInfo ai = getApplicationInfo(loadingPackageName, /*flags*/ 0, userId); + if (ai == null) { + Slog.w(TAG, "Loading a package that does not exist for the calling user. package=" + + loadingPackageName + ", user=" + userId); + return; + } + mDexManager.notifyDexLoad(ai, dexPaths, loaderIsa, userId); } // TODO: this is not used nor needed. Delete it. diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java new file mode 100644 index 0000000000000..aa2bcefd00e4e --- /dev/null +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.pm.dex; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageParser; +import android.content.pm.ApplicationInfo; + +import android.util.Slog; + +import com.android.server.pm.PackageManagerServiceUtils; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * This class keeps track of how dex files are used. + * Every time it gets a notification about a dex file being loaded it tracks + * its owning package and records it in PackageDexUsage (package-dex-usage.list). + * + * TODO(calin): Extract related dexopt functionality from PackageManagerService + * into this class. + */ +public class DexManager { + private static final String TAG = "DexManager"; + + private static final boolean DEBUG = false; + + // Maps package name to code locations. + // It caches the code locations for the installed packages. This allows for + // faster lookups (no locks) when finding what package owns the dex file. + private final Map mPackageCodeLocationsCache; + + // PackageDexUsage handles the actual I/O operations. It is responsible to + // encode and save the dex usage data. + private final PackageDexUsage mPackageDexUsage; + + // Possible outcomes of a dex search. + private static int DEX_SEARCH_NOT_FOUND = 0; // dex file not found + private static int DEX_SEARCH_FOUND_PRIMARY = 1; // dex file is the primary/base apk + private static int DEX_SEARCH_FOUND_SPLIT = 2; // dex file is a split apk + private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex + + public DexManager() { + mPackageCodeLocationsCache = new HashMap<>(); + mPackageDexUsage = new PackageDexUsage(); + } + + /** + * Notify about dex files loads. + * Note that this method is invoked when apps load dex files and it should + * return as fast as possible. + * + * @param loadingPackage the package performing the load + * @param dexPaths the list of dex files being loaded + * @param loaderIsa the ISA of the app loading the dex files + * @param loaderUserId the user id which runs the code loading the dex files + */ + public void notifyDexLoad(ApplicationInfo loadingAppInfo, List dexPaths, + String loaderIsa, int loaderUserId) { + try { + notifyDexLoadInternal(loadingAppInfo, dexPaths, loaderIsa, loaderUserId); + } catch (Exception e) { + Slog.w(TAG, "Exception while notifying dex load for package " + + loadingAppInfo.packageName, e); + } + } + + private void notifyDexLoadInternal(ApplicationInfo loadingAppInfo, List dexPaths, + String loaderIsa, int loaderUserId) { + if (!PackageManagerServiceUtils.checkISA(loaderIsa)) { + Slog.w(TAG, "Loading dex files " + dexPaths + " in unsupported ISA: " + + loaderIsa + "?"); + return; + } + + for (String dexPath : dexPaths) { + // Find the owning package name. + DexSearchResult searchResult = getDexPackage(loadingAppInfo, dexPath, loaderUserId); + + if (DEBUG) { + Slog.i(TAG, loadingAppInfo.packageName + + " loads from " + searchResult + " : " + loaderUserId + " : " + dexPath); + } + + if (searchResult.mOutcome != DEX_SEARCH_NOT_FOUND) { + // TODO(calin): extend isUsedByOtherApps check to detect the cases where + // different apps share the same runtime. In that case we should not mark the dex + // file as isUsedByOtherApps. Currently this is a safe approximation. + boolean isUsedByOtherApps = !loadingAppInfo.packageName.equals( + searchResult.mOwningPackageName); + boolean primaryOrSplit = searchResult.mOutcome == DEX_SEARCH_FOUND_PRIMARY || + searchResult.mOutcome == DEX_SEARCH_FOUND_SPLIT; + + if (primaryOrSplit && !isUsedByOtherApps) { + // If the dex file is the primary apk (or a split) and not isUsedByOtherApps + // do not record it. This case does not bring any new usable information + // and can be safely skipped. + continue; + } + + // Record dex file usage. If the current usage is a new pattern (e.g. new secondary, + // or UsedBytOtherApps), record will return true and we trigger an async write + // to disk to make sure we don't loose the data in case of a reboot. + if (mPackageDexUsage.record(searchResult.mOwningPackageName, + dexPath, loaderUserId, loaderIsa, isUsedByOtherApps, primaryOrSplit)) { + mPackageDexUsage.maybeWriteAsync(); + } + } else { + // This can happen in a few situations: + // - bogus dex loads + // - recent installs/uninstalls that we didn't detect. + // - new installed splits + // If we can't find the owner of the dex we simply do not track it. The impact is + // that the dex file will not be considered for offline optimizations. + // TODO(calin): add hooks for install/uninstall notifications to + // capture new or obsolete packages. + if (DEBUG) { + Slog.i(TAG, "Could not find owning package for dex file: " + dexPath); + } + } + } + } + + /** + * Read the dex usage from disk and populate the code cache locations. + * @param existingPackages a map containing information about what packages + * are available to what users. Only packages in this list will be + * recognized during notifyDexLoad(). + */ + public void load(Map> existingPackages) { + try { + loadInternal(existingPackages); + } catch (Exception e) { + mPackageDexUsage.clear(); + Slog.w(TAG, "Exception while loading package dex usage. " + + "Starting with a fresh state.", e); + } + } + + private void loadInternal(Map> existingPackages) { + Map> packageToUsersMap = new HashMap<>(); + // Cache the code locations for the installed packages. This allows for + // faster lookups (no locks) when finding what package owns the dex file. + for (Map.Entry> entry : existingPackages.entrySet()) { + List packageInfoList = entry.getValue(); + int userId = entry.getKey(); + for (PackageInfo pi : packageInfoList) { + // Cache the code locations. + PackageCodeLocations pcl = mPackageCodeLocationsCache.get(pi.packageName); + if (pcl != null) { + pcl.mergeAppDataDirs(pi.applicationInfo, userId); + } else { + mPackageCodeLocationsCache.put(pi.packageName, + new PackageCodeLocations(pi.applicationInfo, userId)); + } + // Cache a map from package name to the set of user ids who installed the package. + // We will use it to sync the data and remove obsolete entries from + // mPackageDexUsage. + Set users = putIfAbsent( + packageToUsersMap, pi.packageName, new HashSet<>()); + users.add(userId); + } + } + + mPackageDexUsage.read(); + mPackageDexUsage.syncData(packageToUsersMap); + } + + /** + * Get the package dex usage for the given package name. + * @return the package data or null if there is no data available for this package. + */ + public PackageDexUsage.PackageUseInfo getPackageUseInfo(String packageName) { + return mPackageDexUsage.getPackageUseInfo(packageName); + } + + /** + * Retrieves the package which owns the given dexPath. + */ + private DexSearchResult getDexPackage( + ApplicationInfo loadingAppInfo, String dexPath, int userId) { + // Ignore framework code. + // TODO(calin): is there a better way to detect it? + if (dexPath.startsWith("/system/framework/")) { + new DexSearchResult("framework", DEX_SEARCH_NOT_FOUND); + } + + // First, check if the package which loads the dex file actually owns it. + // Most of the time this will be true and we can return early. + PackageCodeLocations loadingPackageCodeLocations = + new PackageCodeLocations(loadingAppInfo, userId); + int outcome = loadingPackageCodeLocations.searchDex(dexPath, userId); + if (outcome != DEX_SEARCH_NOT_FOUND) { + // TODO(calin): evaluate if we bother to detect symlinks at the dexPath level. + return new DexSearchResult(loadingPackageCodeLocations.mPackageName, outcome); + } + + // The loadingPackage does not own the dex file. + // Perform a reverse look-up in the cache to detect if any package has ownership. + // Note that we can have false negatives if the cache falls out of date. + for (PackageCodeLocations pcl : mPackageCodeLocationsCache.values()) { + outcome = pcl.searchDex(dexPath, userId); + if (outcome != DEX_SEARCH_NOT_FOUND) { + return new DexSearchResult(pcl.mPackageName, outcome); + } + } + + // Cache miss. Return not found for the moment. + // + // TODO(calin): this may be because of a newly installed package, an update + // or a new added user. We can either perform a full look up again or register + // observers to be notified of package/user updates. + return new DexSearchResult(null, DEX_SEARCH_NOT_FOUND); + } + + private static V putIfAbsent(Map map, K key, V newValue) { + V existingValue = map.putIfAbsent(key, newValue); + return existingValue == null ? newValue : existingValue; + } + + /** + * Convenience class to store the different locations where a package might + * own code. + */ + private static class PackageCodeLocations { + private final String mPackageName; + private final String mBaseCodePath; + private final Set mSplitCodePaths; + // Maps user id to the application private directory. + private final Map> mAppDataDirs; + + public PackageCodeLocations(ApplicationInfo ai, int userId) { + mPackageName = ai.packageName; + mBaseCodePath = ai.sourceDir; + mSplitCodePaths = new HashSet<>(); + if (ai.splitSourceDirs != null) { + for (String split : ai.splitSourceDirs) { + mSplitCodePaths.add(split); + } + } + mAppDataDirs = new HashMap<>(); + mergeAppDataDirs(ai, userId); + } + + public void mergeAppDataDirs(ApplicationInfo ai, int userId) { + Set dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>()); + dataDirs.add(ai.dataDir); + + // Compute and cache the real path as well since data dir may be a symlink. + // e.g. /data/data/ -> /data/user/0/ + try { + dataDirs.add(PackageManagerServiceUtils.realpath(new File(ai.dataDir))); + } catch (IOException e) { + Slog.w(TAG, "Error to get realpath of " + ai.dataDir, e); + } + + } + + public int searchDex(String dexPath, int userId) { + // First check that this package is installed or active for the given user. + // If we don't have a data dir it means this user is trying to load something + // unavailable for them. + Set userDataDirs = mAppDataDirs.get(userId); + if (userDataDirs == null) { + Slog.w(TAG, "Trying to load a dex path which does not exist for the current " + + "user. dexPath=" + dexPath + ", userId=" + userId); + return DEX_SEARCH_NOT_FOUND; + } + + if (mBaseCodePath.equals(dexPath)) { + return DEX_SEARCH_FOUND_PRIMARY; + } + if (mSplitCodePaths.contains(dexPath)) { + return DEX_SEARCH_FOUND_SPLIT; + } + for (String dataDir : userDataDirs) { + if (dexPath.startsWith(dataDir)) { + return DEX_SEARCH_FOUND_SECONDARY; + } + } + return DEX_SEARCH_NOT_FOUND; + } + } + + /** + * Convenience class to store ownership search results. + */ + private class DexSearchResult { + private String mOwningPackageName; + private int mOutcome; + + public DexSearchResult(String owningPackageName, int outcome) { + this.mOwningPackageName = owningPackageName; + this.mOutcome = outcome; + } + + @Override + public String toString() { + return mOwningPackageName + "-" + mOutcome; + } + } + + +} diff --git a/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java new file mode 100644 index 0000000000000..b655f3af3a243 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/dex/DexManagerTests.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.pm.dex; + +import android.os.Build; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import dalvik.system.VMRuntime; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import static com.android.server.pm.dex.PackageDexUsage.PackageUseInfo; +import static com.android.server.pm.dex.PackageDexUsage.DexUseInfo; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DexManagerTests { + private DexManager mDexManager; + + private TestData mFooUser0; + private TestData mBarUser0; + private TestData mBarUser1; + private TestData mInvalidIsa; + private TestData mDoesNotExist; + + private int mUser0; + private int mUser1; + @Before + public void setup() { + + mUser0 = 0; + mUser1 = 1; + + String isa = VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0]); + String foo = "foo"; + String bar = "bar"; + + mFooUser0 = new TestData(foo, isa, mUser0); + mBarUser0 = new TestData(bar, isa, mUser0); + mBarUser1 = new TestData(bar, isa, mUser1); + mInvalidIsa = new TestData("INVALID", "INVALID_ISA", mUser0); + mDoesNotExist = new TestData("DOES.NOT.EXIST", isa, mUser1); + + + mDexManager = new DexManager(); + + // Foo and Bar are available to user0. + // Only Bar is available to user1; + Map> existingPackages = new HashMap<>(); + existingPackages.put(mUser0, Arrays.asList(mFooUser0.mPackageInfo, mBarUser0.mPackageInfo)); + existingPackages.put(mUser1, Arrays.asList(mBarUser1.mPackageInfo)); + mDexManager.load(existingPackages); + } + + @Test + public void testNotifyPrimaryUse() { + // The main dex file and splits are re-loaded by the app. + notifyDexLoad(mFooUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser0); + + // Package is not used by others, so we should get nothing back. + assertNull(getPackageUseInfo(mFooUser0)); + } + + @Test + public void testNotifyPrimaryForeignUse() { + // Foo loads Bar main apks. + notifyDexLoad(mFooUser0, mBarUser0.getBaseAndSplitDexPaths(), mUser0); + + // Bar is used by others now and should be in our records + PackageUseInfo pui = getPackageUseInfo(mBarUser0); + assertNotNull(pui); + assertTrue(pui.isUsedByOtherApps()); + assertTrue(pui.getDexUseInfoMap().isEmpty()); + } + + @Test + public void testNotifySecondary() { + // Foo loads its own secondary files. + List fooSecondaries = mFooUser0.getSecondaryDexPaths(); + notifyDexLoad(mFooUser0, fooSecondaries, mUser0); + + PackageUseInfo pui = getPackageUseInfo(mFooUser0); + assertNotNull(pui); + assertFalse(pui.isUsedByOtherApps()); + assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size()); + assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0); + } + + @Test + public void testNotifySecondaryForeign() { + // Foo loads bar secondary files. + List barSecondaries = mBarUser0.getSecondaryDexPaths(); + notifyDexLoad(mFooUser0, barSecondaries, mUser0); + + PackageUseInfo pui = getPackageUseInfo(mBarUser0); + assertNotNull(pui); + assertFalse(pui.isUsedByOtherApps()); + assertEquals(barSecondaries.size(), pui.getDexUseInfoMap().size()); + assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0); + } + + @Test + public void testNotifySequence() { + // Foo loads its own secondary files. + List fooSecondaries = mFooUser0.getSecondaryDexPaths(); + notifyDexLoad(mFooUser0, fooSecondaries, mUser0); + // Foo loads Bar own secondary files. + List barSecondaries = mBarUser0.getSecondaryDexPaths(); + notifyDexLoad(mFooUser0, barSecondaries, mUser0); + // Foo loads Bar primary files. + notifyDexLoad(mFooUser0, mBarUser0.getBaseAndSplitDexPaths(), mUser0); + // Bar loads its own secondary files. + notifyDexLoad(mBarUser0, barSecondaries, mUser0); + // Bar loads some own secondary files which foo didn't load. + List barSecondariesForOwnUse = mBarUser0.getSecondaryDexPathsForOwnUse(); + notifyDexLoad(mBarUser0, barSecondariesForOwnUse, mUser0); + + // Check bar usage. Should be used by other app (for primary and barSecondaries). + PackageUseInfo pui = getPackageUseInfo(mBarUser0); + assertNotNull(pui); + assertTrue(pui.isUsedByOtherApps()); + assertEquals(barSecondaries.size() + barSecondariesForOwnUse.size(), + pui.getDexUseInfoMap().size()); + + assertSecondaryUse(mFooUser0, pui, barSecondaries, /*isUsedByOtherApps*/true, mUser0); + assertSecondaryUse(mFooUser0, pui, barSecondariesForOwnUse, + /*isUsedByOtherApps*/false, mUser0); + + // Check foo usage. Should not be used by other app. + pui = getPackageUseInfo(mFooUser0); + assertNotNull(pui); + assertFalse(pui.isUsedByOtherApps()); + assertEquals(fooSecondaries.size(), pui.getDexUseInfoMap().size()); + assertSecondaryUse(mFooUser0, pui, fooSecondaries, /*isUsedByOtherApps*/false, mUser0); + } + + @Test + public void testPackageUseInfoNotFound() { + // Assert we don't get back data we did not previously record. + assertNull(getPackageUseInfo(mFooUser0)); + } + + @Test + public void testInvalidIsa() { + // Notifying with an invalid ISA should be ignored. + notifyDexLoad(mInvalidIsa, mInvalidIsa.getSecondaryDexPaths(), mUser0); + assertNull(getPackageUseInfo(mInvalidIsa)); + } + + @Test + public void testNotExistingPackate() { + // Notifying about the load of a package which was previously not + // register in DexManager#load should be ignored. + notifyDexLoad(mDoesNotExist, mDoesNotExist.getBaseAndSplitDexPaths(), mUser0); + assertNull(getPackageUseInfo(mDoesNotExist)); + } + + @Test + public void testCrossUserAttempt() { + // Bar from User1 tries to load secondary dex files from User0 Bar. + // Request should be ignored. + notifyDexLoad(mBarUser1, mBarUser0.getSecondaryDexPaths(), mUser1); + assertNull(getPackageUseInfo(mBarUser1)); + } + + @Test + public void testPackageNotInstalledForUser() { + // User1 tries to load Foo which is installed for User0 but not for User1. + // Note that the PackageManagerService already filters this out but we + // still check that nothing goes unexpected in DexManager. + notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser1); + assertNull(getPackageUseInfo(mBarUser1)); + } + + private void assertSecondaryUse(TestData testData, PackageUseInfo pui, + List secondaries, boolean isUsedByOtherApps, int ownerUserId) { + for (String dex : secondaries) { + DexUseInfo dui = pui.getDexUseInfoMap().get(dex); + assertNotNull(dui); + assertEquals(isUsedByOtherApps, dui.isUsedByOtherApps()); + assertEquals(ownerUserId, dui.getOwnerUserId()); + assertEquals(1, dui.getLoaderIsas().size()); + assertTrue(dui.getLoaderIsas().contains(testData.mLoaderIsa)); + } + } + + private void notifyDexLoad(TestData testData, List dexPaths, int loaderUserId) { + mDexManager.notifyDexLoad(testData.mPackageInfo.applicationInfo, dexPaths, + testData.mLoaderIsa, loaderUserId); + } + + private PackageUseInfo getPackageUseInfo(TestData testData) { + return mDexManager.getPackageUseInfo(testData.mPackageInfo.packageName); + } + + private static PackageInfo getMockPackageInfo(String packageName, int userId) { + PackageInfo pi = new PackageInfo(); + pi.packageName = packageName; + pi.applicationInfo = getMockApplicationInfo(packageName, userId); + return pi; + } + + private static ApplicationInfo getMockApplicationInfo(String packageName, int userId) { + ApplicationInfo ai = new ApplicationInfo(); + String codeDir = "/data/app/" + packageName; + ai.setBaseCodePath(codeDir + "/base.dex"); + ai.setSplitCodePaths(new String[] {codeDir + "/split-1.dex", codeDir + "/split-2.dex"}); + ai.dataDir = "/data/user/" + userId + "/" + packageName; + ai.packageName = packageName; + return ai; + } + + private static class TestData { + private final PackageInfo mPackageInfo; + private final String mLoaderIsa; + + private TestData(String packageName, String loaderIsa, int userId) { + mPackageInfo = getMockPackageInfo(packageName, userId); + mLoaderIsa = loaderIsa; + } + + private String getPackageName() { + return mPackageInfo.packageName; + } + + List getSecondaryDexPaths() { + List paths = new ArrayList<>(); + paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary1.dex"); + paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary2.dex"); + paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary3.dex"); + return paths; + } + + List getSecondaryDexPathsForOwnUse() { + List paths = new ArrayList<>(); + paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary4.dex"); + paths.add(mPackageInfo.applicationInfo.dataDir + "/secondary5.dex"); + return paths; + } + + List getBaseAndSplitDexPaths() { + List paths = new ArrayList<>(); + paths.add(mPackageInfo.applicationInfo.sourceDir); + for (String split : mPackageInfo.applicationInfo.splitSourceDirs) { + paths.add(split); + } + return paths; + } + } +} From 271bacbf5c524bbe33a7f0551b33f7a21cb64f17 Mon Sep 17 00:00:00 2001 From: Calin Juravle Date: Thu, 22 Dec 2016 14:31:19 +0000 Subject: [PATCH 3/6] Log DexManager realpath errors only in debug mode. Test: runtest -x .../DexManagerTests.java Bug: 33807524 (cherry picked from commit bb9ed1f3c93f0267fa97d6cbeb58ecd1de41d795) Change-Id: Ib962edf67ce7f5b9b93c9bc6855675e00e025810 --- .../core/java/com/android/server/pm/dex/DexManager.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index aa2bcefd00e4e..a1060dcb6eff7 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -271,7 +271,11 @@ public class DexManager { try { dataDirs.add(PackageManagerServiceUtils.realpath(new File(ai.dataDir))); } catch (IOException e) { - Slog.w(TAG, "Error to get realpath of " + ai.dataDir, e); + if (DEBUG) { + // Verify why we're getting spam at boot for some devices. + // b/33807524 + Slog.w(TAG, "Error to get realpath of " + ai.dataDir, e); + } } } From b96dba9badc18f1c831c0c83ff7934f6aaf4f51c Mon Sep 17 00:00:00 2001 From: Calin Juravle Date: Thu, 22 Dec 2016 18:47:05 +0200 Subject: [PATCH 4/6] Do not try to resolve realpath in DexManager. PM should already provide the real path of the application directory. Test: runtest -x .../DexManagerTests.java Bug: 33807524 Bug: 32871170 (cherry picked from commit c066205cea051c6d9f386188b9cb426c03dbee2d) Change-Id: Ie6b5f5e61d08710e7ef7d3149b7b13cc7d03a242 --- .../server/pm/PackageManagerService.java | 28 ++++++++++--------- .../com/android/server/pm/dex/DexManager.java | 27 +++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index a87204254dfcd..daa217e003307 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -2550,19 +2550,6 @@ public class PackageManagerService extends IPackageManager.Stub { mPackageUsage.read(mPackages); mCompilerStats.read(); - // Read and update the usage of dex files. - // At this point we know the code paths of the packages, so we can validate - // the disk file and build the internal cache. - // The usage file is expected to be small so loading and verifying it - // should take a fairly small time compare to the other activities (e.g. package - // scanning). - final Map> userPackages = new HashMap<>(); - final int[] currentUserIds = UserManagerService.getInstance().getUserIds(); - for (int userId : currentUserIds) { - userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList()); - } - mDexManager.load(userPackages); - EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END, SystemClock.uptimeMillis()); Slog.i(TAG, "Time to scan packages: " @@ -2728,6 +2715,21 @@ public class PackageManagerService extends IPackageManager.Stub { } mEphemeralApplicationRegistry = new EphemeralApplicationRegistry(this); + + // Read and update the usage of dex files. + // Do this at the end of PM init so that all the packages have their + // data directory reconciled. + // At this point we know the code paths of the packages, so we can validate + // the disk file and build the internal cache. + // The usage file is expected to be small so loading and verifying it + // should take a fairly small time compare to the other activities (e.g. package + // scanning). + final Map> userPackages = new HashMap<>(); + final int[] currentUserIds = UserManagerService.getInstance().getUserIds(); + for (int userId : currentUserIds) { + userPackages.put(userId, getInstalledPackages(/*flags*/ 0, userId).getList()); + } + mDexManager.load(userPackages); } // synchronized (mPackages) } // synchronized (mInstallLock) diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index a1060dcb6eff7..6d06838cd24f4 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -265,19 +265,6 @@ public class DexManager { public void mergeAppDataDirs(ApplicationInfo ai, int userId) { Set dataDirs = putIfAbsent(mAppDataDirs, userId, new HashSet<>()); dataDirs.add(ai.dataDir); - - // Compute and cache the real path as well since data dir may be a symlink. - // e.g. /data/data/ -> /data/user/0/ - try { - dataDirs.add(PackageManagerServiceUtils.realpath(new File(ai.dataDir))); - } catch (IOException e) { - if (DEBUG) { - // Verify why we're getting spam at boot for some devices. - // b/33807524 - Slog.w(TAG, "Error to get realpath of " + ai.dataDir, e); - } - } - } public int searchDex(String dexPath, int userId) { @@ -302,6 +289,20 @@ public class DexManager { return DEX_SEARCH_FOUND_SECONDARY; } } + + // TODO(calin): What if we get a symlink? e.g. data dir may be a symlink, + // /data/data/ -> /data/user/0/. + if (DEBUG) { + try { + String dexPathReal = PackageManagerServiceUtils.realpath(new File(dexPath)); + if (dexPathReal != dexPath) { + Slog.d(TAG, "Dex loaded with symlink. dexPath=" + + dexPath + " dexPathReal=" + dexPathReal); + } + } catch (IOException e) { + // Ignore + } + } return DEX_SEARCH_NOT_FOUND; } } From 95176bb18e5efeb78978f4950a18fadaf9e6d57e Mon Sep 17 00:00:00 2001 From: Calin Juravle Date: Thu, 22 Dec 2016 18:50:05 +0200 Subject: [PATCH 5/6] Some refactoring in BackgroundDexOptService. Extract postOta/idle optimizations in their own method. In preparation for adding the logic to handle secondary dex files. Test: device boots, pacakges get compiled Bug: 32871170 (cherry picked from commit be6a71a0b3f369843a26c91dd5123d0499f00e7e) Change-Id: Ie6cdd8461e7214f5de68bc9172f4171ebf72aa39 --- .../server/pm/BackgroundDexOptService.java | 196 ++++++++++-------- 1 file changed, 104 insertions(+), 92 deletions(-) diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index cec105816b36b..2bf5ef10ec890 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -69,7 +69,7 @@ public class BackgroundDexOptService extends JobService { */ final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false); - private final File dataDir = Environment.getDataDirectory(); + private final File mDataDir = Environment.getDataDirectory(); public static void schedule(Context context) { JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); @@ -120,7 +120,7 @@ public class BackgroundDexOptService extends JobService { private long getLowStorageThreshold() { @SuppressWarnings("deprecation") - final long lowThreshold = StorageManager.from(this).getStorageLowBytes(dataDir); + final long lowThreshold = StorageManager.from(this).getStorageLowBytes(mDataDir); if (lowThreshold == 0) { Log.e(TAG, "Invalid low storage threshold"); } @@ -134,114 +134,126 @@ public class BackgroundDexOptService extends JobService { // This job has already been superseded. Do not start it. return false; } - - // Load low battery threshold from the system config. This is a 0-100 integer. - final int lowBatteryThreshold = getResources().getInteger( - com.android.internal.R.integer.config_lowBatteryWarningLevel); - - final long lowThreshold = getLowStorageThreshold(); - - mAbortPostBootUpdate.set(false); new Thread("BackgroundDexOptService_PostBootUpdate") { @Override public void run() { - for (String pkg : pkgs) { - if (mAbortPostBootUpdate.get()) { - // JobScheduler requested an early abort. - return; - } - if (mExitPostBootUpdate.get()) { - // Different job, which supersedes this one, is running. - break; - } - if (getBatteryLevel() < lowBatteryThreshold) { - // Rather bail than completely drain the battery. - break; - } - long usableSpace = dataDir.getUsableSpace(); - if (usableSpace < lowThreshold) { - // Rather bail than completely fill up the disk. - Log.w(TAG, "Aborting background dex opt job due to low storage: " + - usableSpace); - break; - } + postBootUpdate(jobParams, pm, pkgs); + } - if (DEBUG_DEXOPT) { - Log.i(TAG, "Updating package " + pkg); - } + }.start(); + return true; + } - // Update package if needed. Note that there can be no race between concurrent - // jobs because PackageDexOptimizer.performDexOpt is synchronized. + private void postBootUpdate(JobParameters jobParams, PackageManagerService pm, + ArraySet pkgs) { + // Load low battery threshold from the system config. This is a 0-100 integer. + final int lowBatteryThreshold = getResources().getInteger( + com.android.internal.R.integer.config_lowBatteryWarningLevel); + final long lowThreshold = getLowStorageThreshold(); - // checkProfiles is false to avoid merging profiles during boot which - // might interfere with background compilation (b/28612421). - // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will - // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a - // trade-off worth doing to save boot time work. - pm.performDexOpt(pkg, - /* checkProfiles */ false, - PackageManagerService.REASON_BOOT, - /* force */ false); - } - // Ran to completion, so we abandon our timeslice and do not reschedule. - jobFinished(jobParams, /* reschedule */ false); + mAbortPostBootUpdate.set(false); + + for (String pkg : pkgs) { + if (mAbortPostBootUpdate.get()) { + // JobScheduler requested an early abort. + return; + } + if (mExitPostBootUpdate.get()) { + // Different job, which supersedes this one, is running. + break; + } + if (getBatteryLevel() < lowBatteryThreshold) { + // Rather bail than completely drain the battery. + break; + } + long usableSpace = mDataDir.getUsableSpace(); + if (usableSpace < lowThreshold) { + // Rather bail than completely fill up the disk. + Log.w(TAG, "Aborting background dex opt job due to low storage: " + + usableSpace); + break; + } + + if (DEBUG_DEXOPT) { + Log.i(TAG, "Updating package " + pkg); + } + + // Update package if needed. Note that there can be no race between concurrent + // jobs because PackageDexOptimizer.performDexOpt is synchronized. + + // checkProfiles is false to avoid merging profiles during boot which + // might interfere with background compilation (b/28612421). + // Unfortunately this will also means that "pm.dexopt.boot=speed-profile" will + // behave differently than "pm.dexopt.bg-dexopt=speed-profile" but that's a + // trade-off worth doing to save boot time work. + pm.performDexOpt(pkg, + /* checkProfiles */ false, + PackageManagerService.REASON_BOOT, + /* force */ false); + } + // Ran to completion, so we abandon our timeslice and do not reschedule. + jobFinished(jobParams, /* reschedule */ false); + } + + private boolean runIdleOptimization(final JobParameters jobParams, + final PackageManagerService pm, final ArraySet pkgs) { + new Thread("BackgroundDexOptService_IdleOptimization") { + @Override + public void run() { + idleOptimization(jobParams, pm, pkgs); } }.start(); return true; } - private boolean runIdleOptimization(final JobParameters jobParams, - final PackageManagerService pm, final ArraySet pkgs) { + private void idleOptimization(JobParameters jobParams, PackageManagerService pm, + ArraySet pkgs) { // If post-boot update is still running, request that it exits early. mExitPostBootUpdate.set(true); mAbortIdleOptimization.set(false); final long lowThreshold = getLowStorageThreshold(); - - new Thread("BackgroundDexOptService_IdleOptimization") { - @Override - public void run() { - for (String pkg : pkgs) { - if (mAbortIdleOptimization.get()) { - // JobScheduler requested an early abort. - return; - } - if (sFailedPackageNames.contains(pkg)) { - // Skip previously failing package - continue; - } - - long usableSpace = dataDir.getUsableSpace(); - if (usableSpace < lowThreshold) { - // Rather bail than completely fill up the disk. - Log.w(TAG, "Aborting background dex opt job due to low storage: " + - usableSpace); - break; - } - - // Conservatively add package to the list of failing ones in case performDexOpt - // never returns. - synchronized (sFailedPackageNames) { - sFailedPackageNames.add(pkg); - } - // Optimize package if needed. Note that there can be no race between - // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. - if (pm.performDexOpt(pkg, - /* checkProfiles */ true, - PackageManagerService.REASON_BACKGROUND_DEXOPT, - /* force */ false)) { - // Dexopt succeeded, remove package from the list of failing ones. - synchronized (sFailedPackageNames) { - sFailedPackageNames.remove(pkg); - } - } - } - // Ran to completion, so we abandon our timeslice and do not reschedule. - jobFinished(jobParams, /* reschedule */ false); + for (String pkg : pkgs) { + if (mAbortIdleOptimization.get()) { + // JobScheduler requested an early abort. + return; } - }.start(); - return true; + + synchronized (sFailedPackageNames) { + if (sFailedPackageNames.contains(pkg)) { + // Skip previously failing package + continue; + } + } + + long usableSpace = mDataDir.getUsableSpace(); + if (usableSpace < lowThreshold) { + // Rather bail than completely fill up the disk. + Log.w(TAG, "Aborting background dex opt job due to low storage: " + + usableSpace); + break; + } + + // Conservatively add package to the list of failing ones in case performDexOpt + // never returns. + synchronized (sFailedPackageNames) { + sFailedPackageNames.add(pkg); + } + // Optimize package if needed. Note that there can be no race between + // concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. + if (pm.performDexOpt(pkg, + /* checkProfiles */ true, + PackageManagerService.REASON_BACKGROUND_DEXOPT, + /* force */ false)) { + // Dexopt succeeded, remove package from the list of failing ones. + synchronized (sFailedPackageNames) { + sFailedPackageNames.remove(pkg); + } + } + } + // Ran to completion, so we abandon our timeslice and do not reschedule. + jobFinished(jobParams, /* reschedule */ false); } @Override From 3a2b7f7d59da9f54d9de4afbbfeebcfb4a3866f2 Mon Sep 17 00:00:00 2001 From: Calin Juravle Date: Thu, 22 Dec 2016 18:50:05 +0200 Subject: [PATCH 6/6] Add an extra debug flag to BackgroundDexOptimizer This makes testing/debugging the job a bit easier. Test: device boots, packages get compiled Bug: 32871170 (cherry picked from commit a50d58e22630cd651a815381639e70476991bdbf) Change-Id: I5b94a8f0b3bbf9075dcaecf028aaf79a21aaab7b --- .../server/pm/BackgroundDexOptService.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index 2bf5ef10ec890..601a2194e8f37 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -42,12 +42,18 @@ import java.util.concurrent.TimeUnit; * {@hide} */ public class BackgroundDexOptService extends JobService { - static final String TAG = "BackgroundDexOptService"; + private static final String TAG = "BackgroundDexOptService"; - static final long RETRY_LATENCY = 4 * AlarmManager.INTERVAL_HOUR; + private static final boolean DEBUG = false; - static final int JOB_IDLE_OPTIMIZE = 800; - static final int JOB_POST_BOOT_UPDATE = 801; + private static final long RETRY_LATENCY = 4 * AlarmManager.INTERVAL_HOUR; + + private static final int JOB_IDLE_OPTIMIZE = 800; + private static final int JOB_POST_BOOT_UPDATE = 801; + + private static final long IDLE_OPTIMIZATION_PERIOD = DEBUG + ? TimeUnit.MINUTES.toMillis(1) + : TimeUnit.DAYS.toMillis(1); private static ComponentName sDexoptServiceName = new ComponentName( "android", @@ -86,7 +92,7 @@ public class BackgroundDexOptService extends JobService { js.schedule(new JobInfo.Builder(JOB_IDLE_OPTIMIZE, sDexoptServiceName) .setRequiresDeviceIdle(true) .setRequiresCharging(true) - .setPeriodic(TimeUnit.DAYS.toMillis(1)) + .setPeriodic(IDLE_OPTIMIZATION_PERIOD) .build()); if (DEBUG_DEXOPT) { @@ -208,6 +214,7 @@ public class BackgroundDexOptService extends JobService { private void idleOptimization(JobParameters jobParams, PackageManagerService pm, ArraySet pkgs) { + Log.i(TAG, "Performing idle optimizations"); // If post-boot update is still running, request that it exits early. mExitPostBootUpdate.set(true);