Continue dexopt for other packages when a package reports an error

- The job itelf will report an error if any error happens.
- The failed APK will be added to failed list so that it is not
  tried agai until the package is updated.
- Updated the unit test to test this logic.

Bug: 230869482
Test: atest com.android.server.pm.BackgroundDexOptServiceUnitTest
      atest android.compilation.cts.BackgroundDexOptimizationTest
Change-Id: I3a55b07b0f88d94fdf8a70f3e365be743dc1782e
This commit is contained in:
Keun young Park
2022-05-04 17:13:03 -07:00
parent f19925afc9
commit e78c034bbb
2 changed files with 216 additions and 38 deletions

View File

@@ -91,17 +91,21 @@ public final class BackgroundDexOptService {
// Possible return codes of individual optimization steps.
/** Ok status: Optimizations finished, All packages were processed, can continue */
private static final int STATUS_OK = 0;
/* package */ static final int STATUS_OK = 0;
/** Optimizations should be aborted. Job scheduler requested it. */
private static final int STATUS_ABORT_BY_CANCELLATION = 1;
/* package */ static final int STATUS_ABORT_BY_CANCELLATION = 1;
/** Optimizations should be aborted. No space left on device. */
private static final int STATUS_ABORT_NO_SPACE_LEFT = 2;
/* package */ static final int STATUS_ABORT_NO_SPACE_LEFT = 2;
/** Optimizations should be aborted. Thermal throttling level too high. */
private static final int STATUS_ABORT_THERMAL = 3;
/* package */ static final int STATUS_ABORT_THERMAL = 3;
/** Battery level too low */
private static final int STATUS_ABORT_BATTERY = 4;
/** {@link PackageDexOptimizer#DEX_OPT_FAILED} case */
private static final int STATUS_DEX_OPT_FAILED = 5;
/* package */ static final int STATUS_ABORT_BATTERY = 4;
/**
* {@link PackageDexOptimizer#DEX_OPT_FAILED} case. This state means some packages have failed
* compilation during the job. Note that the failure will not be permanent as the next dexopt
* job will exclude those failed packages.
*/
/* package */ static final int STATUS_DEX_OPT_FAILED = 5;
@IntDef(prefix = {"STATUS_"}, value = {
STATUS_OK,
@@ -525,7 +529,10 @@ public final class BackgroundDexOptService {
}
}
/** Returns true if completed */
/**
* Returns whether we've successfully run the job. Note that it will return true even if some
* packages may have failed compiling.
*/
private boolean runIdleOptimization(PackageManagerService pm, List<String> pkgs,
boolean isPostBootUpdate) {
synchronized (mLock) {
@@ -541,7 +548,7 @@ public final class BackgroundDexOptService {
mLastExecutionDurationMs = SystemClock.elapsedRealtime() - mLastExecutionStartTimeMs;
}
return status == STATUS_OK;
return status == STATUS_OK || status == STATUS_DEX_OPT_FAILED;
}
/** Gets the size of the directory. It uses recursion to go over all files. */
@@ -661,6 +668,11 @@ public final class BackgroundDexOptService {
ArraySet<String> updatedPackages, boolean isPostBootUpdate) {
boolean supportSecondaryDex = mInjector.supportSecondaryDex();
// Keep the error if there is any error from any package.
@Status int status = STATUS_OK;
// Other than cancellation, all packages will be processed even if an error happens
// in a package.
for (String pkg : pkgs) {
int abortCode = abortIdleOptimizations(lowStorageThreshold);
if (abortCode != STATUS_OK) {
@@ -670,10 +682,13 @@ public final class BackgroundDexOptService {
@DexOptResult int primaryResult =
optimizePackage(pkg, true /* isForPrimaryDex */, isPostBootUpdate);
if (primaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) {
return STATUS_ABORT_BY_CANCELLATION;
}
if (primaryResult == PackageDexOptimizer.DEX_OPT_PERFORMED) {
updatedPackages.add(pkg);
} else if (primaryResult != PackageDexOptimizer.DEX_OPT_SKIPPED) {
return convertPackageDexOptimizerStatusToInternal(primaryResult);
} else if (primaryResult == PackageDexOptimizer.DEX_OPT_FAILED) {
status = convertPackageDexOptimizerStatusToInternal(primaryResult);
}
if (!supportSecondaryDex) {
@@ -682,12 +697,14 @@ public final class BackgroundDexOptService {
@DexOptResult int secondaryResult =
optimizePackage(pkg, false /* isForPrimaryDex */, isPostBootUpdate);
if (secondaryResult != PackageDexOptimizer.DEX_OPT_PERFORMED
&& secondaryResult != PackageDexOptimizer.DEX_OPT_SKIPPED) {
return convertPackageDexOptimizerStatusToInternal(secondaryResult);
if (secondaryResult == PackageDexOptimizer.DEX_OPT_CANCELLED) {
return STATUS_ABORT_BY_CANCELLATION;
}
if (secondaryResult == PackageDexOptimizer.DEX_OPT_FAILED) {
status = convertPackageDexOptimizerStatusToInternal(secondaryResult);
}
}
return STATUS_OK;
return status;
}
/**
@@ -779,9 +796,9 @@ public final class BackgroundDexOptService {
@DexOptResult
private int performDexOptPrimary(String pkg, int reason,
int dexoptFlags) {
DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, dexoptFlags);
return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true,
() -> mDexOptHelper.performDexOptWithStatus(
new DexoptOptions(pkg, reason, dexoptFlags)));
() -> mDexOptHelper.performDexOptWithStatus(dexoptOptions));
}
@DexOptResult
@@ -791,7 +808,7 @@ public final class BackgroundDexOptService {
dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX);
return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false,
() -> mDexOptHelper.performDexOpt(dexoptOptions)
? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED
);
}

View File

@@ -16,6 +16,9 @@
package com.android.server.pm;
import static com.android.server.pm.BackgroundDexOptService.STATUS_DEX_OPT_FAILED;
import static com.android.server.pm.BackgroundDexOptService.STATUS_OK;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -23,12 +26,14 @@ import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
import android.annotation.Nullable;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
@@ -38,10 +43,13 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.os.HandlerThread;
import android.os.PowerManager;
import android.util.Log;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.PinnerService;
import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.DexoptOptions;
import org.junit.After;
import org.junit.Before;
@@ -52,7 +60,11 @@ import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
@@ -66,8 +78,12 @@ public final class BackgroundDexOptServiceUnitTest {
private static final long TEST_WAIT_TIMEOUT_MS = 10_000;
private static final List<String> DEFAULT_PACKAGE_LIST = List.of("aaa", "bbb");
private static final List<String> EMPTY_PACKAGE_LIST = List.of();
private static final String PACKAGE_AAA = "aaa";
private static final List<String> DEFAULT_PACKAGE_LIST = List.of(PACKAGE_AAA, "bbb");
private int mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
// Store expected dexopt sequence for verification.
private ArrayList<DexOptInfo> mDexInfoSequence = new ArrayList<>();
@Mock
private Context mContext;
@@ -116,14 +132,23 @@ public final class BackgroundDexOptServiceUnitTest {
when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL);
when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE);
when(mInjector.supportSecondaryDex()).thenReturn(true);
when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST);
when(mDexOptHelper.performDexOptWithStatus(any())).thenReturn(
PackageDexOptimizer.DEX_OPT_PERFORMED);
when(mDexOptHelper.performDexOpt(any())).thenReturn(true);
setupDexOptHelper();
mService = new BackgroundDexOptService(mInjector);
}
private void setupDexOptHelper() {
when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST);
when(mDexOptHelper.performDexOptWithStatus(any())).thenAnswer(inv -> {
DexoptOptions opt = inv.getArgument(0);
if (opt.getPackageName().equals(PACKAGE_AAA)) {
return mDexOptResultForPackageAAA;
}
return PackageDexOptimizer.DEX_OPT_PERFORMED;
});
when(mDexOptHelper.performDexOpt(any())).thenReturn(true);
}
@After
public void tearDown() throws Exception {
LocalServices.removeServiceForTest(BackgroundDexOptService.class);
@@ -159,7 +184,7 @@ public final class BackgroundDexOptServiceUnitTest {
@Test
public void testNoExecutionForNoOptimizablePackages() {
initUntilBootCompleted();
when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(EMPTY_PACKAGE_LIST);
when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(Collections.emptyList());
assertThat(mService.onStartJob(mJobServiceForPostBoot,
mJobParametersForPostBoot)).isFalse();
@@ -170,15 +195,70 @@ public final class BackgroundDexOptServiceUnitTest {
public void testPostBootUpdateFullRun() {
initUntilBootCompleted();
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1);
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
/* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
/* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
}
@Test
public void testPostBootUpdateFullRunWithPackageFailure() {
mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED;
initUntilBootCompleted();
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
/* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED,
/* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
assertThat(getFailedPackageNamesSecondary()).isEmpty();
}
@Test
public void testIdleJobFullRun() {
initUntilBootCompleted();
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
/* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK,
/* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
runFullJob(mJobServiceForIdle, mJobParametersForIdle,
/* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
/* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null);
}
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1);
runFullJob(mJobServiceForIdle, mJobParametersForIdle, true, 2);
@Test
public void testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate() {
mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED;
initUntilBootCompleted();
runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot,
/* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED,
/* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
assertThat(getFailedPackageNamesSecondary()).isEmpty();
runFullJob(mJobServiceForIdle, mJobParametersForIdle,
/* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
/* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA);
assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA);
assertThat(getFailedPackageNamesSecondary()).isEmpty();
mService.notifyPackageChanged(PACKAGE_AAA);
assertThat(getFailedPackageNamesPrimary()).isEmpty();
assertThat(getFailedPackageNamesSecondary()).isEmpty();
// Succeed this time.
mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED;
runFullJob(mJobServiceForIdle, mJobParametersForIdle,
/* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK,
/* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null);
assertThat(getFailedPackageNamesPrimary()).isEmpty();
assertThat(getFailedPackageNamesSecondary()).isEmpty();
}
@Test
@@ -404,8 +484,10 @@ public final class BackgroundDexOptServiceUnitTest {
}
private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params,
boolean expectedReschedule, int totalJobRuns) {
boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams,
@Nullable String expectedSkippedPackage) {
when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread());
addFullRunSequence(expectedSkippedPackage);
assertThat(mService.onStartJob(jobService, params)).isTrue();
ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class);
@@ -413,20 +495,99 @@ public final class BackgroundDexOptServiceUnitTest {
argThreadRunnable.getValue().run();
verify(jobService).jobFinished(params, expectedReschedule);
verify(jobService, times(totalJobFinishedWithParams)).jobFinished(params,
expectedReschedule);
// Never block
verify(mDexOptHelper, never()).controlDexOptBlocking(true);
verifyPerformDexOpt(DEFAULT_PACKAGE_LIST, totalJobRuns);
verifyPerformDexOpt();
assertThat(getLastExecutionStatus()).isEqualTo(expectedStatus);
}
private void verifyPerformDexOpt(List<String> pkgs, int expectedRuns) {
private void verifyPerformDexOpt() {
InOrder inOrder = inOrder(mDexOptHelper);
for (int i = 0; i < expectedRuns; i++) {
for (String pkg : pkgs) {
inOrder.verify(mDexOptHelper, times(1)).performDexOptWithStatus(argThat((option) ->
option.getPackageName().equals(pkg) && !option.isDexoptOnlySecondaryDex()));
inOrder.verify(mDexOptHelper, times(1)).performDexOpt(argThat((option) ->
option.getPackageName().equals(pkg) && option.isDexoptOnlySecondaryDex()));
inOrder.verify(mDexOptHelper).getOptimizablePackages(any());
for (DexOptInfo info : mDexInfoSequence) {
if (info.isPrimary) {
verify(mDexOptHelper).performDexOptWithStatus(
argThat((option) -> option.getPackageName().equals(info.packageName)
&& !option.isDexoptOnlySecondaryDex()));
} else {
inOrder.verify(mDexOptHelper).performDexOpt(
argThat((option) -> option.getPackageName().equals(info.packageName)
&& option.isDexoptOnlySecondaryDex()));
}
}
// Even InOrder cannot check the order if the same call is made multiple times.
// To check the order across multiple runs, we reset the mock so that order can be checked
// in each call.
mDexInfoSequence.clear();
reset(mDexOptHelper);
setupDexOptHelper();
}
private String findDumpValueForKey(String key) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintWriter pw = new PrintWriter(out, true);
IndentingPrintWriter writer = new IndentingPrintWriter(pw, "");
try {
mService.dump(writer);
writer.flush();
Log.i(TAG, "dump output:" + out.toString());
for (String line : out.toString().split(System.lineSeparator())) {
String[] vals = line.split(":");
if (vals[0].equals(key)) {
if (vals.length == 2) {
return vals[1].strip();
} else {
break;
}
}
}
return "";
} finally {
writer.close();
}
}
List<String> findStringListFromDump(String key) {
String values = findDumpValueForKey(key);
if (values.isEmpty()) {
return Collections.emptyList();
}
return Arrays.asList(values.split(","));
}
private List<String> getFailedPackageNamesPrimary() {
return findStringListFromDump("mFailedPackageNamesPrimary");
}
private List<String> getFailedPackageNamesSecondary() {
return findStringListFromDump("mFailedPackageNamesSecondary");
}
private int getLastExecutionStatus() {
return Integer.parseInt(findDumpValueForKey("mLastExecutionStatus"));
}
private static class DexOptInfo {
public final String packageName;
public final boolean isPrimary;
private DexOptInfo(String packageName, boolean isPrimary) {
this.packageName = packageName;
this.isPrimary = isPrimary;
}
}
private void addFullRunSequence(@Nullable String expectedSkippedPackage) {
for (String packageName : DEFAULT_PACKAGE_LIST) {
if (packageName.equals(expectedSkippedPackage)) {
// only fails primary dexopt in mocking but add secodary
mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false));
} else {
mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ true));
mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false));
}
}
}