Reschedule bg dexopt job when it's cancelled by API.
Partners may call `cancelBackgroundDexoptJob` in
`OptimizePackagesCallback` to indicate that it's not a good timing to
run the job. In this case, we should reschedule the job.
This CL also changes the stats reporting code to make use of two new
enum values added by https://r.android.com/2311750.
Bug: 255563304
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: Ia15545040e5840974f5ca41eed7f616227180fab
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 7514543..926e0aa 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -54,6 +54,7 @@
import com.android.server.art.model.OptimizeResult;
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 java.io.File;
@@ -345,11 +346,12 @@
/**
* Overrides the default params for {@link
- * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String). This method is thread-safe.
+ * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String, CancellationSignal). This
+ * method is thread-safe.
*
* This method gives users the opportunity to change the behavior of {@link
- * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String)}, which is called by ART
- * Service automatically during boot / background dexopt.
+ * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String, CancellationSignal)}, which
+ * is called by ART Service automatically during boot / background dexopt.
*
* If this method is not called, the default list of packages and options determined by {@code
* reason} will be used.
@@ -360,8 +362,9 @@
}
/**
- * Clears the callback set by {@link #setOptimizePackagesCallback(Executor,
- * OptimizePackagesCallback)}. This method is thread-safe.
+ * Clears the callback set by {@link
+ * #setOptimizePackagesCallback(Executor, OptimizePackagesCallback)}. This method is
+ * thread-safe.
*/
public void clearOptimizePackagesCallback() {
mInjector.getConfig().clearOptimizePackagesCallback();
@@ -374,9 +377,10 @@
* dexopt.
*
* The job will be run by the job scheduler. The job scheduling configuration can be overridden
- * by {@link #setScheduleBackgroundDexoptJobCallback(Executor,
- * ScheduleBackgroundDexoptJobCallback)}. By default, it runs periodically (at most once a day)
- * when all the following constraints are meet.
+ * by {@link
+ * #setScheduleBackgroundDexoptJobCallback(Executor, ScheduleBackgroundDexoptJobCallback)}. By
+ * default, it runs periodically (at most once a day) when all the following constraints are
+ * meet.
*
* <ul>
* <li>The device is idling. (see {@link JobInfo.Builder#setRequiresDeviceIdle(boolean)})
@@ -387,13 +391,15 @@
* (see {@link JobInfo.Builder#setRequiresStorageNotLow(boolean)})
* </ul>
*
- * When the job is running, the job scheduler cancels the job immediately whenever one of the
- * constraints above is no longer met, and retries it in the next <i>maintenance window</i>.
- * For information about <i>maintenance window</i>, see
+ * When the job is running, it may be cancelled by the job scheduler immediately whenever one of
+ * the constraints above is no longer met or cancelled by the {@link
+ * #cancelBackgroundDexoptJob()} API. The job scheduler retries it in the next <i>maintenance
+ * window</i>. For information about <i>maintenance window</i>, see
* https://developer.android.com/training/monitoring-device-state/doze-standby.
*
- * See {@link #optimizePackages(PackageManagerLocal.FilteredSnapshot, String,
- * CancellationSignal)} for how to customize the behavior of the job.
+ * See {@link
+ * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String, CancellationSignal)} for how
+ * to customize the behavior of the job.
*
* When the job ends (either completed or cancelled), the result is sent to the callbacks added
* by {@link #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} with the
@@ -428,8 +434,9 @@
}
/**
- * Clears the callback set by {@link #setScheduleBackgroundDexoptJobCallback(Executor,
- * ScheduleBackgroundDexoptJobCallback)}. This method is thread-safe.
+ * Clears the callback set by {@link
+ * #setScheduleBackgroundDexoptJobCallback(Executor, ScheduleBackgroundDexoptJobCallback)}. This
+ * method is thread-safe.
*/
public void clearScheduleBackgroundDexoptJobCallback() {
mInjector.getConfig().clearScheduleBackgroundDexoptJobCallback();
@@ -443,8 +450,9 @@
* constraints described in {@link #scheduleBackgroundDexoptJob()}, and hence will not be
* cancelled when they aren't met.
*
- * See {@link #optimizePackages(PackageManagerLocal.FilteredSnapshot, String,
- * CancellationSignal)} for how to customize the behavior of the job.
+ * See {@link
+ * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String, CancellationSignal)} for how
+ * to customize the behavior of the job.
*
* When the job ends (either completed or cancelled), the result is sent to the callbacks added
* by {@link #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} with the
@@ -459,8 +467,9 @@
* #startBackgroundDexoptJob()}. Does nothing if the job is not running. This method is not
* blocking.
*
- * The result sent to the callbacks added by {@link #addOptimizePackageDoneCallback(Executor,
- * OptimizePackageDoneCallback)} will contain {@link OptimizeResult#OPTIMIZE_CANCELLED}.
+ * The result sent to the callbacks added by {@link
+ * #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} will contain {@link
+ * OptimizeResult#OPTIMIZE_CANCELLED}.
*/
public void cancelBackgroundDexoptJob() {
mInjector.getBackgroundDexOptJob().cancel();
@@ -480,9 +489,9 @@
}
/**
- * Removes the listener added by {@link #addOptimizePackageDoneCallback(Executor,
- * OptimizePackageDoneCallback)}. Does nothing if the callback was not added. This method is
- * thread-safe.
+ * Removes the listener added by {@link
+ * #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)}. Does nothing if the
+ * callback was not added. This method is thread-safe.
*/
public void removeOptimizePackageDoneCallback(@NonNull OptimizePackageDoneCallback callback) {
mInjector.getConfig().removeOptimizePackageDoneCallback(callback);
@@ -643,8 +652,8 @@
public interface OptimizePackagesCallback {
/**
* Mutates {@code builder} to override the default params for {@link
- * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String). It must ignore unknown
- * reasons because more reasons may be added in the future.
+ * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String, CancellationSignal). It
+ * must ignore unknown reasons because more reasons may be added in the future.
*
* If {@code builder.setPackages} is not called, {@code defaultPackages} will be used as the
* list of packages to optimize.
@@ -653,9 +662,15 @@
* new OptimizeParams.Builder(reason)} will to used as the params for optimizing each
* package.
*
+ * Additionally, if {@code reason} is {@link ReasonMapping#REASON_BG_DEXOPT}, {@link
+ * #cancelBackgroundDexoptJob()} can be called to skip this run. The job will be retried in
+ * the next <i>maintenance window</i>. For information about <i>maintenance window</i>, see
+ * https://developer.android.com/training/monitoring-device-state/doze-standby.
+ *
* Changing the reason is not allowed. Doing so will result in {@link IllegalStateException}
- * when {@link #optimizePackages(PackageManagerLocal.FilteredSnapshot, String,
- * CancellationSignal)} is called.
+ * when {@link
+ * #optimizePackages(PackageManagerLocal.FilteredSnapshot, String, CancellationSignal)} is
+ * called.
*/
void onOverrideBatchOptimizeParams(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull @BatchOptimizeReason String reason, @NonNull List<String> defaultPackages,
diff --git a/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java b/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java
index b93bdda..28677fa 100644
--- a/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java
+++ b/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java
@@ -42,6 +42,7 @@
import com.google.auto.value.AutoValue;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@@ -64,6 +65,7 @@
@GuardedBy("this") @Nullable private CompletableFuture<Result> mRunningJob = null;
@GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null;
+ @GuardedBy("this") @NonNull private Optional<Integer> mLastStopReason = Optional.empty();
public BackgroundDexOptJob(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal,
@NonNull Config config) {
@@ -79,12 +81,16 @@
public boolean onStartJob(
@NonNull BackgroundDexOptJobService jobService, @NonNull JobParameters params) {
start().thenAcceptAsync(result -> {
- writeStats(params, result);
- // This is a periodic job, where the interval is specified in the `JobInfo`. "false"
- // means not to execute again during a future idle maintenance window in the same
- // interval. This job will be executed again in the next interval.
+ writeStats(result);
+ // This is a periodic job, where the interval is specified in the `JobInfo`. "true"
+ // means to execute again during a future idle maintenance window in the same
+ // interval, while "false" means not to execute again during a future idle maintenance
+ // window in the same interval but to execute again in the next interval.
// This call will be ignored if `onStopJob` is called.
- jobService.jobFinished(params, false /* wantReschedule */);
+ boolean wantsReschedule = result instanceof CompletedResult
+ && ((CompletedResult) result).dexoptResult().getFinalStatus()
+ == OptimizeResult.OPTIMIZE_CANCELLED;
+ jobService.jobFinished(params, wantsReschedule);
});
// "true" means the job will continue running until `jobFinished` is called.
return true;
@@ -92,6 +98,9 @@
/** Handles {@link BackgroundDexOptJobService#onStopJob(JobParameters)}. */
public boolean onStopJob(@NonNull JobParameters params) {
+ synchronized (this) {
+ mLastStopReason = Optional.of(params.getStopReason());
+ }
cancel();
// "true" means to execute again during a future idle maintenance window in the same
// interval.
@@ -149,6 +158,7 @@
}
mCancellationSignal = new CancellationSignal();
+ mLastStopReason = Optional.empty();
mRunningJob = new CompletableFuture().supplyAsync(() -> {
Log.i(TAG, "Job started");
try {
@@ -191,22 +201,37 @@
return CompletedResult.create(dexoptResult, SystemClock.uptimeMillis() - startTimeMs);
}
- private void writeStats(@NonNull JobParameters params, @NonNull Result result) {
+ private void writeStats(@NonNull Result result) {
+ Optional<Integer> stopReason;
+ synchronized (this) {
+ stopReason = mLastStopReason;
+ }
if (result instanceof CompletedResult) {
var completedResult = (CompletedResult) result;
- int status = completedResult.dexoptResult().getFinalStatus()
- == OptimizeResult.OPTIMIZE_CANCELLED
- ? ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION
- : ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED;
- ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED, status,
- params.getStopReason(), completedResult.durationMs(), 0 /* deprecated */);
+ ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
+ getStatusForStats(completedResult, stopReason),
+ stopReason.orElse(JobParameters.STOP_REASON_UNDEFINED),
+ completedResult.durationMs(), 0 /* deprecated */);
} else if (result instanceof FatalErrorResult) {
ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
- ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN,
+ ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_FATAL_ERROR,
JobParameters.STOP_REASON_UNDEFINED, 0 /* durationMs */, 0 /* deprecated */);
}
}
+ private int getStatusForStats(@NonNull CompletedResult result, Optional<Integer> stopReason) {
+ if (result.dexoptResult().getFinalStatus() == OptimizeResult.OPTIMIZE_CANCELLED) {
+ if (stopReason.isPresent()) {
+ return ArtStatsLog
+ .BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_CANCELLATION;
+ } else {
+ return ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_ABORT_BY_API;
+ }
+ } else {
+ return ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_JOB_FINISHED;
+ }
+ }
+
static abstract class Result {}
static class FatalErrorResult extends Result {}
diff --git a/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java b/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java
index c235f65..3bf229c 100644
--- a/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java
+++ b/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java
@@ -32,6 +32,7 @@
import static org.mockito.Mockito.when;
import android.app.job.JobInfo;
+import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.os.CancellationSignal;
import android.os.SystemProperties;
@@ -74,8 +75,11 @@
@Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
@Mock private JobScheduler mJobScheduler;
@Mock private OptimizeResult mOptimizeResult;
+ @Mock private BackgroundDexOptJobService mJobService;
+ @Mock private JobParameters mJobParameters;
private Config mConfig;
private BackgroundDexOptJob mBackgroundDexOptJob;
+ private Semaphore mJobFinishedCalled = new Semaphore(0);
@Before
public void setUp() throws Exception {
@@ -94,6 +98,18 @@
mBackgroundDexOptJob = new BackgroundDexOptJob(mInjector);
lenient().when(BackgroundDexOptJobService.getJob()).thenReturn(mBackgroundDexOptJob);
+
+ lenient()
+ .doAnswer(invocation -> {
+ mJobFinishedCalled.release();
+ return null;
+ })
+ .when(mJobService)
+ .jobFinished(any(), anyBoolean());
+
+ lenient()
+ .when(mJobParameters.getStopReason())
+ .thenReturn(JobParameters.STOP_REASON_UNDEFINED);
}
@Test
@@ -241,4 +257,37 @@
mBackgroundDexOptJob.unschedule();
verify(mJobScheduler).cancel(anyInt());
}
+
+ @Test
+ public void testWantsRescheduleFalsePerformed() throws Exception {
+ when(mOptimizeResult.getFinalStatus()).thenReturn(OptimizeResult.OPTIMIZE_PERFORMED);
+ when(mArtManagerLocal.optimizePackages(any(), any(), any())).thenReturn(mOptimizeResult);
+
+ mBackgroundDexOptJob.onStartJob(mJobService, mJobParameters);
+ assertThat(mJobFinishedCalled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+
+ verify(mJobService).jobFinished(any(), eq(false) /* wantsReschedule */);
+ }
+
+ @Test
+ public void testWantsRescheduleFalseFatalError() throws Exception {
+ when(mArtManagerLocal.optimizePackages(any(), any(), any()))
+ .thenThrow(RuntimeException.class);
+
+ mBackgroundDexOptJob.onStartJob(mJobService, mJobParameters);
+ assertThat(mJobFinishedCalled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+
+ verify(mJobService).jobFinished(any(), eq(false) /* wantsReschedule */);
+ }
+
+ @Test
+ public void testWantsRescheduleTrue() throws Exception {
+ when(mOptimizeResult.getFinalStatus()).thenReturn(OptimizeResult.OPTIMIZE_CANCELLED);
+ when(mArtManagerLocal.optimizePackages(any(), any(), any())).thenReturn(mOptimizeResult);
+
+ mBackgroundDexOptJob.onStartJob(mJobService, mJobParameters);
+ assertThat(mJobFinishedCalled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+
+ verify(mJobService).jobFinished(any(), eq(true) /* wantsReschedule */);
+ }
}