blob: 527203b5a3b1504841d54b306bc2c49d4f7a91a4 [file] [log] [blame]
/*
* 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 static com.android.server.art.ArtManagerLocal.DexoptDoneCallback;
import static com.android.server.art.model.DexoptResult.DexContainerFileDexoptResult;
import static com.android.server.art.model.DexoptResult.DexoptResultStatus;
import static com.android.server.art.model.DexoptResult.PackageDexoptResult;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.apphibernation.AppHibernationManager;
import android.os.CancellationSignal;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.modules.utils.pm.PackageStateModulesUtils;
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.Config;
import com.android.server.art.model.DexoptParams;
import com.android.server.art.model.DexoptResult;
import com.android.server.art.model.OperationProgress;
import com.android.server.art.testing.StaticMockitoRule;
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
import com.android.server.pm.pkg.SharedLibrary;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class DexoptHelperTest {
private static final String PKG_NAME_FOO = "com.example.foo";
private static final String PKG_NAME_BAR = "com.example.bar";
private static final String PKG_NAME_LIB1 = "com.example.lib1";
private static final String PKG_NAME_LIB2 = "com.example.lib2";
private static final String PKG_NAME_LIB3 = "com.example.lib3";
private static final String PKG_NAME_LIB4 = "com.example.lib4";
private static final String PKG_NAME_LIBBAZ = "com.example.libbaz";
@Rule
public StaticMockitoRule mockitoRule = new StaticMockitoRule(PackageStateModulesUtils.class);
@Mock private DexoptHelper.Injector mInjector;
@Mock private PrimaryDexopter mPrimaryDexopter;
@Mock private SecondaryDexopter mSecondaryDexopter;
@Mock private AppHibernationManager mAhm;
@Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
private PackageState mPkgStateFoo;
private PackageState mPkgStateBar;
private PackageState mPkgStateLib1;
private PackageState mPkgStateLib2;
private PackageState mPkgStateLib4;
private PackageState mPkgStateLibbaz;
private AndroidPackage mPkgFoo;
private AndroidPackage mPkgBar;
private AndroidPackage mPkgLib1;
private AndroidPackage mPkgLib2;
private AndroidPackage mPkgLib4;
private AndroidPackage mPkgLibbaz;
private CancellationSignal mCancellationSignal;
private ExecutorService mExecutor;
private List<DexContainerFileDexoptResult> mPrimaryResults;
private List<DexContainerFileDexoptResult> mSecondaryResults;
private Config mConfig;
private DexoptParams mParams;
private List<String> mRequestedPackages;
private DexoptHelper mDexoptHelper;
@Before
public void setUp() throws Exception {
lenient().when(mAhm.isHibernatingGlobally(any())).thenReturn(false);
lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(true);
mCancellationSignal = new CancellationSignal();
mExecutor = Executors.newSingleThreadExecutor();
mConfig = new Config();
preparePackagesAndLibraries();
mPrimaryResults = createResults("/somewhere/app/foo/base.apk",
DexoptResult.DEXOPT_PERFORMED /* status1 */,
DexoptResult.DEXOPT_PERFORMED /* status2 */);
mSecondaryResults = createResults("/data/user_de/0/foo/foo.apk",
DexoptResult.DEXOPT_PERFORMED /* status1 */,
DexoptResult.DEXOPT_PERFORMED /* status2 */);
lenient()
.when(mInjector.getPrimaryDexopter(any(), any(), any(), any()))
.thenReturn(mPrimaryDexopter);
lenient().when(mPrimaryDexopter.dexopt()).thenReturn(mPrimaryResults);
lenient()
.when(mInjector.getSecondaryDexopter(any(), any(), any(), any()))
.thenReturn(mSecondaryDexopter);
lenient().when(mSecondaryDexopter.dexopt()).thenReturn(mSecondaryResults);
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build();
lenient().when(mInjector.getAppHibernationManager()).thenReturn(mAhm);
lenient().when(mInjector.getConfig()).thenReturn(mConfig);
mDexoptHelper = new DexoptHelper(mInjector);
}
@After
public void tearDown() {
mExecutor.shutdown();
}
@Test
public void testDexopt() throws Exception {
// Only package libbaz fails.
var failingPrimaryDexopter = mock(PrimaryDexopter.class);
List<DexContainerFileDexoptResult> partialFailureResults = createResults(
"/somewhere/app/foo/base.apk", DexoptResult.DEXOPT_PERFORMED /* status1 */,
DexoptResult.DEXOPT_FAILED /* status2 */);
lenient().when(failingPrimaryDexopter.dexopt()).thenReturn(partialFailureResults);
when(mInjector.getPrimaryDexopter(same(mPkgStateLibbaz), any(), any(), any()))
.thenReturn(failingPrimaryDexopter);
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getRequestedCompilerFilter()).isEqualTo("speed-profile");
assertThat(result.getReason()).isEqualTo("install");
assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_FAILED);
// The requested packages must come first.
assertThat(result.getPackageDexoptResults()).hasSize(6);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_FAILED,
List.of(partialFailureResults, mSecondaryResults));
checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
// The order matters. When running in a single thread, it should dexopt primary dex files
// and the secondary dex files together for each package, and it should dexopt requested
// packages, in the given order, and then dexopt dependencies.
InOrder inOrder = inOrder(mInjector);
inOrder.verify(mInjector).getPrimaryDexopter(
same(mPkgStateFoo), same(mPkgFoo), same(mParams), any());
inOrder.verify(mInjector).getSecondaryDexopter(
same(mPkgStateFoo), same(mPkgFoo), same(mParams), any());
inOrder.verify(mInjector).getPrimaryDexopter(
same(mPkgStateBar), same(mPkgBar), same(mParams), any());
inOrder.verify(mInjector).getSecondaryDexopter(
same(mPkgStateBar), same(mPkgBar), same(mParams), any());
inOrder.verify(mInjector).getPrimaryDexopter(
same(mPkgStateLibbaz), same(mPkgLibbaz), same(mParams), any());
inOrder.verify(mInjector).getSecondaryDexopter(
same(mPkgStateLibbaz), same(mPkgLibbaz), same(mParams), any());
inOrder.verify(mInjector).getPrimaryDexopter(
same(mPkgStateLib1), same(mPkgLib1), same(mParams), any());
inOrder.verify(mInjector).getSecondaryDexopter(
same(mPkgStateLib1), same(mPkgLib1), same(mParams), any());
inOrder.verify(mInjector).getPrimaryDexopter(
same(mPkgStateLib2), same(mPkgLib2), same(mParams), any());
inOrder.verify(mInjector).getSecondaryDexopter(
same(mPkgStateLib2), same(mPkgLib2), same(mParams), any());
inOrder.verify(mInjector).getPrimaryDexopter(
same(mPkgStateLib4), same(mPkgLib4), same(mParams), any());
inOrder.verify(mInjector).getSecondaryDexopter(
same(mPkgStateLib4), same(mPkgLib4), same(mParams), any());
verifyNoMoreDexopt(6 /* expectedPrimaryTimes */, 6 /* expectedSecondaryTimes */);
}
@Test
public void testDexoptNoDependencies() throws Exception {
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX,
ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build();
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getPackageDexoptResults()).hasSize(3);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
verifyNoMoreDexopt(3 /* expectedPrimaryTimes */, 3 /* expectedSecondaryTimes */);
}
@Test
public void testDexoptPrimaryOnly() throws Exception {
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build();
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getPackageDexoptResults()).hasSize(6);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
verifyNoMoreDexopt(6 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
}
@Test
public void testDexoptPrimaryOnlyNoDependencies() throws Exception {
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(0,
ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build();
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getPackageDexoptResults()).hasSize(3);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults));
verifyNoMoreDexopt(3 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
}
@Test
public void testDexoptCancelledBetweenDex2oatInvocations() throws Exception {
when(mPrimaryDexopter.dexopt()).thenAnswer(invocation -> {
mCancellationSignal.cancel();
return mPrimaryResults;
});
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_CANCELLED);
assertThat(result.getPackageDexoptResults()).hasSize(6);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_CANCELLED,
List.of(mPrimaryResults));
checkPackageResult(
result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_CANCELLED, List.of());
checkPackageResult(
result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_CANCELLED, List.of());
checkPackageResult(
result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_CANCELLED, List.of());
checkPackageResult(
result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_CANCELLED, List.of());
checkPackageResult(
result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_CANCELLED, List.of());
verify(mInjector).getPrimaryDexopter(
same(mPkgStateFoo), same(mPkgFoo), same(mParams), any());
verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
}
// This test verifies that every child thread can register its own listener on the cancellation
// signal through `setOnCancelListener` (i.e., the listeners don't overwrite each other).
@Test
public void testDexoptCancelledDuringDex2oatInvocationsMultiThreaded() throws Exception {
final int NUM_PACKAGES = 6;
final long TIMEOUT_SEC = 10;
var dexoptStarted = new Semaphore(0);
var dexoptCancelled = new Semaphore(0);
when(mInjector.getPrimaryDexopter(any(), any(), any(), any())).thenAnswer(invocation -> {
var cancellationSignal = invocation.<CancellationSignal>getArgument(3);
var dexopter = mock(PrimaryDexopter.class);
when(dexopter.dexopt()).thenAnswer(innerInvocation -> {
// Simulate that the child thread registers its own listener.
var isListenerCalled = new AtomicBoolean(false);
cancellationSignal.setOnCancelListener(() -> isListenerCalled.set(true));
dexoptStarted.release();
assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
// Verify that the listener is called.
assertThat(isListenerCalled.get()).isTrue();
return mPrimaryResults;
});
return dexopter;
});
ExecutorService dexoptExecutor = Executors.newFixedThreadPool(NUM_PACKAGES);
Future<DexoptResult> future = ForkJoinPool.commonPool().submit(() -> {
return mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, dexoptExecutor);
});
try {
// Wait for all dexopt operations to start.
for (int i = 0; i < NUM_PACKAGES; i++) {
assertThat(dexoptStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
}
mCancellationSignal.cancel();
for (int i = 0; i < NUM_PACKAGES; i++) {
dexoptCancelled.release();
}
} finally {
dexoptExecutor.shutdown();
Utils.getFuture(future);
}
}
// This test verifies that dexopt operation on the current thread can be cancelled.
@Test
public void testDexoptCancelledDuringDex2oatInvocationsOnCurrentThread() throws Exception {
final long TIMEOUT_SEC = 10;
var dexoptStarted = new Semaphore(0);
var dexoptCancelled = new Semaphore(0);
when(mInjector.getPrimaryDexopter(any(), any(), any(), any())).thenAnswer(invocation -> {
var cancellationSignal = invocation.<CancellationSignal>getArgument(3);
var dexopter = mock(PrimaryDexopter.class);
when(dexopter.dexopt()).thenAnswer(innerInvocation -> {
if (cancellationSignal.isCanceled()) {
return mPrimaryResults;
}
var isListenerCalled = new AtomicBoolean(false);
cancellationSignal.setOnCancelListener(() -> isListenerCalled.set(true));
dexoptStarted.release();
assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
// Verify that the listener is called.
assertThat(isListenerCalled.get()).isTrue();
return mPrimaryResults;
});
return dexopter;
});
// Use the current thread (the one in ForkJoinPool).
Executor dexoptExecutor = Runnable::run;
Future<DexoptResult> future = ForkJoinPool.commonPool().submit(() -> {
return mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, dexoptExecutor);
});
try {
// Only one dexopt operation should start.
assertThat(dexoptStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
mCancellationSignal.cancel();
dexoptCancelled.release();
} finally {
Utils.getFuture(future);
}
}
@Test
public void testDexoptNotDexoptable() throws Exception {
when(PackageStateModulesUtils.isDexoptable(mPkgStateFoo)).thenReturn(false);
mRequestedPackages = List.of(PKG_NAME_FOO);
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_SKIPPED);
assertThat(result.getPackageDexoptResults()).hasSize(1);
checkPackageResult(
result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_SKIPPED, List.of());
verifyNoDexopt();
}
@Test
public void testDexoptLibraryNotDexoptable() throws Exception {
when(PackageStateModulesUtils.isDexoptable(mPkgStateLib1)).thenReturn(false);
mRequestedPackages = List.of(PKG_NAME_FOO);
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_PERFORMED);
assertThat(result.getPackageDexoptResults()).hasSize(1);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 1 /* expectedSecondaryTimes */);
}
@Test
public void testDexoptIsHibernating() throws Exception {
lenient().when(mAhm.isHibernatingGlobally(PKG_NAME_FOO)).thenReturn(true);
mRequestedPackages = List.of(PKG_NAME_FOO);
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_SKIPPED);
checkPackageResult(
result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_SKIPPED, List.of());
verifyNoDexopt();
}
@Test
public void testDexoptIsHibernatingButOatArtifactDeletionDisabled() throws Exception {
lenient().when(mAhm.isHibernatingGlobally(PKG_NAME_FOO)).thenReturn(true);
lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(false);
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(result.getPackageDexoptResults()).hasSize(6);
checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_PERFORMED,
List.of(mPrimaryResults, mSecondaryResults));
}
@Test(expected = IllegalArgumentException.class)
public void testDexoptPackageNotFound() throws Exception {
when(mSnapshot.getPackageState(any())).thenReturn(null);
mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
verifyNoDexopt();
}
@Test(expected = IllegalArgumentException.class)
public void testDexoptNoPackage() throws Exception {
lenient().when(mPkgStateFoo.getAndroidPackage()).thenReturn(null);
mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
verifyNoDexopt();
}
@Test
public void testDexoptSplit() throws Exception {
mRequestedPackages = List.of(PKG_NAME_FOO);
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SINGLE_SPLIT)
.setSplitName("split_0")
.build();
mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
}
@Test
public void testDexoptSplitNotFound() throws Exception {
mRequestedPackages = List.of(PKG_NAME_FOO);
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SINGLE_SPLIT)
.setSplitName("split_bogus")
.build();
assertThrows(IllegalArgumentException.class, () -> {
mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
});
}
@Test
public void testCallbacks() throws Exception {
List<DexoptResult> list1 = new ArrayList<>();
mConfig.addDexoptDoneCallback(
false /* onlyIncludeUpdates */, Runnable::run, result -> list1.add(result));
List<DexoptResult> list2 = new ArrayList<>();
mConfig.addDexoptDoneCallback(
false /* onlyIncludeUpdates */, Runnable::run, result -> list2.add(result));
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(list1).containsExactly(result);
assertThat(list2).containsExactly(result);
}
@Test
public void testCallbackRemoved() throws Exception {
List<DexoptResult> list1 = new ArrayList<>();
DexoptDoneCallback callback1 = result -> list1.add(result);
mConfig.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, callback1);
List<DexoptResult> list2 = new ArrayList<>();
mConfig.addDexoptDoneCallback(
false /* onlyIncludeUpdates */, Runnable::run, result -> list2.add(result));
mConfig.removeDexoptDoneCallback(callback1);
DexoptResult result = mDexoptHelper.dexopt(
mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
assertThat(list1).isEmpty();
assertThat(list2).containsExactly(result);
}
@Test(expected = IllegalStateException.class)
public void testCallbackAlreadyAdded() throws Exception {
List<DexoptResult> list = new ArrayList<>();
DexoptDoneCallback callback = result -> list.add(result);
mConfig.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, callback);
mConfig.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, callback);
}
// Tests `addDexoptDoneCallback` with `onlyIncludeUpdates` being true and false.
@Test
public void testCallbackWithFailureResults() throws Exception {
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(0,
ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build();
// This list should collect all results.
List<DexoptResult> listAll = new ArrayList<>();
mConfig.addDexoptDoneCallback(
false /* onlyIncludeUpdates */, Runnable::run, result -> listAll.add(result));
// This list should only collect results that have updates.
List<DexoptResult> listOnlyIncludeUpdates = new ArrayList<>();
mConfig.addDexoptDoneCallback(true /* onlyIncludeUpdates */, Runnable::run,
result -> listOnlyIncludeUpdates.add(result));
// Dexopt partially fails on package "foo".
List<DexContainerFileDexoptResult> partialFailureResults = createResults(
"/somewhere/app/foo/base.apk", DexoptResult.DEXOPT_PERFORMED /* status1 */,
DexoptResult.DEXOPT_FAILED /* status2 */);
var fooPrimaryDexopter = mock(PrimaryDexopter.class);
when(mInjector.getPrimaryDexopter(same(mPkgStateFoo), any(), any(), any()))
.thenReturn(fooPrimaryDexopter);
when(fooPrimaryDexopter.dexopt()).thenReturn(partialFailureResults);
// Dexopt totally fails on package "bar".
List<DexContainerFileDexoptResult> totalFailureResults = createResults(
"/somewhere/app/bar/base.apk", DexoptResult.DEXOPT_FAILED /* status1 */,
DexoptResult.DEXOPT_FAILED /* status2 */);
var barPrimaryDexopter = mock(PrimaryDexopter.class);
when(mInjector.getPrimaryDexopter(same(mPkgStateBar), any(), any(), any()))
.thenReturn(barPrimaryDexopter);
when(barPrimaryDexopter.dexopt()).thenReturn(totalFailureResults);
DexoptResult resultWithSomeUpdates = mDexoptHelper.dexopt(mSnapshot,
List.of(PKG_NAME_FOO, PKG_NAME_BAR), mParams, mCancellationSignal, mExecutor);
DexoptResult resultWithNoUpdates = mDexoptHelper.dexopt(
mSnapshot, List.of(PKG_NAME_BAR), mParams, mCancellationSignal, mExecutor);
assertThat(listAll).containsExactly(resultWithSomeUpdates, resultWithNoUpdates);
assertThat(listOnlyIncludeUpdates).hasSize(1);
assertThat(listOnlyIncludeUpdates.get(0)
.getPackageDexoptResults()
.stream()
.map(PackageDexoptResult::getPackageName)
.collect(Collectors.toList()))
.containsExactly(PKG_NAME_FOO);
}
@Test
public void testProgressCallback() throws Exception {
mParams = new DexoptParams.Builder("install")
.setCompilerFilter("speed-profile")
.setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX,
ArtFlags.FLAG_FOR_SECONDARY_DEX
| ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
.build();
// Delay the executor to verify that the commands passed to the executor are not bound to
// changing variables.
var progressCallbackExecutor = new DelayedExecutor();
Consumer<OperationProgress> progressCallback = mock(Consumer.class);
mDexoptHelper.dexopt(mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor,
progressCallbackExecutor, progressCallback);
progressCallbackExecutor.runAll();
InOrder inOrder = inOrder(progressCallback);
inOrder.verify(progressCallback)
.accept(eq(OperationProgress.create(0 /* current */, 3 /* total */)));
inOrder.verify(progressCallback)
.accept(eq(OperationProgress.create(1 /* current */, 3 /* total */)));
inOrder.verify(progressCallback)
.accept(eq(OperationProgress.create(2 /* current */, 3 /* total */)));
inOrder.verify(progressCallback)
.accept(eq(OperationProgress.create(3 /* current */, 3 /* total */)));
}
private AndroidPackage createPackage(boolean multiSplit) {
AndroidPackage pkg = mock(AndroidPackage.class);
var baseSplit = mock(AndroidPackageSplit.class);
if (multiSplit) {
var split0 = mock(AndroidPackageSplit.class);
lenient().when(split0.getName()).thenReturn("split_0");
lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit, split0));
} else {
lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit));
}
return pkg;
}
private PackageState createPackageState(
String packageName, List<SharedLibrary> deps, boolean multiSplit) {
PackageState pkgState = mock(PackageState.class);
lenient().when(pkgState.getPackageName()).thenReturn(packageName);
lenient().when(pkgState.getAppId()).thenReturn(12345);
lenient().when(pkgState.getSharedLibraryDependencies()).thenReturn(deps);
AndroidPackage pkg = createPackage(multiSplit);
lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
lenient().when(PackageStateModulesUtils.isDexoptable(pkgState)).thenReturn(true);
return pkgState;
}
private SharedLibrary createLibrary(
String libraryName, String packageName, List<SharedLibrary> deps) {
SharedLibrary library = mock(SharedLibrary.class);
lenient().when(library.getName()).thenReturn(libraryName);
lenient().when(library.getPackageName()).thenReturn(packageName);
lenient().when(library.getDependencies()).thenReturn(deps);
lenient().when(library.isNative()).thenReturn(false);
return library;
}
private void preparePackagesAndLibraries() {
// Dependency graph:
// foo bar
// | |
// lib1a (lib1) lib1b (lib1) lib1c (lib1)
// / \ / \ |
// / \ / \ |
// libbaz (libbaz) lib2 (lib2) lib4 (lib4) lib3 (lib3)
//
// "lib1a", "lib1b", and "lib1c" belong to the same package "lib1".
mRequestedPackages = List.of(PKG_NAME_FOO, PKG_NAME_BAR, PKG_NAME_LIBBAZ);
// The native library is not dexoptable.
SharedLibrary libNative = createLibrary("libnative", "com.example.libnative", List.of());
lenient().when(libNative.isNative()).thenReturn(true);
SharedLibrary libbaz = createLibrary("libbaz", PKG_NAME_LIBBAZ, List.of());
SharedLibrary lib4 = createLibrary("lib4", PKG_NAME_LIB4, List.of());
SharedLibrary lib3 = createLibrary("lib3", PKG_NAME_LIB3, List.of());
SharedLibrary lib2 = createLibrary("lib2", PKG_NAME_LIB2, List.of());
SharedLibrary lib1a = createLibrary("lib1a", PKG_NAME_LIB1, List.of(libbaz, lib2));
SharedLibrary lib1b = createLibrary("lib1b", PKG_NAME_LIB1, List.of(lib2, libNative, lib4));
SharedLibrary lib1c = createLibrary("lib1c", PKG_NAME_LIB1, List.of(lib3));
mPkgStateFoo =
createPackageState(PKG_NAME_FOO, List.of(lib1a, libNative), true /* multiSplit */);
mPkgFoo = mPkgStateFoo.getAndroidPackage();
lenient().when(mSnapshot.getPackageState(PKG_NAME_FOO)).thenReturn(mPkgStateFoo);
mPkgStateBar = createPackageState(PKG_NAME_BAR, List.of(lib1b), false /* multiSplit */);
mPkgBar = mPkgStateBar.getAndroidPackage();
lenient().when(mSnapshot.getPackageState(PKG_NAME_BAR)).thenReturn(mPkgStateBar);
mPkgStateLib1 = createPackageState(
PKG_NAME_LIB1, List.of(libbaz, lib2, lib3, lib4), false /* multiSplit */);
mPkgLib1 = mPkgStateLib1.getAndroidPackage();
lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB1)).thenReturn(mPkgStateLib1);
mPkgStateLib2 = createPackageState(PKG_NAME_LIB2, List.of(), false /* multiSplit */);
mPkgLib2 = mPkgStateLib2.getAndroidPackage();
lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB2)).thenReturn(mPkgStateLib2);
// This should not be considered as a transitive dependency of any requested package, even
// though it is a dependency of package "lib1".
PackageState pkgStateLib3 =
createPackageState(PKG_NAME_LIB3, List.of(), false /* multiSplit */);
lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB3)).thenReturn(pkgStateLib3);
mPkgStateLib4 = createPackageState(PKG_NAME_LIB4, List.of(), false /* multiSplit */);
mPkgLib4 = mPkgStateLib4.getAndroidPackage();
lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB4)).thenReturn(mPkgStateLib4);
mPkgStateLibbaz = createPackageState(PKG_NAME_LIBBAZ, List.of(), false /* multiSplit */);
mPkgLibbaz = mPkgStateLibbaz.getAndroidPackage();
lenient().when(mSnapshot.getPackageState(PKG_NAME_LIBBAZ)).thenReturn(mPkgStateLibbaz);
}
private void verifyNoDexopt() {
verify(mInjector, never()).getPrimaryDexopter(any(), any(), any(), any());
verify(mInjector, never()).getSecondaryDexopter(any(), any(), any(), any());
}
private void verifyNoMoreDexopt(int expectedPrimaryTimes, int expectedSecondaryTimes) {
verify(mInjector, times(expectedPrimaryTimes))
.getPrimaryDexopter(any(), any(), any(), any());
verify(mInjector, times(expectedSecondaryTimes))
.getSecondaryDexopter(any(), any(), any(), any());
}
private List<DexContainerFileDexoptResult> createResults(
String dexPath, @DexoptResultStatus int status1, @DexoptResultStatus int status2) {
return List.of(DexContainerFileDexoptResult.create(
dexPath, true /* isPrimaryAbi */, "arm64-v8a", "verify", status1),
DexContainerFileDexoptResult.create(
dexPath, false /* isPrimaryAbi */, "armeabi-v7a", "verify", status2));
}
private void checkPackageResult(DexoptResult result, int index, String packageName,
@DexoptResult.DexoptResultStatus int status,
List<List<DexContainerFileDexoptResult>> dexContainerFileDexoptResults) {
PackageDexoptResult packageResult = result.getPackageDexoptResults().get(index);
assertThat(packageResult.getPackageName()).isEqualTo(packageName);
assertThat(packageResult.getStatus()).isEqualTo(status);
assertThat(packageResult.getDexContainerFileDexoptResults())
.containsExactlyElementsIn(dexContainerFileDexoptResults.stream()
.flatMap(r -> r.stream())
.collect(Collectors.toList()));
}
/** An executor that delays execution until `runAll` is called. */
private static class DelayedExecutor implements Executor {
private List<Runnable> mCommands = new ArrayList<>();
public void execute(Runnable command) {
mCommands.add(command);
}
public void runAll() {
for (Runnable command : mCommands) {
command.run();
}
mCommands.clear();
}
}
}