Fix the instruction set that ART Services uses to compile apps.
- Apps can have a "null" primary ABI, meaning the package manager can't
infer the ABIs. In that case, the app is launched with the device's
preferred ABI.
- Apps can have an ABI that isn't native to the device. In that case,
the app is launched with the ISA that the native bridge translates
to.
Bug: 251123324
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: I83fbde71ace55aa169092e0a58498932e077b640
diff --git a/libartservice/service/java/com/android/server/art/Constants.java b/libartservice/service/java/com/android/server/art/Constants.java
new file mode 100644
index 0000000..10953c7
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/Constants.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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;
+
+/**
+ * A mockable wrapper class for device-specific constants.
+ *
+ * @hide
+ */
+public class Constants {
+ private Constants() {}
+
+ /** Returns the ABI that the device prefers. */
+ @NonNull
+ public static String getPreferredAbi() {
+ return Build.SUPPORTED_ABIS[0];
+ }
+
+ /** Returns the 64 bit ABI that is native to the device. */
+ @Nullable
+ public static String getNative64BitAbi() {
+ // The value comes from "ro.product.cpu.abilist64" and we assume that the first element is
+ // the native one.
+ return Build.SUPPORTED_64_BIT_ABIS.length > 0 ? Build.SUPPORTED_64_BIT_ABIS[0] : null;
+ }
+
+ /** Returns the 32 bit ABI that is native to the device. */
+ @Nullable
+ public static String getNative32BitAbi() {
+ // The value comes from "ro.product.cpu.abilist32" and we assume that the first element is
+ // the native one.
+ return Build.SUPPORTED_32_BIT_ABIS.length > 0 ? Build.SUPPORTED_32_BIT_ABIS[0] : null;
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 66c9704..c626168 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -19,6 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.ServiceManager;
+import android.os.SystemProperties;
+import android.text.TextUtils;
import android.util.SparseArray;
import com.android.server.art.ArtifactsPath;
@@ -65,23 +67,66 @@
List<Abi> abis = new ArrayList<>();
String primaryCpuAbi = pkgState.getPrimaryCpuAbi();
if (primaryCpuAbi != null) {
- abis.add(Abi.create(primaryCpuAbi, VMRuntime.getInstructionSet(primaryCpuAbi),
- true /* isPrimaryAbi */));
+ String isa = getTranslatedIsa(VMRuntime.getInstructionSet(primaryCpuAbi));
+ abis.add(Abi.create(nativeIsaToAbi(isa), isa, true /* isPrimaryAbi */));
}
String secondaryCpuAbi = pkgState.getSecondaryCpuAbi();
if (secondaryCpuAbi != null) {
- abis.add(Abi.create(secondaryCpuAbi, VMRuntime.getInstructionSet(secondaryCpuAbi),
- false /* isPrimaryAbi */));
+ Utils.check(primaryCpuAbi != null);
+ String isa = getTranslatedIsa(VMRuntime.getInstructionSet(secondaryCpuAbi));
+ abis.add(Abi.create(nativeIsaToAbi(isa), isa, false /* isPrimaryAbi */));
}
- // Primary and secondary ABIs are guaranteed to have different ISAs.
+ if (abis.isEmpty()) {
+ // This is the most common case. The package manager can't infer the ABIs, probably
+ // because the package doesn't contain any native library. The app is launched with
+ // the device's preferred ABI.
+ String preferredAbi = Constants.getPreferredAbi();
+ abis.add(Abi.create(preferredAbi, VMRuntime.getInstructionSet(preferredAbi),
+ true /* isPrimaryAbi */));
+ }
+ // Primary and secondary ABIs should be guaranteed to have different ISAs.
if (abis.size() == 2 && abis.get(0).isa().equals(abis.get(1).isa())) {
- throw new IllegalStateException(
- String.format("Duplicate ISA: primary ABI '%s', secondary ABI '%s'",
- primaryCpuAbi, secondaryCpuAbi));
+ throw new IllegalStateException(String.format(
+ "Duplicate ISA: primary ABI '%s' ('%s'), secondary ABI '%s' ('%s')",
+ primaryCpuAbi, abis.get(0).name(), secondaryCpuAbi, abis.get(1).name()));
}
return abis;
}
+ /**
+ * If the given ISA isn't native to the device, returns the ISA that the native bridge
+ * translates it to. Otherwise, returns the ISA as is. This is the ISA that the app is actually
+ * launched with and therefore the ISA that should be used to compile the app.
+ */
+ @NonNull
+ private static String getTranslatedIsa(@NonNull String isa) {
+ String abi64 = Constants.getNative64BitAbi();
+ String abi32 = Constants.getNative32BitAbi();
+ if ((abi64 != null && isa.equals(VMRuntime.getInstructionSet(abi64)))
+ || (abi32 != null && isa.equals(VMRuntime.getInstructionSet(abi32)))) {
+ return isa;
+ }
+ String translatedIsa = SystemProperties.get("ro.dalvik.vm.isa." + isa);
+ if (TextUtils.isEmpty(translatedIsa)) {
+ throw new IllegalStateException(String.format("Unsupported isa '%s'", isa));
+ }
+ return translatedIsa;
+ }
+
+ @NonNull
+ private static String nativeIsaToAbi(@NonNull String isa) {
+ String abi64 = Constants.getNative64BitAbi();
+ if (abi64 != null && isa.equals(VMRuntime.getInstructionSet(abi64))) {
+ return abi64;
+ }
+ String abi32 = Constants.getNative32BitAbi();
+ if (abi32 != null && isa.equals(VMRuntime.getInstructionSet(abi32))) {
+ return abi32;
+ }
+ throw new IllegalStateException(String.format("Non-native isa '%s'", isa));
+ }
+
+ @NonNull
public static boolean isInDalvikCache(@NonNull PackageState pkg) {
return pkg.isSystem() && !pkg.isUpdatedSystemApp();
}
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index b10e31b..bca1203 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -34,6 +34,7 @@
import android.content.pm.ApplicationInfo;
import android.os.CancellationSignal;
import android.os.ServiceSpecificException;
+import android.os.SystemProperties;
import androidx.test.filters.SmallTest;
@@ -41,6 +42,7 @@
import com.android.server.art.model.OptimizationStatus;
import com.android.server.art.model.OptimizeParams;
import com.android.server.art.model.OptimizeResult;
+import com.android.server.art.testing.StaticMockitoRule;
import com.android.server.art.wrapper.AndroidPackageApi;
import com.android.server.art.wrapper.PackageManagerLocal;
import com.android.server.art.wrapper.PackageState;
@@ -54,9 +56,6 @@
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-import org.mockito.quality.Strictness;
import java.util.List;
@@ -65,7 +64,9 @@
public class ArtManagerLocalTest {
private static final String PKG_NAME = "com.example.foo";
- @Rule public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+ @Rule
+ public StaticMockitoRule mockitoRule =
+ new StaticMockitoRule(SystemProperties.class, Constants.class);
@Mock private ArtManagerLocal.Injector mInjector;
@Mock private PackageManagerLocal mPackageManagerLocal;
@@ -93,6 +94,17 @@
lenient().when(mInjector.getArtd()).thenReturn(mArtd);
lenient().when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper);
+ lenient().when(SystemProperties.get(eq("pm.dexopt.install"))).thenReturn("speed-profile");
+
+ // No ISA translation.
+ lenient()
+ .when(SystemProperties.get(argThat(arg -> arg.startsWith("ro.dalvik.vm.isa."))))
+ .thenReturn("");
+
+ lenient().when(Constants.getPreferredAbi()).thenReturn("arm64-v8a");
+ lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
+ lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");
+
mPkgState = createPackageState();
mPkg = mPkgState.getAndroidPackage();
lenient()
@@ -129,6 +141,39 @@
verifyNoMoreInteractions(mArtd);
}
+ @Test
+ public void testDeleteOptimizedArtifactsTranslatedIsas() throws Exception {
+ lenient().when(SystemProperties.get("ro.dalvik.vm.isa.arm64")).thenReturn("x86_64");
+ lenient().when(SystemProperties.get("ro.dalvik.vm.isa.arm")).thenReturn("x86");
+ lenient().when(Constants.getPreferredAbi()).thenReturn("x86_64");
+ lenient().when(Constants.getNative64BitAbi()).thenReturn("x86_64");
+ lenient().when(Constants.getNative32BitAbi()).thenReturn("x86");
+
+ when(mArtd.deleteArtifacts(any())).thenReturn(1l);
+
+ DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts(
+ mock(PackageDataSnapshot.class), PKG_NAME);
+ assertThat(result.getFreedBytes()).isEqualTo(4);
+
+ verify(mArtd).deleteArtifacts(argThat(artifactsPath
+ -> artifactsPath.dexPath.equals("/data/app/foo/base.apk")
+ && artifactsPath.isa.equals("x86_64")
+ && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+ verify(mArtd).deleteArtifacts(argThat(artifactsPath
+ -> artifactsPath.dexPath.equals("/data/app/foo/base.apk")
+ && artifactsPath.isa.equals("x86")
+ && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+ verify(mArtd).deleteArtifacts(argThat(artifactsPath
+ -> artifactsPath.dexPath.equals("/data/app/foo/split_0.apk")
+ && artifactsPath.isa.equals("x86_64")
+ && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+ verify(mArtd).deleteArtifacts(argThat(artifactsPath
+ -> artifactsPath.dexPath.equals("/data/app/foo/split_0.apk")
+ && artifactsPath.isa.equals("x86")
+ && artifactsPath.isInDalvikCache == mIsInReadonlyPartition));
+ verifyNoMoreInteractions(mArtd);
+ }
+
@Test(expected = IllegalArgumentException.class)
public void testDeleteOptimizedArtifactsPackageNotFound() throws Exception {
when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))).thenReturn(null);
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
index 0aac42c..4808ea4 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
@@ -21,6 +21,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
@@ -50,7 +51,9 @@
protected static final int UID = 12345;
protected static final int SHARED_GID = UserHandle.getSharedAppGid(UID);
- @Rule public StaticMockitoRule mockitoRule = new StaticMockitoRule(SystemProperties.class);
+ @Rule
+ public StaticMockitoRule mockitoRule =
+ new StaticMockitoRule(SystemProperties.class, Constants.class);
@Mock protected PrimaryDexOptimizer.Injector mInjector;
@Mock protected IArtd mArtd;
@@ -79,6 +82,15 @@
lenient().when(SystemProperties.get("dalvik.vm.appimageformat")).thenReturn("lz4");
lenient().when(SystemProperties.get("pm.dexopt.shared")).thenReturn("speed");
+ // No ISA translation.
+ lenient()
+ .when(SystemProperties.get(argThat(arg -> arg.startsWith("ro.dalvik.vm.isa."))))
+ .thenReturn("");
+
+ lenient().when(Constants.getPreferredAbi()).thenReturn("arm64-v8a");
+ lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
+ lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");
+
lenient()
.when(mUserManager.getUserHandles(anyBoolean()))
.thenReturn(List.of(UserHandle.of(0), UserHandle.of(1), UserHandle.of(2)));
diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
index f154800..cf6d9ae 100644
--- a/libartservice/service/javatests/com/android/server/art/UtilsTest.java
+++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
@@ -18,21 +18,47 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.os.SystemProperties;
import android.util.SparseArray;
import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
import com.android.server.art.Utils;
+import com.android.server.art.testing.StaticMockitoRule;
+import com.android.server.art.wrapper.PackageState;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.junit.MockitoJUnitRunner;
import java.util.List;
@SmallTest
-@RunWith(MockitoJUnitRunner.StrictStubs.class)
+@RunWith(AndroidJUnit4.class)
public class UtilsTest {
+ @Rule
+ public StaticMockitoRule mockitoRule =
+ new StaticMockitoRule(SystemProperties.class, Constants.class);
+
+ @Before
+ public void setUp() throws Exception {
+ lenient().when(SystemProperties.get(eq("ro.dalvik.vm.isa.x86_64"))).thenReturn("arm64");
+ lenient().when(SystemProperties.get(eq("ro.dalvik.vm.isa.x86"))).thenReturn("arm");
+
+ // In reality, the preferred ABI should be either the native 64 bit ABI or the native 32 bit
+ // ABI, but we use a different value here to make sure the value is used only if expected.
+ lenient().when(Constants.getPreferredAbi()).thenReturn("x86");
+ lenient().when(Constants.getNative64BitAbi()).thenReturn("arm64-v8a");
+ lenient().when(Constants.getNative32BitAbi()).thenReturn("armeabi-v7a");
+ }
+
@Test
public void testCollectionIsEmptyTrue() {
assertThat(Utils.isEmpty(List.of())).isTrue();
@@ -66,6 +92,70 @@
}
@Test
+ public void testGetAllAbis() {
+ var pkgState = mock(PackageState.class);
+ when(pkgState.getPrimaryCpuAbi()).thenReturn("armeabi-v7a");
+ when(pkgState.getSecondaryCpuAbi()).thenReturn("arm64-v8a");
+
+ assertThat(Utils.getAllAbis(pkgState))
+ .containsExactly(Utils.Abi.create("armeabi-v7a", "arm", true /* isPrimaryAbi */),
+ Utils.Abi.create("arm64-v8a", "arm64", false /* isPrimaryAbi */));
+ }
+
+ @Test
+ public void testGetAllAbisTranslated() {
+ var pkgState = mock(PackageState.class);
+ when(pkgState.getPrimaryCpuAbi()).thenReturn("x86_64");
+ when(pkgState.getSecondaryCpuAbi()).thenReturn("x86");
+
+ assertThat(Utils.getAllAbis(pkgState))
+ .containsExactly(Utils.Abi.create("arm64-v8a", "arm64", true /* isPrimaryAbi */),
+ Utils.Abi.create("armeabi-v7a", "arm", false /* isPrimaryAbi */));
+ }
+
+ @Test
+ public void testGetAllAbisPrimaryOnly() {
+ var pkgState = mock(PackageState.class);
+ when(pkgState.getPrimaryCpuAbi()).thenReturn("armeabi-v7a");
+ when(pkgState.getSecondaryCpuAbi()).thenReturn(null);
+
+ assertThat(Utils.getAllAbis(pkgState))
+ .containsExactly(Utils.Abi.create("armeabi-v7a", "arm", true /* isPrimaryAbi */));
+ }
+
+ @Test
+ public void testGetAllAbisNone() {
+ var pkgState = mock(PackageState.class);
+ when(pkgState.getPrimaryCpuAbi()).thenReturn(null);
+ when(pkgState.getSecondaryCpuAbi()).thenReturn(null);
+
+ assertThat(Utils.getAllAbis(pkgState))
+ .containsExactly(Utils.Abi.create("x86", "x86", true /* isPrimaryAbi */));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testGetAllAbisInvalidNativeIsa() {
+ lenient().when(SystemProperties.get(eq("ro.dalvik.vm.isa.x86_64"))).thenReturn("x86");
+
+ var pkgState = mock(PackageState.class);
+ when(pkgState.getPrimaryCpuAbi()).thenReturn("x86_64");
+ when(pkgState.getSecondaryCpuAbi()).thenReturn(null);
+
+ Utils.getAllAbis(pkgState);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testGetAllAbisUnsupportedTranslation() {
+ lenient().when(SystemProperties.get(eq("ro.dalvik.vm.isa.x86_64"))).thenReturn("");
+
+ var pkgState = mock(PackageState.class);
+ when(pkgState.getPrimaryCpuAbi()).thenReturn("x86_64");
+ when(pkgState.getSecondaryCpuAbi()).thenReturn(null);
+
+ Utils.getAllAbis(pkgState);
+ }
+
+ @Test
public void testImplies() {
assertThat(Utils.implies(false, false)).isTrue();
assertThat(Utils.implies(false, true)).isTrue();