diff options
author | 2024-03-21 17:53:13 +0000 | |
---|---|---|
committer | 2024-04-03 14:34:51 +0100 | |
commit | cf654f0fe56c83110114e1eb82bac73faf1b07da (patch) | |
tree | 1ec39451fff29de0f55a3629f0d600cd52d47259 | |
parent | 52a32e54464e10c525e25b749c591e09fe122f83 (diff) |
Support disabling embedded profile through a dm file.
After this change, ART Service will look at `config.pb` in the dm file
and check the `enable_embedded_profile` field to determine whether to
allow using the the embedded profile
(`assets/art-profile/baseline.prof`) in the apk. The field default to
true.
Bug: 330689353
Test: atest CtsCompilationTestCases
Test: atest ArtServiceTests
Change-Id: Ie763e7deaa3f283cf469980c857f0a23abfec5b9
11 files changed, 341 insertions, 42 deletions
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java index f7083309d3..f6fccee9e5 100644 --- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java +++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java @@ -19,6 +19,7 @@ package com.android.server.art; import static com.android.server.art.ArtFileManager.ProfileLists; import static com.android.server.art.ArtFileManager.UsableArtifactLists; import static com.android.server.art.ArtFileManager.WritableArtifactLists; +import static com.android.server.art.DexMetadataHelper.DexMetadataInfo; import static com.android.server.art.PrimaryDexUtils.DetailedPrimaryDexInfo; import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo; import static com.android.server.art.ReasonMapping.BatchDexoptReason; @@ -728,12 +729,15 @@ public final class ArtManagerLocal { PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName); AndroidPackage pkg = Utils.getPackageOrThrow(pkgState); PrimaryDexInfo dexInfo = PrimaryDexUtils.getDexInfoBySplitName(pkg, splitName); + DexMetadataPath dmPath = AidlUtils.buildDexMetadataPath(dexInfo.dexPath()); + DexMetadataInfo dmInfo = mInjector.getDexMetadataHelper().getDexMetadataInfo(dmPath); List<ProfilePath> profiles = new ArrayList<>(); InitProfileResult result = Utils.getOrInitReferenceProfile(mInjector.getArtd(), dexInfo.dexPath(), PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo), PrimaryDexUtils.getExternalProfiles(dexInfo), + dmInfo.config().getEnableEmbeddedProfile(), PrimaryDexUtils.buildOutputProfile(pkgState, dexInfo, Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */)); if (!result.externalProfileErrors().isEmpty()) { @@ -1526,5 +1530,11 @@ public final class ArtManagerLocal { public ArtFileManager getArtFileManager() { return new ArtFileManager(getContext()); } + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @NonNull + public DexMetadataHelper getDexMetadataHelper() { + return new DexMetadataHelper(); + } } } diff --git a/libartservice/service/java/com/android/server/art/DexMetadataHelper.java b/libartservice/service/java/com/android/server/art/DexMetadataHelper.java new file mode 100644 index 0000000000..1252123479 --- /dev/null +++ b/libartservice/service/java/com/android/server/art/DexMetadataHelper.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 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.art; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.art.proto.DexMetadataConfig; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.NoSuchFileException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * A helper class to handle dex metadata (dm) files. + * + * Note that this is not the only consumer of dm files. A dm file is a container that contains + * various types of files for various purposes, passed down to various layers and consumed by them. + * + * @hide + */ +@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public class DexMetadataHelper { + private static final String TAG = ArtManagerLocal.TAG; + + @NonNull private final Injector mInjector; + + public DexMetadataHelper() { + this(new Injector()); + } + + @VisibleForTesting + public DexMetadataHelper(@NonNull Injector injector) { + mInjector = injector; + } + + @NonNull + public DexMetadataInfo getDexMetadataInfo(@Nullable DexMetadataPath dmPath) { + if (dmPath == null) { + return getDefaultDexMetadataInfo(); + } + + String realDmPath = getDmPath(dmPath); + try (var zipFile = mInjector.openZipFile(realDmPath)) { + ZipEntry entry = zipFile.getEntry("config.pb"); + if (entry == null) { + return new DexMetadataInfo(dmPath, DexMetadataConfig.getDefaultInstance()); + } + try (InputStream stream = zipFile.getInputStream(entry)) { + return new DexMetadataInfo(dmPath, DexMetadataConfig.parseFrom(stream)); + } + } catch (IOException e) { + if (!(e instanceof FileNotFoundException || e instanceof NoSuchFileException)) { + Log.e(TAG, String.format("Failed to read dm file '%s'", realDmPath), e); + } + return getDefaultDexMetadataInfo(); + } + } + + @NonNull + private DexMetadataInfo getDefaultDexMetadataInfo() { + return new DexMetadataInfo(null /* dmPath */, DexMetadataConfig.getDefaultInstance()); + } + + @NonNull + public static String getDmPath(@NonNull DexMetadataPath dmPath) { + String dexPath = dmPath.dexPath; + int pos = dexPath.lastIndexOf("."); + return (pos != -1 ? dexPath.substring(0, pos) : dexPath) + ".dm"; + } + + /** + * @param dmPath Represents the path to the dm file, if it exists. Or null if the file doesn't + * exist or an error occurred. + * @param config The config deserialized from `config.pb`, if it exists. Or the default instance + * if the file doesn't exist or an error occurred. + */ + public record DexMetadataInfo( + @Nullable DexMetadataPath dmPath, @NonNull DexMetadataConfig config) {} + + /** + * Injector pattern for testing purpose. + * + * @hide + */ + @VisibleForTesting + public static class Injector { + @NonNull + ZipFile openZipFile(@NonNull String path) throws IOException { + return new ZipFile(path); + } + } +} diff --git a/libartservice/service/java/com/android/server/art/Dexopter.java b/libartservice/service/java/com/android/server/art/Dexopter.java index 5ff63b30e8..d8d6b3d5c6 100644 --- a/libartservice/service/java/com/android/server/art/Dexopter.java +++ b/libartservice/service/java/com/android/server/art/Dexopter.java @@ -17,6 +17,7 @@ package com.android.server.art; import static com.android.server.art.ArtManagerLocal.AdjustCompilerFilterCallback; +import static com.android.server.art.DexMetadataHelper.DexMetadataInfo; import static com.android.server.art.OutputArtifacts.PermissionSettings; import static com.android.server.art.ProfilePath.TmpProfilePath; import static com.android.server.art.Utils.Abi; @@ -123,18 +124,29 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { continue; } + DexMetadataInfo dmInfo = + mInjector.getDexMetadataHelper().getDexMetadataInfo(buildDmPath(dexInfo)); + boolean needsToBeShared = needsToBeShared(dexInfo); boolean isOtherReadable = true; // If true, implies that the profile has changed since the last compilation. boolean profileMerged = false; if (DexFile.isProfileGuidedCompilerFilter(compilerFilter)) { + if (!dmInfo.config().getEnableEmbeddedProfile()) { + String dmPath = DexMetadataHelper.getDmPath( + Objects.requireNonNull(dmInfo.dmPath())); + Log.i(TAG, "Embedded profile disabled by config in the dm file " + dmPath); + } + if (needsToBeShared) { - InitProfileResult result = initReferenceProfile(dexInfo); + InitProfileResult result = initReferenceProfile( + dexInfo, dmInfo.config().getEnableEmbeddedProfile()); profile = result.profile(); isOtherReadable = result.isOtherReadable(); externalProfileErrors = result.externalProfileErrors(); } else { - InitProfileResult result = getOrInitReferenceProfile(dexInfo); + InitProfileResult result = getOrInitReferenceProfile( + dexInfo, dmInfo.config().getEnableEmbeddedProfile()); profile = result.profile(); isOtherReadable = result.isOtherReadable(); externalProfileErrors = result.externalProfileErrors(); @@ -183,6 +195,7 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { .setIsa(abi.isa()) .setIsInDalvikCache(isInDalvikCache) .setCompilerFilter(compilerFilter) + .setDmPath(dmInfo.dmPath()) .build(); var options = GetDexoptNeededOptions.builder() .setProfileMerged(profileMerged) @@ -377,18 +390,19 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { /** @see Utils#getOrInitReferenceProfile */ @Nullable - private InitProfileResult getOrInitReferenceProfile(@NonNull DexInfoType dexInfo) - throws RemoteException { + private InitProfileResult getOrInitReferenceProfile( + @NonNull DexInfoType dexInfo, boolean enableEmbeddedProfile) throws RemoteException { return Utils.getOrInitReferenceProfile(mInjector.getArtd(), dexInfo.dexPath(), - buildRefProfilePath(dexInfo), getExternalProfiles(dexInfo), + buildRefProfilePath(dexInfo), getExternalProfiles(dexInfo), enableEmbeddedProfile, buildOutputProfile(dexInfo, true /* isPublic */)); } @Nullable - private InitProfileResult initReferenceProfile(@NonNull DexInfoType dexInfo) - throws RemoteException { + private InitProfileResult initReferenceProfile( + @NonNull DexInfoType dexInfo, boolean enableEmbeddedProfile) throws RemoteException { return Utils.initReferenceProfile(mInjector.getArtd(), dexInfo.dexPath(), - getExternalProfiles(dexInfo), buildOutputProfile(dexInfo, true /* isPublic */)); + getExternalProfiles(dexInfo), enableEmbeddedProfile, + buildOutputProfile(dexInfo, true /* isPublic */)); } @NonNull @@ -485,8 +499,7 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { VdexPath inputVdex = getInputVdex(getDexoptNeededResult, target.dexInfo().dexPath(), target.isa()); - DexMetadataPath dmFile = getDmFile(target.dexInfo()); - if (dmFile != null + if (target.dmPath() != null && ReasonMapping.REASONS_FOR_INSTALL.contains(dexoptOptions.compilationReason)) { // If the DM file is passed to dex2oat, then add the "-dm" suffix to the reason (e.g., // "install-dm"). @@ -503,8 +516,8 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { ArtdDexoptResult result = mInjector.getArtd().dexopt(outputArtifacts, target.dexInfo().dexPath(), target.isa(), target.dexInfo().classLoaderContext(), - target.compilerFilter(), profile, inputVdex, dmFile, priorityClass, dexoptOptions, - artdCancellationSignal); + target.compilerFilter(), profile, inputVdex, target.dmPath(), priorityClass, + dexoptOptions, artdCancellationSignal); // Delete the existing runtime images after the dexopt is performed, even if they are still // usable (e.g., the compiler filter is "verify"). This is to make sure the dexopt puts the @@ -545,22 +558,6 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { } } - @Nullable - private DexMetadataPath getDmFile(@NonNull DexInfoType dexInfo) throws RemoteException { - DexMetadataPath path = buildDmPath(dexInfo); - if (path == null) { - return null; - } - try { - if (mInjector.getArtd().getDmFileVisibility(path) != FileVisibility.NOT_FOUND) { - return path; - } - } catch (ServiceSpecificException e) { - Log.e(TAG, "Failed to check DM file for " + dexInfo.dexPath(), e); - } - return null; - } - private boolean commitProfileChanges(@NonNull TmpProfilePath profile) throws RemoteException { try { mInjector.getArtd().commitTmpProfile(profile); @@ -662,6 +659,7 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { abstract @NonNull String isa(); abstract boolean isInDalvikCache(); abstract @NonNull String compilerFilter(); + abstract @Nullable DexMetadataPath dmPath(); static <DexInfoType extends DetailedDexInfo> Builder<DexInfoType> builder() { return new AutoValue_Dexopter_DexoptTarget.Builder<DexInfoType>(); @@ -673,6 +671,7 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { abstract Builder setIsa(@NonNull String value); abstract Builder setIsInDalvikCache(boolean value); abstract Builder setCompilerFilter(@NonNull String value); + abstract Builder setDmPath(@Nullable DexMetadataPath value); abstract DexoptTarget<DexInfoType> build(); } } @@ -769,5 +768,10 @@ public abstract class Dexopter<DexInfoType extends DetailedDexInfo> { public Config getConfig() { return mConfig; } + + @NonNull + public DexMetadataHelper getDexMetadataHelper() { + return new DexMetadataHelper(); + } } } diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java index 1b9a201b5d..e4032cf570 100644 --- a/libartservice/service/java/com/android/server/art/Utils.java +++ b/libartservice/service/java/com/android/server/art/Utils.java @@ -366,13 +366,15 @@ public final class Utils { * @param refProfile the path where an existing reference profile would be found, if present * @param externalProfiles a list of external profiles to initialize the reference profile from, * in the order of preference + * @param enableEmbeddedProfile whether to allow initializing the reference profile from the + * embedded profile * @param initOutput the final location to initialize the reference profile to */ @NonNull public static InitProfileResult getOrInitReferenceProfile(@NonNull IArtd artd, @NonNull String dexPath, @NonNull ProfilePath refProfile, - @NonNull List<ProfilePath> externalProfiles, @NonNull OutputProfile initOutput) - throws RemoteException { + @NonNull List<ProfilePath> externalProfiles, boolean enableEmbeddedProfile, + @NonNull OutputProfile initOutput) throws RemoteException { try { if (artd.isProfileUsable(refProfile, dexPath)) { boolean isOtherReadable = @@ -387,7 +389,8 @@ public final class Utils { e); } - return initReferenceProfile(artd, dexPath, externalProfiles, initOutput); + return initReferenceProfile( + artd, dexPath, externalProfiles, enableEmbeddedProfile, initOutput); } /** @@ -400,7 +403,7 @@ public final class Utils { @Nullable public static InitProfileResult initReferenceProfile(@NonNull IArtd artd, @NonNull String dexPath, @NonNull List<ProfilePath> externalProfiles, - @NonNull OutputProfile output) throws RemoteException { + boolean enableEmbeddedProfile, @NonNull OutputProfile output) throws RemoteException { // Each element is a pair of a profile name (for logging) and the corresponding initializer. // The order matters. Non-embedded profiles should take precedence. List<Pair<String, ProfileInitializer>> profileInitializers = new ArrayList<>(); @@ -413,8 +416,10 @@ public final class Utils { profileInitializers.add(Pair.create(AidlUtils.toString(profile), () -> artd.copyAndRewriteProfile(profile, output, dexPath))); } - profileInitializers.add(Pair.create( - "embedded profile", () -> artd.copyAndRewriteEmbeddedProfile(output, dexPath))); + if (enableEmbeddedProfile) { + profileInitializers.add(Pair.create( + "embedded profile", () -> artd.copyAndRewriteEmbeddedProfile(output, dexPath))); + } List<String> externalProfileErrors = new ArrayList<>(); for (var pair : profileInitializers) { diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java index fc3c4657a0..b7d67eccc3 100644 --- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java +++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java @@ -67,6 +67,7 @@ import com.android.server.art.model.DeleteResult; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; import com.android.server.art.model.DexoptStatus; +import com.android.server.art.proto.DexMetadataConfig; import com.android.server.art.testing.StaticMockitoRule; import com.android.server.art.testing.TestingUtils; import com.android.server.pm.PackageManagerLocal; @@ -90,6 +91,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.List; import java.util.Map; @@ -99,6 +101,7 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.zip.ZipFile; @SmallTest @RunWith(Parameterized.class) @@ -128,11 +131,13 @@ public class ArtManagerLocalTest { @Mock private DexUseManagerLocal mDexUseManager; @Mock private StorageManager mStorageManager; @Mock private ArtdRefCache.Pin mArtdPin; + @Mock private DexMetadataHelper.Injector mDexMetadataHelperInjector; private PackageState mPkgState1; private AndroidPackage mPkg1; private CheckedSecondaryDexInfo mPkg1SecondaryDexInfo1; private CheckedSecondaryDexInfo mPkg1SecondaryDexInfoNotFound; private Config mConfig; + private DexMetadataHelper mDexMetadataHelper; // True if the artifacts should be in dalvik-cache. @Parameter(0) public boolean mIsInDalvikCache; @@ -147,6 +152,7 @@ public class ArtManagerLocalTest { @Before public void setUp() throws Exception { mConfig = new Config(); + mDexMetadataHelper = new DexMetadataHelper(mDexMetadataHelperInjector); // Use `lenient()` to suppress `UnnecessaryStubbingException` thrown by the strict stubs. // These are the default test setups. They may or may not be used depending on the code path @@ -166,6 +172,7 @@ public class ArtManagerLocalTest { lenient() .when(mInjector.getArtFileManager()) .thenReturn(new ArtFileManager(mArtFileManagerInjector)); + lenient().when(mInjector.getDexMetadataHelper()).thenReturn(mDexMetadataHelper); lenient().when(mArtFileManagerInjector.getArtd()).thenReturn(mArtd); lenient().when(mArtFileManagerInjector.getUserManager()).thenReturn(mUserManager); @@ -258,6 +265,10 @@ public class ArtManagerLocalTest { .when(mArtd.copyAndRewriteEmbeddedProfile(any(), any())) .thenReturn(TestingUtils.createCopyAndRewriteProfileNoProfile()); + lenient() + .when(mDexMetadataHelperInjector.openZipFile(any())) + .thenThrow(NoSuchFileException.class); + mArtManagerLocal = new ArtManagerLocal(mInjector); } @@ -908,6 +919,44 @@ public class ArtManagerLocalTest { } @Test + public void testSnapshotAppProfileFromEmbeddedProfile() throws Exception { + ProfilePath refProfile = AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME_1, "primary"); + String dexPath = "/somewhere/app/foo/base.apk"; + + // Simulate that the reference profile doesn't exist. + when(mArtd.isProfileUsable(deepEq(refProfile), eq(dexPath))).thenReturn(false); + + // Verify that the embedded profile is used. + when(mArtd.copyAndRewriteEmbeddedProfile(any(), eq(dexPath))) + .thenReturn(TestingUtils.createCopyAndRewriteProfileSuccess()); + + when(mArtd.mergeProfiles(any(), any(), any(), any(), any())).thenReturn(false); + + mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME_1, null /* splitName */); + } + + @Test + public void testSnapshotAppProfileDisableEmbeddedProfile() throws Exception { + ProfilePath refProfile = AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME_1, "primary"); + String dexPath = "/somewhere/app/foo/base.apk"; + + // Simulate that the reference profile doesn't exist. + when(mArtd.isProfileUsable(deepEq(refProfile), eq(dexPath))).thenReturn(false); + + // The embedded profile is disabled. + var config = DexMetadataConfig.newBuilder().setEnableEmbeddedProfile(false).build(); + String dmPath = TestingUtils.createTempZipWithEntry("config.pb", config.toByteArray()); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(any()); + + when(mArtd.mergeProfiles(any(), any(), any(), any(), any())).thenReturn(false); + + mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME_1, null /* splitName */); + + // Verify that the embedded profile is not used. + verify(mArtd, never()).copyAndRewriteEmbeddedProfile(any(), eq(dexPath)); + } + + @Test public void testSnapshotAppProfileSplit() throws Exception { ProfilePath refProfile = AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME_1, "split_0.split"); diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java index b9b8bd31e4..8f7befb957 100644 --- a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java +++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterParameterizedTest.java @@ -61,6 +61,7 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; import org.mockito.ArgumentMatcher; +import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.List; @@ -271,7 +272,9 @@ public class PrimaryDexopterParameterizedTest extends PrimaryDexopterTestBase { PKG_NAME, APP_VERSION_NAME, APP_VERSION_CODE, ART_VERSION)); when(mArtd.createCancellationSignal()).thenReturn(mock(IArtdCancellationSignal.class)); - when(mArtd.getDmFileVisibility(any())).thenReturn(FileVisibility.NOT_FOUND); + lenient() + .when(mDexMetadataHelperInjector.openZipFile(any())) + .thenThrow(NoSuchFileException.class); // The first one is normal. doReturn(dexoptIsNeeded()) diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java index edb90a101c..a58add5f30 100644 --- a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java +++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTest.java @@ -47,6 +47,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.DexoptParams; import com.android.server.art.model.DexoptResult; +import com.android.server.art.proto.DexMetadataConfig; import com.android.server.art.testing.TestingUtils; import org.junit.Before; @@ -54,6 +55,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; +import java.nio.file.NoSuchFileException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ForkJoinPool; @@ -61,11 +63,13 @@ import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.zip.ZipFile; @SmallTest @RunWith(AndroidJUnit4.class) public class PrimaryDexopterTest extends PrimaryDexopterTestBase { private final String mDexPath = "/somewhere/app/foo/base.apk"; + private final String mDmPath = "/somewhere/app/foo/base.dm"; private final ProfilePath mRefProfile = AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME, "primary"); private final ProfilePath mPrebuiltProfile = AidlUtils.buildProfilePathForPrebuilt(mDexPath); @@ -117,7 +121,9 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { .thenReturn(TestingUtils.createCopyAndRewriteProfileNoProfile()); // By default, no DM file exists. - lenient().when(mArtd.getDmFileVisibility(any())).thenReturn(FileVisibility.NOT_FOUND); + lenient() + .when(mDexMetadataHelperInjector.openZipFile(any())) + .thenThrow(NoSuchFileException.class); // Dexopt is by default needed and successful. lenient() @@ -187,9 +193,8 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { @Test public void testDexoptDm() throws Exception { - lenient() - .when(mArtd.getDmFileVisibility(deepEq(mDmFile))) - .thenReturn(FileVisibility.OTHER_READABLE); + String dmPath = TestingUtils.createTempZipWithEntry("primary.vdex", new byte[0] /* data */); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(mDmPath); List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); verifyStatusAllOk(results); @@ -415,8 +420,7 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { verifyEmbeddedProfileNotUsed(mDexPath); } - @Test - public void testDexoptUsesEmbeddedProfile() throws Exception { + private void checkDexoptUsesEmbeddedProfile() throws Exception { makeEmbeddedProfileUsable(mDexPath); List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); @@ -438,6 +442,65 @@ public class PrimaryDexopterTest extends PrimaryDexopterTestBase { } @Test + public void testDexoptUsesEmbeddedProfileNoDm() throws Exception { + checkDexoptUsesEmbeddedProfile(); + } + + @Test + public void testDexoptUsesEmbeddedProfileDmNoConfig() throws Exception { + String dmPath = TestingUtils.createTempZipWithEntry("primary.vdex", new byte[0] /* data */); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(mDmPath); + checkDexoptUsesEmbeddedProfile(); + } + + @Test + public void testDexoptUsesEmbeddedProfileDmEmptyConfig() throws Exception { + String dmPath = TestingUtils.createTempZipWithEntry("config.pb", new byte[0] /* data */); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(mDmPath); + checkDexoptUsesEmbeddedProfile(); + } + + @Test + public void testDexoptUsesEmbeddedProfileDmBadConfig() throws Exception { + String dmPath = TestingUtils.createTempZipWithEntry( + "config.pb", new byte[] {0x42, 0x43, 0x44} /* data */); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(mDmPath); + checkDexoptUsesEmbeddedProfile(); + } + + @Test + public void testDexoptUsesEmbeddedProfileDmDisableEmbeddedProfile() throws Exception { + var config = DexMetadataConfig.newBuilder().setEnableEmbeddedProfile(false).build(); + String dmPath = TestingUtils.createTempZipWithEntry("config.pb", config.toByteArray()); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(mDmPath); + + makeEmbeddedProfileUsable(mDexPath); + + List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); + verifyStatusAllOk(results); + + checkDexoptWithNoProfile(verify(mArtd), mDexPath, "arm64", "verify"); + checkDexoptWithNoProfile(verify(mArtd), mDexPath, "arm", "verify"); + + verifyEmbeddedProfileNotUsed(mDexPath); + } + + @Test + public void testDexoptUsesEmbeddedProfileNoEmbeddedProfile() throws Exception { + var config = DexMetadataConfig.newBuilder().setEnableEmbeddedProfile(true).build(); + String dmPath = TestingUtils.createTempZipWithEntry("config.pb", config.toByteArray()); + doReturn(new ZipFile(dmPath)).when(mDexMetadataHelperInjector).openZipFile(mDmPath); + + List<DexContainerFileDexoptResult> results = mPrimaryDexopter.dexopt(); + verifyStatusAllOk(results); + + checkDexoptWithNoProfile(verify(mArtd), mDexPath, "arm64", "verify"); + checkDexoptWithNoProfile(verify(mArtd), mDexPath, "arm", "verify"); + + verifyEmbeddedProfileNotUsed(mDexPath); + } + + @Test public void testDexoptExternalProfileErrors() throws Exception { // Having no profile should not be reported. // Having a bad profile should be reported. diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTestBase.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTestBase.java index 7a8287fe4c..5e26835da2 100644 --- a/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTestBase.java +++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexopterTestBase.java @@ -64,12 +64,14 @@ public class PrimaryDexopterTestBase { @Mock protected UserManager mUserManager; @Mock protected DexUseManagerLocal mDexUseManager; @Mock protected StorageManager mStorageManager; + @Mock protected DexMetadataHelper.Injector mDexMetadataHelperInjector; protected PackageState mPkgState; protected AndroidPackage mPkg; protected PackageUserState mPkgUserStateNotInstalled; protected PackageUserState mPkgUserStateInstalled; protected CancellationSignal mCancellationSignal; protected Config mConfig; + protected DexMetadataHelper mDexMetadataHelper; @Before public void setUp() throws Exception { @@ -79,6 +81,7 @@ public class PrimaryDexopterTestBase { mPkg = mPkgState.getAndroidPackage(); mCancellationSignal = new CancellationSignal(); mConfig = new Config(); + mDexMetadataHelper = new DexMetadataHelper(mDexMetadataHelperInjector); lenient().when(mInjector.getArtd()).thenReturn(mArtd); lenient().when(mInjector.isSystemUiPackage(any())).thenReturn(false); @@ -88,6 +91,7 @@ public class PrimaryDexopterTestBase { lenient().when(mInjector.getStorageManager()).thenReturn(mStorageManager); lenient().when(mInjector.getArtVersion()).thenReturn(ART_VERSION); lenient().when(mInjector.getConfig()).thenReturn(mConfig); + lenient().when(mInjector.getDexMetadataHelper()).thenReturn(mDexMetadataHelper); lenient() .when(SystemProperties.get("dalvik.vm.systemuicompilerfilter")) diff --git a/libartservice/service/javatests/com/android/server/art/SecondaryDexopterTest.java b/libartservice/service/javatests/com/android/server/art/SecondaryDexopterTest.java index f80503d09b..13ff28a908 100644 --- a/libartservice/service/javatests/com/android/server/art/SecondaryDexopterTest.java +++ b/libartservice/service/javatests/com/android/server/art/SecondaryDexopterTest.java @@ -103,10 +103,12 @@ public class SecondaryDexopterTest { @Mock private SecondaryDexopter.Injector mInjector; @Mock private IArtd mArtd; @Mock private DexUseManagerLocal mDexUseManager; + @Mock private DexMetadataHelper.Injector mDexMetadataHelperInjector; private PackageState mPkgState; private AndroidPackage mPkg; private CancellationSignal mCancellationSignal; - protected Config mConfig; + private Config mConfig; + private DexMetadataHelper mDexMetadataHelper; private SecondaryDexopter mSecondaryDexopter; @@ -116,6 +118,7 @@ public class SecondaryDexopterTest { mPkg = mPkgState.getAndroidPackage(); mCancellationSignal = new CancellationSignal(); mConfig = new Config(); + mDexMetadataHelper = new DexMetadataHelper(mDexMetadataHelperInjector); lenient() .when(SystemProperties.getBoolean(eq("dalvik.vm.always_debuggable"), anyBoolean())) @@ -137,6 +140,7 @@ public class SecondaryDexopterTest { lenient().when(mInjector.isLauncherPackage(any())).thenReturn(false); lenient().when(mInjector.getDexUseManager()).thenReturn(mDexUseManager); lenient().when(mInjector.getConfig()).thenReturn(mConfig); + lenient().when(mInjector.getDexMetadataHelper()).thenReturn(mDexMetadataHelper); List<CheckedSecondaryDexInfo> secondaryDexInfo = createSecondaryDexInfo(); lenient() diff --git a/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java b/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java index ee55170f05..aabc7836b9 100644 --- a/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java +++ b/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java @@ -27,9 +27,13 @@ import com.android.server.art.CopyAndRewriteProfileResult; import com.google.common.truth.Correspondence; import com.google.common.truth.Truth; +import java.io.File; +import java.io.FileOutputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; public final class TestingUtils { private static final String TAG = "ArtServiceTesting"; @@ -181,6 +185,17 @@ public final class TestingUtils { return result; } + public static String createTempZipWithEntry(String entryName, byte[] data) throws Exception { + File tempFile = File.createTempFile("temp", ".zip"); + tempFile.deleteOnExit(); + try (var out = new ZipOutputStream(new FileOutputStream(tempFile))) { + out.putNextEntry(new ZipEntry(entryName)); + out.write(data); + out.closeEntry(); + } + return tempFile.getPath(); + } + private static boolean listDeepEquals( @NonNull List<?> a, @NonNull List<?> b, @NonNull StringBuilder errorMsg) { if (a.size() != b.size()) { diff --git a/libartservice/service/proto/dex_metadata_config.proto b/libartservice/service/proto/dex_metadata_config.proto new file mode 100644 index 0000000000..21cc3b27b8 --- /dev/null +++ b/libartservice/service/proto/dex_metadata_config.proto @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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. + */ + +syntax = "proto2"; + +package com.android.server.art.proto; +option java_multiple_files = true; + +// The definition of `config.pb` in a dm file. +message DexMetadataConfig { + // Whether to enable using the embedded profile + // (`assets/art-profile/baseline.prof` in the APK/JAR file), if it exists. + optional bool enable_embedded_profile = 1 [default = true]; +} |