Implement background dexopt job.
Bug: 255563304
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services
Change-Id: I6ecbee4492304f1899f4188244a91c23e89afb38
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index cd43853..55653b5 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -86,8 +86,16 @@
],
libs: [
"auto_value_annotations",
+ // TODO(b/256866172): Transitive dependency, for r8 only.
+ "framework-statsd.stubs.module_lib",
+ // TODO(b/256866172): Transitive dependency, for r8 only. This module
+ // always refers to the jar in prebuilts/sdk. We can't use
+ // "framework-connectivity.stubs.module_lib" here because it's not
+ // available on master-art.
+ "sdk_module-lib_current_framework-connectivity",
],
static_libs: [
+ "art-statslog-art-java",
"artd-aidl-java",
"modules-utils-shell-command-handler",
"service-art-proto-java",
@@ -118,6 +126,29 @@
],
}
+java_library {
+ name: "art-statslog-art-java",
+ srcs: [
+ ":art-statslog-art-java-gen",
+ ],
+ libs: [
+ "framework-statsd.stubs.module_lib",
+ ],
+ sdk_version: "system_server_current",
+ min_sdk_version: "31",
+ apex_available: [
+ "com.android.art",
+ "com.android.art.debug",
+ ],
+}
+
+genrule {
+ name: "art-statslog-art-java-gen",
+ tools: ["stats-log-api-gen"],
+ cmd: "$(location stats-log-api-gen) --java $(out) --module art --javaPackage com.android.server.art --javaClass ArtStatsLog",
+ out: ["java/com/android/server/art/ArtStatsLog.java"],
+}
+
art_cc_defaults {
name: "art_libartservice_tests_defaults",
defaults: ["libartservice_defaults"],
diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt
index 1003030..6639926 100644
--- a/libartservice/service/api/system-server-current.txt
+++ b/libartservice/service/api/system-server-current.txt
@@ -4,7 +4,9 @@
public final class ArtManagerLocal {
ctor @Deprecated public ArtManagerLocal();
ctor public ArtManagerLocal(@NonNull android.content.Context);
+ method public void cancelBackgroundDexoptJob();
method public void clearOptimizePackagesCallback();
+ method public void clearScheduleBackgroundDexoptJobCallback();
method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String);
method @NonNull public com.android.server.art.model.DeleteResult deleteOptimizedArtifacts(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, int);
method @NonNull public com.android.server.art.model.OptimizationStatus getOptimizationStatus(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String);
@@ -13,13 +15,21 @@
method public void notifyDexContainersLoaded(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull java.util.Map<java.lang.String,java.lang.String>);
method @NonNull public com.android.server.art.model.OptimizeResult optimizePackage(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull com.android.server.art.model.OptimizeParams);
method @NonNull public com.android.server.art.model.OptimizeResult optimizePackage(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull com.android.server.art.model.OptimizeParams, @NonNull android.os.CancellationSignal);
+ method public int scheduleBackgroundDexoptJob();
method public void setOptimizePackagesCallback(@NonNull java.util.concurrent.Executor, @NonNull com.android.server.art.ArtManagerLocal.OptimizePackagesCallback);
+ method public void setScheduleBackgroundDexoptJobCallback(@NonNull java.util.concurrent.Executor, @NonNull com.android.server.art.ArtManagerLocal.ScheduleBackgroundDexoptJobCallback);
+ method public void startBackgroundDexoptJob();
+ method public void unscheduleBackgroundDexoptJob();
}
public static interface ArtManagerLocal.OptimizePackagesCallback {
method public void onOverrideBatchOptimizeParams(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @NonNull java.util.List<java.lang.String>, @NonNull com.android.server.art.model.BatchOptimizeParams.Builder);
}
+ public static interface ArtManagerLocal.ScheduleBackgroundDexoptJobCallback {
+ method public void onOverrideJobInfo(@NonNull android.app.job.JobInfo.Builder);
+ }
+
public class ReasonMapping {
field public static final String REASON_BG_DEXOPT = "bg-dexopt";
field public static final String REASON_BOOT_AFTER_OTA = "boot-after-ota";
@@ -51,6 +61,9 @@
field public static final int PRIORITY_BOOT = 100; // 0x64
field public static final int PRIORITY_INTERACTIVE = 60; // 0x3c
field public static final int PRIORITY_INTERACTIVE_FAST = 80; // 0x50
+ field public static final int SCHEDULE_DISABLED_BY_SYSPROP = 2; // 0x2
+ field public static final int SCHEDULE_JOB_SCHEDULER_FAILURE = 1; // 0x1
+ field public static final int SCHEDULE_SUCCESS = 0; // 0x0
}
public abstract class BatchOptimizeParams {
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index cc64f71..ed14e19 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -22,6 +22,7 @@
import static com.android.server.art.Utils.Abi;
import static com.android.server.art.model.ArtFlags.DeleteFlags;
import static com.android.server.art.model.ArtFlags.GetStatusFlags;
+import static com.android.server.art.model.ArtFlags.ScheduleStatus;
import static com.android.server.art.model.Config.Callback;
import static com.android.server.art.model.OptimizationStatus.DexContainerFileOptimizationStatus;
@@ -30,6 +31,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.job.JobInfo;
import android.apphibernation.AppHibernationManager;
import android.content.Context;
import android.os.Binder;
@@ -78,11 +80,11 @@
@Deprecated
public ArtManagerLocal() {
- this(new Injector(null /* context */));
+ mInjector = new Injector(this, null /* context */);
}
public ArtManagerLocal(@NonNull Context context) {
- this(new Injector(context));
+ mInjector = new Injector(this, context);
}
/** @hide */
@@ -248,6 +250,9 @@
* Optimizes a package. The time this operation takes ranges from a few milliseconds to several
* minutes, depending on the params and the code size of the package.
*
+ * When this operation ends (either completed or cancelled), callbacks added by {@link
+ * #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} are called.
+ *
* @throws IllegalArgumentException if the package is not found or the params are illegal
* @throws IllegalStateException if an internal error occurs
*/
@@ -300,6 +305,9 @@
* usage is always bound by {@code dalvik.vm.*dex2oat-cpu-set} regardless of the number of
* threads.
*
+ * When this operation ends (either completed or cancelled), callbacks added by {@link
+ * #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} are called.
+ *
* @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
* @param reason determines the default list of packages and options
* @param cancellationSignal provides the ability to cancel this operation
@@ -358,6 +366,105 @@
}
/**
+ * Schedules a background dexopt job. Does nothing if the job is already scheduled.
+ *
+ * Use this method if you want the system to automatically determine the best time to run
+ * 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.
+ *
+ * <ul>
+ * <li>The device is idling. (see {@link JobInfo.Builder#setRequiresDeviceIdle(boolean)})
+ * <li>The device is charging. (see {@link JobInfo.Builder#setRequiresCharging(boolean)})
+ * <li>The battery level is not low.
+ * (see {@link JobInfo.Builder#setRequiresBatteryNotLow(boolean)})
+ * <li>The free storage space is not low.
+ * (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
+ * 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.
+ *
+ * When the job ends (either completed or cancelled), the result is sent to the callbacks added
+ * by {@link #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} with the
+ * reason {@link ReasonMapping#REASON_BG_DEXOPT}.
+ */
+ public @ScheduleStatus int scheduleBackgroundDexoptJob() {
+ return mInjector.getBackgroundDexOptJob().schedule();
+ }
+
+ /**
+ * Unschedules the background dexopt job scheduled by {@link #scheduleBackgroundDexoptJob()}.
+ * Does nothing if the job is not scheduled.
+ *
+ * Use this method if you no longer want the system to automatically run dexopt.
+ *
+ * If the job is already started by the job scheduler and is running, it will be cancelled
+ * immediately, and the result sent to the callbacks added by {@link
+ * #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} will contain {@link
+ * OptimizeResult#OPTIMIZE_CANCELLED}. Note that a job started by {@link
+ * #startBackgroundDexoptJob()} will not be cancelled by this method.
+ */
+ public void unscheduleBackgroundDexoptJob() {
+ mInjector.getBackgroundDexOptJob().unschedule();
+ }
+
+ /**
+ * Overrides the configuration of the background dexopt job. This method is thread-safe.
+ */
+ public void setScheduleBackgroundDexoptJobCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull ScheduleBackgroundDexoptJobCallback callback) {
+ mInjector.getConfig().setScheduleBackgroundDexoptJobCallback(executor, callback);
+ }
+
+ /**
+ * Clears the callback set by {@link #setScheduleBackgroundDexoptJobCallback(Executor,
+ * ScheduleBackgroundDexoptJobCallback)}. This method is thread-safe.
+ */
+ public void clearScheduleBackgroundDexoptJobCallback() {
+ mInjector.getConfig().clearScheduleBackgroundDexoptJobCallback();
+ }
+
+ /**
+ * Manually starts a background dexopt job. Does nothing if a job is already started by this
+ * method or by the job scheduler. This method is not blocking.
+ *
+ * Unlike the job started by job scheduler, the job started by this method does not respect
+ * 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.
+ *
+ * When the job ends (either completed or cancelled), the result is sent to the callbacks added
+ * by {@link #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} with the
+ * reason {@link ReasonMapping#REASON_BG_DEXOPT}.
+ */
+ public void startBackgroundDexoptJob() {
+ mInjector.getBackgroundDexOptJob().start();
+ }
+
+ /**
+ * Cancels the running background dexopt job started by the job scheduler or by {@link
+ * #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}.
+ */
+ public void cancelBackgroundDexoptJob() {
+ mInjector.getBackgroundDexOptJob().cancel();
+ }
+
+ /**
* Notifies ART Service that a list of dex container files have been loaded.
*
* ART Service uses this information to:
@@ -381,6 +488,16 @@
snapshot, loadingPackageName, classLoaderContextByDexContainerFile);
}
+ /**
+ * Should be used by {@link BackgroundDexOptJobService} ONLY.
+ *
+ * @hide
+ */
+ @NonNull
+ BackgroundDexOptJob getBackgroundDexOptJob() {
+ return mInjector.getBackgroundDexOptJob();
+ }
+
@NonNull
private List<String> getDefaultPackages(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@NonNull @BatchOptimizeReason String reason) {
@@ -415,6 +532,17 @@
@NonNull BatchOptimizeParams.Builder builder);
}
+ public interface ScheduleBackgroundDexoptJobCallback {
+ /**
+ * Mutates {@code builder} to override the configuration of the background dexopt job.
+ *
+ * The default configuration described in {@link
+ * ArtManagerLocal#scheduleBackgroundDexoptJob()} is passed to the callback as the {@code
+ * builder} argument.
+ */
+ void onOverrideJobInfo(@NonNull JobInfo.Builder builder);
+ }
+
/**
* Injector pattern for testing purpose.
*
@@ -425,16 +553,19 @@
@Nullable private final Context mContext;
@Nullable private final PackageManagerLocal mPackageManagerLocal;
@Nullable private final Config mConfig;
+ @Nullable private final BackgroundDexOptJob mBgDexOptJob;
- Injector(@Nullable Context context) {
+ Injector(@NonNull ArtManagerLocal artManagerLocal, @Nullable Context context) {
mContext = context;
if (context != null) {
// We only need them on Android U and above, where a context is passed.
mPackageManagerLocal = LocalManagerRegistry.getManager(PackageManagerLocal.class);
mConfig = new Config();
+ mBgDexOptJob = new BackgroundDexOptJob(context, artManagerLocal, mConfig);
} else {
mPackageManagerLocal = null;
mConfig = null;
+ mBgDexOptJob = null;
}
}
@@ -467,5 +598,10 @@
public AppHibernationManager getAppHibernationManager() {
return Objects.requireNonNull(mContext.getSystemService(AppHibernationManager.class));
}
+
+ @NonNull
+ public BackgroundDexOptJob getBackgroundDexOptJob() {
+ return Objects.requireNonNull(mBgDexOptJob);
+ }
}
}
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index b50ad3e..d0986c1 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -183,6 +183,42 @@
throw new RuntimeException(e);
}
}
+ case "bg-dexopt-job": {
+ String opt = getNextOption();
+ if (opt == null) {
+ mArtManagerLocal.startBackgroundDexoptJob();
+ return 0;
+ }
+ switch (opt) {
+ case "--cancel": {
+ mArtManagerLocal.cancelBackgroundDexoptJob();
+ return 0;
+ }
+ case "--enable": {
+ // This operation requires the uid to be "system" (1000).
+ long identityToken = Binder.clearCallingIdentity();
+ try {
+ mArtManagerLocal.scheduleBackgroundDexoptJob();
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return 0;
+ }
+ case "--disable": {
+ // This operation requires the uid to be "system" (1000).
+ long identityToken = Binder.clearCallingIdentity();
+ try {
+ mArtManagerLocal.unscheduleBackgroundDexoptJob();
+ } finally {
+ Binder.restoreCallingIdentity(identityToken);
+ }
+ return 0;
+ }
+ default:
+ pw.println("Error: Unknown option: " + opt);
+ return 1;
+ }
+ }
default:
// Handles empty, help, and invalid commands.
return handleDefaultCommands(cmd);
@@ -242,6 +278,22 @@
pw.println(" Save dex use information to a file in binary proto format.");
pw.println(" dex-use-load PATH");
pw.println(" Load dex use information from a file in binary proto format.");
+ pw.println(" bg-dexopt-job [--cancel | --disable | --enable]");
+ pw.println(" Control the background dexopt job.");
+ pw.println(" Without flags, it starts a background dexopt job immediately. It does");
+ pw.println(" nothing if a job is already started either automatically by the system");
+ pw.println(" or through this command. This command is not blocking.");
+ pw.println(" Options:");
+ pw.println(" --cancel Cancel any currently running background dexopt job");
+ pw.println(" immediately. This cancels jobs started either automatically by the");
+ pw.println(" system or through this command. This command is not blocking.");
+ pw.println(" --disable: Disable the background dexopt job from being started by the");
+ pw.println(" job scheduler. If a job is already started by the job scheduler and");
+ pw.println(" is running, it will be cancelled immediately. Does not affect");
+ pw.println(" jobs started through this command or by the system in other ways.");
+ pw.println(" This state will be lost when the system_server process exits.");
+ pw.println(" --enable: Enable the background dexopt job to be started by the job");
+ pw.println(" scheduler again, if previously disabled by --disable.");
}
private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java b/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java
new file mode 100644
index 0000000..b93bdda
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/BackgroundDexOptJob.java
@@ -0,0 +1,262 @@
+/*
+ * 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.ScheduleBackgroundDexoptJobCallback;
+import static com.android.server.art.model.ArtFlags.ScheduleStatus;
+import static com.android.server.art.model.Config.Callback;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.CancellationSignal;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalManagerRegistry;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.Config;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.pm.PackageManagerLocal;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+/** @hide */
+public class BackgroundDexOptJob {
+ private static final String TAG = "BackgroundDexOptJob";
+
+ /**
+ * "android" is the package name for a <service> declared in
+ * frameworks/base/core/res/AndroidManifest.xml
+ */
+ private static final String JOB_PKG_NAME = Utils.PLATFORM_PACKAGE_NAME;
+ /** An arbitrary number. Must be unique among all jobs owned by the system uid. */
+ private static final int JOB_ID = 27873780;
+
+ @VisibleForTesting public static final long JOB_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
+
+ @NonNull private final Injector mInjector;
+
+ @GuardedBy("this") @Nullable private CompletableFuture<Result> mRunningJob = null;
+ @GuardedBy("this") @Nullable private CancellationSignal mCancellationSignal = null;
+
+ public BackgroundDexOptJob(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal,
+ @NonNull Config config) {
+ this(new Injector(context, artManagerLocal, config));
+ }
+
+ @VisibleForTesting
+ public BackgroundDexOptJob(@NonNull Injector injector) {
+ mInjector = injector;
+ }
+
+ /** Handles {@link BackgroundDexOptJobService#onStartJob(JobParameters)}. */
+ 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.
+ // This call will be ignored if `onStopJob` is called.
+ jobService.jobFinished(params, false /* wantReschedule */);
+ });
+ // "true" means the job will continue running until `jobFinished` is called.
+ return true;
+ }
+
+ /** Handles {@link BackgroundDexOptJobService#onStopJob(JobParameters)}. */
+ public boolean onStopJob(@NonNull JobParameters params) {
+ cancel();
+ // "true" means to execute again during a future idle maintenance window in the same
+ // interval.
+ return true;
+ }
+
+ /** Handles {@link ArtManagerLocal#scheduleBackgroundDexoptJob()}. */
+ public @ScheduleStatus int schedule() {
+ if (this != BackgroundDexOptJobService.getJob()) {
+ throw new IllegalStateException("This job cannot be scheduled");
+ }
+
+ if (SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false /* def */)) {
+ Log.i(TAG, "Job is disabled by system property 'pm.dexopt.disable_bg_dexopt'");
+ return ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP;
+ }
+
+ JobInfo.Builder builder =
+ new JobInfo
+ .Builder(JOB_ID,
+ new ComponentName(
+ JOB_PKG_NAME, BackgroundDexOptJobService.class.getName()))
+ .setPeriodic(JOB_INTERVAL_MS)
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setRequiresBatteryNotLow(true)
+ .setRequiresStorageNotLow(true);
+
+ Callback<ScheduleBackgroundDexoptJobCallback> callback =
+ mInjector.getConfig().getScheduleBackgroundDexoptJobCallback();
+ if (callback != null) {
+ Utils.executeAndWait(
+ callback.executor(), () -> { callback.get().onOverrideJobInfo(builder); });
+ }
+
+ return mInjector.getJobScheduler().schedule(builder.build()) == JobScheduler.RESULT_SUCCESS
+ ? ArtFlags.SCHEDULE_SUCCESS
+ : ArtFlags.SCHEDULE_JOB_SCHEDULER_FAILURE;
+ }
+
+ /** Handles {@link ArtManagerLocal#unscheduleBackgroundDexoptJob()}. */
+ public void unschedule() {
+ if (this != BackgroundDexOptJobService.getJob()) {
+ throw new IllegalStateException("This job cannot be unscheduled");
+ }
+
+ mInjector.getJobScheduler().cancel(JOB_ID);
+ }
+
+ @NonNull
+ public synchronized CompletableFuture<Result> start() {
+ if (mRunningJob != null) {
+ Log.i(TAG, "Job is already running");
+ return mRunningJob;
+ }
+
+ mCancellationSignal = new CancellationSignal();
+ mRunningJob = new CompletableFuture().supplyAsync(() -> {
+ Log.i(TAG, "Job started");
+ try {
+ return run(mCancellationSignal);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Fatal error", e);
+ return new FatalErrorResult();
+ } finally {
+ Log.i(TAG, "Job finished");
+ synchronized (this) {
+ mRunningJob = null;
+ mCancellationSignal = null;
+ }
+ }
+ });
+ return mRunningJob;
+ }
+
+ public synchronized void cancel() {
+ if (mRunningJob == null) {
+ Log.i(TAG, "Job is not running");
+ return;
+ }
+
+ mCancellationSignal.cancel();
+ Log.i(TAG, "Job cancelled");
+ }
+
+ @NonNull
+ private CompletedResult run(@NonNull CancellationSignal cancellationSignal) {
+ // TODO(b/254013427): Cleanup dex use info.
+ // TODO(b/254013425): Cleanup unused secondary dex file artifacts.
+ // TODO(b/255565888): Downgrade inactive apps.
+ long startTimeMs = SystemClock.uptimeMillis();
+ OptimizeResult dexoptResult;
+ try (var snapshot = mInjector.getPackageManagerLocal().withFilteredSnapshot()) {
+ dexoptResult = mInjector.getArtManagerLocal().optimizePackages(
+ snapshot, ReasonMapping.REASON_BG_DEXOPT, cancellationSignal);
+ }
+ return CompletedResult.create(dexoptResult, SystemClock.uptimeMillis() - startTimeMs);
+ }
+
+ private void writeStats(@NonNull JobParameters params, @NonNull Result result) {
+ 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 */);
+ } else if (result instanceof FatalErrorResult) {
+ ArtStatsLog.write(ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED,
+ ArtStatsLog.BACKGROUND_DEXOPT_JOB_ENDED__STATUS__STATUS_UNKNOWN,
+ JobParameters.STOP_REASON_UNDEFINED, 0 /* durationMs */, 0 /* deprecated */);
+ }
+ }
+
+ static abstract class Result {}
+ static class FatalErrorResult extends Result {}
+
+ @AutoValue
+ static abstract class CompletedResult extends Result {
+ abstract @NonNull OptimizeResult dexoptResult();
+ abstract long durationMs();
+
+ @NonNull
+ static CompletedResult create(@NonNull OptimizeResult dexoptResult, long durationMs) {
+ return new AutoValue_BackgroundDexOptJob_CompletedResult(dexoptResult, durationMs);
+ }
+ }
+
+ /**
+ * Injector pattern for testing purpose.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class Injector {
+ @NonNull private final Context mContext;
+ @NonNull private final ArtManagerLocal mArtManagerLocal;
+ @NonNull private final Config mConfig;
+
+ Injector(@NonNull Context context, @NonNull ArtManagerLocal artManagerLocal,
+ @NonNull Config config) {
+ mContext = context;
+ mArtManagerLocal = artManagerLocal;
+ mConfig = config;
+ }
+
+ @NonNull
+ public ArtManagerLocal getArtManagerLocal() {
+ return mArtManagerLocal;
+ }
+
+ @NonNull
+ public PackageManagerLocal getPackageManagerLocal() {
+ return LocalManagerRegistry.getManager(PackageManagerLocal.class);
+ }
+
+ @NonNull
+ public Config getConfig() {
+ return mConfig;
+ }
+
+ @NonNull
+ public JobScheduler getJobScheduler() {
+ return mContext.getSystemService(JobScheduler.class);
+ }
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/BackgroundDexOptJobService.java b/libartservice/service/java/com/android/server/art/BackgroundDexOptJobService.java
new file mode 100644
index 0000000..5ab35d5
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/BackgroundDexOptJobService.java
@@ -0,0 +1,46 @@
+/*
+ * 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.app.job.JobParameters;
+import android.app.job.JobService;
+
+import com.android.server.LocalManagerRegistry;
+
+/**
+ * Entry point for the callback from the job scheduler. This class is instantiated by the system
+ * automatically.
+ *
+ * @hide
+ */
+public class BackgroundDexOptJobService extends JobService {
+ @Override
+ public boolean onStartJob(@NonNull JobParameters params) {
+ return getJob().onStartJob(this, params);
+ }
+
+ @Override
+ public boolean onStopJob(@NonNull JobParameters params) {
+ return getJob().onStopJob(params);
+ }
+
+ @NonNull
+ static BackgroundDexOptJob getJob() {
+ return LocalManagerRegistry.getManager(ArtManagerLocal.class).getBackgroundDexOptJob();
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/model/ArtFlags.java b/libartservice/service/java/com/android/server/art/model/ArtFlags.java
index 9ce066b..bf08a2f 100644
--- a/libartservice/service/java/com/android/server/art/model/ArtFlags.java
+++ b/libartservice/service/java/com/android/server/art/model/ArtFlags.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.SystemApi;
+import android.app.job.JobScheduler;
import com.android.server.art.ArtManagerLocal;
import com.android.server.art.PriorityClass;
@@ -170,5 +171,29 @@
@Retention(RetentionPolicy.SOURCE)
public @interface PriorityClassApi {}
+ /** The job has been successfully scheduled. */
+ public static final int SCHEDULE_SUCCESS = 0;
+
+ /** @see JobScheduler#RESULT_FAILURE */
+ public static final int SCHEDULE_JOB_SCHEDULER_FAILURE = 1;
+
+ /** The job is disabled by the system property {@code pm.dexopt.disable_bg_dexopt}. */
+ public static final int SCHEDULE_DISABLED_BY_SYSPROP = 2;
+
+ /**
+ * Indicates the result of scheduling a background dexopt job.
+ *
+ * @hide
+ */
+ // clang-format off
+ @IntDef(prefix = "SCHEDULE_", value = {
+ SCHEDULE_SUCCESS,
+ SCHEDULE_JOB_SCHEDULER_FAILURE,
+ SCHEDULE_DISABLED_BY_SYSPROP,
+ })
+ // clang-format on
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScheduleStatus {}
+
private ArtFlags() {}
}
diff --git a/libartservice/service/java/com/android/server/art/model/Config.java b/libartservice/service/java/com/android/server/art/model/Config.java
index 3082685..a8ed18b 100644
--- a/libartservice/service/java/com/android/server/art/model/Config.java
+++ b/libartservice/service/java/com/android/server/art/model/Config.java
@@ -17,6 +17,7 @@
package com.android.server.art.model;
import static com.android.server.art.ArtManagerLocal.OptimizePackagesCallback;
+import static com.android.server.art.ArtManagerLocal.ScheduleBackgroundDexoptJobCallback;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -40,6 +41,15 @@
@Nullable
private Callback<OptimizePackagesCallback> mOptimizePackagesCallback = null;
+ /**
+ * @see ArtManagerLocal#setScheduleBackgroundDexoptJobCallback(Executor,
+ * ScheduleBackgroundDexoptJobCallback)
+ */
+ @GuardedBy("this")
+ @Nullable
+ private Callback<ScheduleBackgroundDexoptJobCallback> mScheduleBackgroundDexoptJobCallback =
+ null;
+
public synchronized void setOptimizePackagesCallback(
@NonNull Executor executor, @NonNull OptimizePackagesCallback callback) {
mOptimizePackagesCallback = Callback.<OptimizePackagesCallback>create(callback, executor);
@@ -54,6 +64,22 @@
return mOptimizePackagesCallback;
}
+ public synchronized void setScheduleBackgroundDexoptJobCallback(
+ @NonNull Executor executor, @NonNull ScheduleBackgroundDexoptJobCallback callback) {
+ mScheduleBackgroundDexoptJobCallback =
+ Callback.<ScheduleBackgroundDexoptJobCallback>create(callback, executor);
+ }
+
+ public synchronized void clearScheduleBackgroundDexoptJobCallback() {
+ mScheduleBackgroundDexoptJobCallback = null;
+ }
+
+ @Nullable
+ public synchronized Callback<ScheduleBackgroundDexoptJobCallback>
+ getScheduleBackgroundDexoptJobCallback() {
+ return mScheduleBackgroundDexoptJobCallback;
+ }
+
@AutoValue
public static abstract class Callback<T> {
public abstract @NonNull T get();
diff --git a/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java b/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java
new file mode 100644
index 0000000..c235f65
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/BackgroundDexOptJobTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.model.Config.Callback;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.lenient;
+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.when;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.os.CancellationSignal;
+import android.os.SystemProperties;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.art.BackgroundDexOptJob.CompletedResult;
+import com.android.server.art.BackgroundDexOptJob.FatalErrorResult;
+import com.android.server.art.BackgroundDexOptJob.Result;
+import com.android.server.art.model.ArtFlags;
+import com.android.server.art.model.Config;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.art.testing.StaticMockitoRule;
+import com.android.server.pm.PackageManagerLocal;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BackgroundDexOptJobTest {
+ private static final long TIMEOUT_SEC = 1;
+
+ @Rule
+ public StaticMockitoRule mockitoRule =
+ new StaticMockitoRule(SystemProperties.class, BackgroundDexOptJobService.class);
+
+ @Mock private BackgroundDexOptJob.Injector mInjector;
+ @Mock private ArtManagerLocal mArtManagerLocal;
+ @Mock private PackageManagerLocal mPackageManagerLocal;
+ @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
+ @Mock private JobScheduler mJobScheduler;
+ @Mock private OptimizeResult mOptimizeResult;
+ private Config mConfig;
+ private BackgroundDexOptJob mBackgroundDexOptJob;
+
+ @Before
+ public void setUp() throws Exception {
+ lenient()
+ .when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean()))
+ .thenReturn(false);
+
+ lenient().when(mPackageManagerLocal.withFilteredSnapshot()).thenReturn(mSnapshot);
+
+ mConfig = new Config();
+
+ lenient().when(mInjector.getArtManagerLocal()).thenReturn(mArtManagerLocal);
+ lenient().when(mInjector.getPackageManagerLocal()).thenReturn(mPackageManagerLocal);
+ lenient().when(mInjector.getConfig()).thenReturn(mConfig);
+ lenient().when(mInjector.getJobScheduler()).thenReturn(mJobScheduler);
+
+ mBackgroundDexOptJob = new BackgroundDexOptJob(mInjector);
+ lenient().when(BackgroundDexOptJobService.getJob()).thenReturn(mBackgroundDexOptJob);
+ }
+
+ @Test
+ public void testStart() {
+ when(mArtManagerLocal.optimizePackages(
+ same(mSnapshot), eq(ReasonMapping.REASON_BG_DEXOPT), any()))
+ .thenReturn(mOptimizeResult);
+
+ Result result = Utils.getFuture(mBackgroundDexOptJob.start());
+ assertThat(result).isInstanceOf(CompletedResult.class);
+ assertThat(((CompletedResult) result).dexoptResult()).isSameInstanceAs(mOptimizeResult);
+ }
+
+ @Test
+ public void testStartAlreadyRunning() {
+ Semaphore optimizeDone = new Semaphore(0);
+ when(mArtManagerLocal.optimizePackages(any(), any(), any())).thenAnswer(invocation -> {
+ assertThat(optimizeDone.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+ return mOptimizeResult;
+ });
+
+ Future<Result> future1 = mBackgroundDexOptJob.start();
+ Future<Result> future2 = mBackgroundDexOptJob.start();
+ assertThat(future1).isSameInstanceAs(future2);
+
+ optimizeDone.release();
+ Utils.getFuture(future1);
+
+ verify(mArtManagerLocal, times(1)).optimizePackages(any(), any(), any());
+ }
+
+ @Test
+ public void testStartAnother() {
+ when(mArtManagerLocal.optimizePackages(any(), any(), any())).thenReturn(mOptimizeResult);
+
+ Future<Result> future1 = mBackgroundDexOptJob.start();
+ Utils.getFuture(future1);
+ Future<Result> future2 = mBackgroundDexOptJob.start();
+ Utils.getFuture(future2);
+ assertThat(future1).isNotSameInstanceAs(future2);
+ }
+
+ @Test
+ public void testStartFatalError() {
+ when(mArtManagerLocal.optimizePackages(any(), any(), any()))
+ .thenThrow(IllegalStateException.class);
+
+ Result result = Utils.getFuture(mBackgroundDexOptJob.start());
+ assertThat(result).isInstanceOf(FatalErrorResult.class);
+ }
+
+ @Test
+ public void testStartIgnoreDisabled() {
+ lenient()
+ .when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean()))
+ .thenReturn(true);
+
+ when(mArtManagerLocal.optimizePackages(any(), any(), any())).thenReturn(mOptimizeResult);
+
+ // The `start` method should ignore the system property. The system property is for
+ // `schedule`.
+ Utils.getFuture(mBackgroundDexOptJob.start());
+ }
+
+ @Test
+ public void testCancel() {
+ Semaphore optimizeCancelled = new Semaphore(0);
+ when(mArtManagerLocal.optimizePackages(any(), any(), any())).thenAnswer(invocation -> {
+ assertThat(optimizeCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
+ var cancellationSignal = invocation.<CancellationSignal>getArgument(2);
+ assertThat(cancellationSignal.isCanceled()).isTrue();
+ return mOptimizeResult;
+ });
+
+ Future<Result> future = mBackgroundDexOptJob.start();
+ mBackgroundDexOptJob.cancel();
+ optimizeCancelled.release();
+ Utils.getFuture(future);
+ }
+
+ @Test
+ public void testSchedule() {
+ var captor = ArgumentCaptor.forClass(JobInfo.class);
+ when(mJobScheduler.schedule(captor.capture())).thenReturn(JobScheduler.RESULT_SUCCESS);
+
+ assertThat(mBackgroundDexOptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS);
+
+ JobInfo jobInfo = captor.getValue();
+ assertThat(jobInfo.getIntervalMillis()).isEqualTo(BackgroundDexOptJob.JOB_INTERVAL_MS);
+ assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+ assertThat(jobInfo.isRequireCharging()).isTrue();
+ assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+ assertThat(jobInfo.isRequireStorageNotLow()).isTrue();
+ }
+
+ @Test
+ public void testScheduleDisabled() {
+ when(SystemProperties.getBoolean(eq("pm.dexopt.disable_bg_dexopt"), anyBoolean()))
+ .thenReturn(true);
+
+ assertThat(mBackgroundDexOptJob.schedule())
+ .isEqualTo(ArtFlags.SCHEDULE_DISABLED_BY_SYSPROP);
+
+ verify(mJobScheduler, never()).schedule(any());
+ }
+
+ @Test
+ public void testScheduleOverride() {
+ mConfig.setScheduleBackgroundDexoptJobCallback(Runnable::run, builder -> {
+ builder.setRequiresBatteryNotLow(false);
+ builder.setPriority(JobInfo.PRIORITY_LOW);
+ });
+
+ var captor = ArgumentCaptor.forClass(JobInfo.class);
+ when(mJobScheduler.schedule(captor.capture())).thenReturn(JobScheduler.RESULT_SUCCESS);
+
+ assertThat(mBackgroundDexOptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS);
+
+ JobInfo jobInfo = captor.getValue();
+ assertThat(jobInfo.getIntervalMillis()).isEqualTo(BackgroundDexOptJob.JOB_INTERVAL_MS);
+ assertThat(jobInfo.isRequireDeviceIdle()).isTrue();
+ assertThat(jobInfo.isRequireCharging()).isTrue();
+ assertThat(jobInfo.isRequireBatteryNotLow()).isFalse();
+ assertThat(jobInfo.isRequireStorageNotLow()).isTrue();
+ assertThat(jobInfo.getPriority()).isEqualTo(JobInfo.PRIORITY_LOW);
+ }
+
+ @Test
+ public void testScheduleOverrideCleared() {
+ mConfig.setScheduleBackgroundDexoptJobCallback(
+ Runnable::run, builder -> { builder.setRequiresBatteryNotLow(false); });
+ mConfig.clearScheduleBackgroundDexoptJobCallback();
+
+ var captor = ArgumentCaptor.forClass(JobInfo.class);
+ when(mJobScheduler.schedule(captor.capture())).thenReturn(JobScheduler.RESULT_SUCCESS);
+
+ assertThat(mBackgroundDexOptJob.schedule()).isEqualTo(ArtFlags.SCHEDULE_SUCCESS);
+
+ JobInfo jobInfo = captor.getValue();
+ assertThat(jobInfo.isRequireBatteryNotLow()).isTrue();
+ }
+
+ @Test
+ public void testUnschedule() {
+ mBackgroundDexOptJob.unschedule();
+ verify(mJobScheduler).cancel(anyInt());
+ }
+}
diff --git a/libartservice/service/proguard.flags b/libartservice/service/proguard.flags
index 8315cf2..8ef413f 100644
--- a/libartservice/service/proguard.flags
+++ b/libartservice/service/proguard.flags
@@ -5,3 +5,6 @@
*** set*(***);
*** has*();
}
+
+# A job service is referenced by the framework through reflection.
+-keep class * extends android.app.job.JobService { *; }