ART Service configuration

Before you begin, see a high-level overview of the ART Service.

Starting with Android 14, on-device AOT compilation for apps (a.k.a. dexopt) is handled by ART Service. ART Service is a part of the ART module, and you can customize it through system properties and APIs.

System properties

ART Service supports all the relevant dex2oat options.

Additionally, ART Service supports the following system properties:

pm.dexopt.<reason>

This is a set of system properties that determine the default compiler filters for all predefined compilation reasons described in Dexopt scenarios.

For more information, see Compiler filters.

The standard default values are:

pm.dexopt.first-boot=verify
pm.dexopt.boot-after-ota=verify
pm.dexopt.boot-after-mainline-update=verify
pm.dexopt.bg-dexopt=speed-profile
pm.dexopt.inactive=verify
pm.dexopt.cmdline=verify

pm.dexopt.shared (default: speed)

This is the fallback compiler filter for apps used by other apps.

In principle, ART Service does profile-guided compilation (speed-profile) for all apps when possible, typically during background dexopt. However, there are some apps that are used by other apps (either through <uses-library> or loaded dynamically using Context#createPackageContext with CONTEXT_INCLUDE_CODE). Such apps cannot use local profiles due to privacy reasons.

For such an app, if profile-guided compilation is requested, ART Service first tries to use a cloud profile. If a cloud profile doesn't exist, ART Service falls back to use the compiler filter specified by pm.dexopt.shared.

If the requested compilation is not profile-guided, this property has no effect.

pm.dexopt.<reason>.concurrency (default: 1)

This is the number of dex2oat invocations for certain predefined compilation reasons (first-boot, boot-after-ota, boot-after-mainline-update, and bg-dexopt).

Note that the effect of this option is combined with dex2oat resource usage options (dalvik.vm.*dex2oat-threads, dalvik.vm.*dex2oat-cpu-set, and the task profiles):

  • dalvik.vm.*dex2oat-threads controls the number of threads for each dex2oat invocation, while pm.dexopt.<reason>.concurrency controls the number of dex2oat invocations. That is, the maximum number of concurrent threads is the product of the two system properties.
  • dalvik.vm.*dex2oat-cpu-set and the task profiles always bound the CPU core usage, regardless of the maximum number of concurrent threads (discussed above).

A single dex2oat invocation may not fully utilize all the CPU cores, regardless of dalvik.vm.*dex2oat-threads. Therefore, increasing the number of dex2oat invocations (pm.dexopt.<reason>.concurrency) can utilize CPU cores better, to speed up the overall progress of dexopt. This is particularly useful during boot.

However, having too many dex2oat invocations may cause the device to run out of memory, even though this can be mitigated by setting dalvik.vm.dex2oat-swap to true to allow using a swap file. Too many invocations might also cause unnecessary context switching. Therefore, this number should be carefully tuned on a product-by-product basis.

pm.dexopt.downgrade_after_inactive_days (default: not set)

If this option is set, ART Service only dexopts apps used within the last given number of days.

In addition, If the storage is nearly low, during background dexopt, ART Service downgrades the compiler filter of apps that are not used within the last given number of days, to free up space. The compiler reason for this is inactive, and the compiler filter is determined by pm.dexopt.inactive. The space threshold to trigger this feature is the Storage Manager's low space threshold (configurable through the global settings sys_storage_threshold_percentage and sys_storage_threshold_max_bytes, default: 500MB) plus 500MB.

If you customize the list of packages through ArtManagerLocal#setBatchDexoptStartCallback, the packages in the list provided by BatchDexoptStartCallback for bg-dexopt are never downgraded.

pm.dexopt.disable_bg_dexopt (default: false)

This is for testing only. It prevents ART Service from scheduling the background dexopt job.

If the background dexopt job is already scheduled but has not run yet, this option has no effect. That is, the job will still run.

A recommended sequence of commands to prevent the background dexopt job from running is:

setprop pm.dexopt.disable_bg_dexopt true
pm bg-dexopt-job --disable

The first line prevents the background dexopt job from being scheduled, if it's not scheduled yet. The second line unschedules the background dexopt job, if it's already scheduled, and it cancels the background dexopt job immediately, if it's running.

ART Service APIs

ART Service exposes Java APIs for customization. The APIs are defined in ArtManagerLocal. See the Javadoc in art/libartservice/service/java/com/android/server/art/ArtManagerLocal.java for usages (Android 14 source, unreleased development source).

ArtManagerLocal is a singleton held by LocalManagerRegistry. A helper function com.android.server.pm.DexOptHelper#getArtManagerLocal helps you obtain it.

import static com.android.server.pm.DexOptHelper.getArtManagerLocal;

Most of the APIs require an instance of PackageManagerLocal.FilteredSnapshot, which holds the information of all apps. You can get it by calling PackageManagerLocal#withFilteredSnapshot, where PackageManagerLocal is also a singleton held by LocalManagerRegistry and can be obtained from com.android.server.pm.PackageManagerServiceUtils#getPackageManagerLocal.

import static com.android.server.pm.PackageManagerServiceUtils.getPackageManagerLocal;

The following are some typical use cases of the APIs.

Trigger dexopt for an app

You can trigger dexopt for any app at any time by calling ArtManagerLocal#dexoptPackage.

try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
  getArtManagerLocal().dexoptPackage(
      snapshot,
      "com.google.android.calculator",
      new DexoptParams.Builder(ReasonMapping.REASON_INSTALL).build());
}

You can also pass your own dexopt reason. If you do that, the priority class and the compiler filter must be explicitly set.

try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
  getArtManagerLocal().dexoptPackage(
      snapshot,
      "com.google.android.calculator",
      new DexoptParams.Builder("my-reason")
          .setCompilerFilter("speed-profile")
          .setPriorityClass(ArtFlags.PRIORITY_BACKGROUND)
          .build());
}

Cancel dexopt

If an operation is initiated by a dexoptPackage call, you can pass a cancellation signal, which lets you cancel the operation at some point. This can be useful when you run dexopt asynchronously.

Executor executor = ...;  // Your asynchronous executor here.
var cancellationSignal = new CancellationSignal();
executor.execute(() -> {
  try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
    getArtManagerLocal().dexoptPackage(
        snapshot,
        "com.google.android.calculator",
        new DexoptParams.Builder(ReasonMapping.REASON_INSTALL).build(),
        cancellationSignal);
  }
});

// When you want to cancel the operation.
cancellationSignal.cancel();

You can also cancel background dexopt, which is initiated by ART Service.

getArtManagerLocal().cancelBackgroundDexoptJob();

Get dexopt results

If an operation is initiated by a dexoptPackage call, you can get the result from the return value.

DexoptResult result;
try (var snapshot = getPackageManagerLocal().withFilteredSnapshot()) {
  result = getArtManagerLocal().dexoptPackage(...);
}

// Process the result here.
...

ART Service also initiates dexopt operations itself in many scenarios, such as background dexopt. To listen to all dexopt results, whether the operation is initiated by a dexoptPackage call or by ART Service, use ArtManagerLocal#addDexoptDoneCallback.

getArtManagerLocal().addDexoptDoneCallback(
    false /* onlyIncludeUpdates */,
    Runnable::run,
    (result) -> {
      // Process the result here.
      ...
    });

The first argument determines whether to only include updates in the result. If you only want to listen to packages that are updated by dexopt, set it to true.

The second argument is the executor of the callback. To execute the callback on the same thread that performs dexopt, use Runnable::run. If you don't want the callback to block dexopt, use an asynchronous executor.

You can add multiple callbacks, and ART Service will execute all of them sequentially. All the callbacks will remain active for all future calls unless you remove them.

If you want to remove a callback, keep the reference of the callback when you add it, and use ArtManagerLocal#removeDexoptDoneCallback.

DexoptDoneCallback callback = (result) -> {
  // Process the result here.
  ...
};

getArtManagerLocal().addDexoptDoneCallback(
    false /* onlyIncludeUpdates */, Runnable::run, callback);

// When you want to remove it.
getArtManagerLocal().removeDexoptDoneCallback(callback);

Customize the package list and dexopt parameters

ART Service initiates dexopt operations itself during boot and background dexopt. To customize the package list or dexopt parameters for those operations, use ArtManagerLocal#setBatchDexoptStartCallback.

getArtManagerLocal().setBatchDexoptStartCallback(
    Runnable::run,
    (snapshot, reason, defaultPackages, builder, cancellationSignal) -> {
      switch (reason) {
        case ReasonMapping.REASON_BG_DEXOPT:
          var myPackages = new ArrayList<String>(defaultPackages);
          myPackages.add(...);
          myPackages.remove(...);
          myPackages.sort(...);
          builder.setPackages(myPackages);
          break;
        default:
          // Ignore unknown reasons.
      }
    });

You can add items to the package list, remove items from it, sort it, or even use a completely different list.

Your callback must ignore unknown reasons because more reasons may be added in the future.

You can set at most one BatchDexoptStartCallback. The callback will remain active for all future calls unless you clear it.

If you want to clear the callback, use ArtManagerLocal#clearBatchDexoptStartCallback.

getArtManagerLocal().clearBatchDexoptStartCallback();

Customize the parameters of the background dexopt job

By default, the background dexopt job runs once a day when the device is idle and charging. This can be changed using ArtManagerLocal#setScheduleBackgroundDexoptJobCallback.

getArtManagerLocal().setScheduleBackgroundDexoptJobCallback(
    Runnable::run,
    builder -> {
      builder.setPeriodic(TimeUnit.DAYS.toMillis(2));
    });

You can set at most one ScheduleBackgroundDexoptJobCallback. The callback will remain active for all future calls unless you clear it.

If you want to clear the callback, use ArtManagerLocal#clearScheduleBackgroundDexoptJobCallback.

getArtManagerLocal().clearScheduleBackgroundDexoptJobCallback();

Temporarily disable dexopt

Any dexopt operation that is initiated by ART Service triggers a BatchDexoptStartCallback. You can keep cancelling the operations to effectively disable dexopt.

If the operation that you cancel is background dexopt, it follows the default retry policy (30 seconds, exponential, capped at 5 hours).

// Good example.

var shouldDisableDexopt = new AtomicBoolean(false);

getArtManagerLocal().setBatchDexoptStartCallback(
    Runnable::run,
    (snapshot, reason, defaultPackages, builder, cancellationSignal) -> {
      if (shouldDisableDexopt.get()) {
        cancellationSignal.cancel();
      }
    });

// Disable dexopt.
shouldDisableDexopt.set(true);
getArtManagerLocal().cancelBackgroundDexoptJob();

// Re-enable dexopt.
shouldDisableDexopt.set(false);

You can have at most one BatchDexoptStartCallback. If you also want to use BatchDexoptStartCallback to customize the package list or dexopt parameters, you must combine the code into one callback.

// Bad example.

// Disable dexopt.
getArtManagerLocal().unscheduleBackgroundDexoptJob();

// Re-enable dexopt.
getArtManagerLocal().scheduleBackgroundDexoptJob();

The dexopt operation performed on app install is not initiated by ART Service. Instead, it is initiated by the package manager through a dexoptPackage call. Therefore, it does not trigger BatchDexoptStartCallback. To disable dexopt on app install, prevent the package manager from calling dexoptPackage.

Override the compiler filter for certain packages (Android 15+)

You can override the compiler filter for certain packages by registering a callback through setAdjustCompilerFilterCallback. The callback is called whenever a package is going to be dexopted, no matter the dexopt is initiated by ART Service during boot and background dexopt or by a dexoptPackage API call.

If a package doesn't need adjustment, the callback must return originalCompilerFilter.

getArtManagerLocal().setAdjustCompilerFilterCallback(
    Runnable::run,
    (packageName, originalCompilerFilter, reason) -> {
      if (isVeryImportantPackage(packageName)) {
        return "speed-profile";
      }
      return originalCompilerFilter;
    });

You can set only one AdjustCompilerFilterCallback. If you want to use AdjustCompilerFilterCallback to override the compiler filter for multiple packages, you must combine the code into one callback. The callback remains active for all future calls unless you clear it.

If you want to clear the callback, use ArtManagerLocal#clearAdjustCompilerFilterCallback.

getArtManagerLocal().clearAdjustCompilerFilterCallback();

Other customizations

ART Service also supports some other customizations.

Set the thermal threshold for background dexopt

The thermal control of the background dexopt job is performed by Job Scheduler. The job is cancelled immediately when the temperature reaches THERMAL_STATUS_MODERATE. The threshold of THERMAL_STATUS_MODERATE is tunable.

Determine whether background dexopt is running

The background dexopt job is managed by Job Scheduler, and its job ID is 27873780. To determine whether the job is running, use Job Scheduler APIs.

// Good example.

var jobScheduler =
    Objects.requireNonNull(mContext.getSystemService(JobScheduler.class));
int reason = jobScheduler.getPendingJobReason(27873780);

if (reason == PENDING_JOB_REASON_EXECUTING) {
  // Do something when the job is running.
  ...
}
// Bad example.

var backgroundDexoptRunning = new AtomicBoolean(false);

getArtManagerLocal().setBatchDexoptStartCallback(
    Runnable::run,
    (snapshot, reason, defaultPackages, builder, cancellationSignal) -> {
      if (reason.equals(ReasonMapping.REASON_BG_DEXOPT)) {
        backgroundDexoptRunning.set(true);
      }
    });

getArtManagerLocal().addDexoptDoneCallback(
    false /* onlyIncludeUpdates */,
    Runnable::run,
    (result) -> {
      if (result.getReason().equals(ReasonMapping.REASON_BG_DEXOPT)) {
        backgroundDexoptRunning.set(false);
      }
    });

if (backgroundDexoptRunning.get()) {
  // Do something when the job is running.
  ...
}

Provide a profile for dexopt

To use a profile to guide dexopt, put a .prof file or a .dm file next to the APK.

The .prof file must be a binary-format profile file, and the filename must be the filename of the APK + .prof. For example,

base.apk.prof

The filename of the .dm file must be the filename of the APK with the extension replaced by .dm. For example,

base.dm

To verify that the profile is being used for dexopt, run dexopt with speed-profile and check the result.

pm art clear-app-profiles <package-name>
pm compile -m speed-profile -f -v <package-name>

The first line clears all profiles produced by the runtime (i.e., those in /data/misc/profiles), if any, to make sure that the profile next to the APK is the only profile that ART Service can possibly use. The second line runs dexopt with speed-profile, and it passes -v to print the verbose result.

If the profile is being used, you see actualCompilerFilter=speed-profile in the result. Otherwise, you see actualCompilerFilter=verify. For example,

DexContainerFileDexoptResult{dexContainerFile=/data/app/~~QR0fTV0UbDbIP1Su7XzyPg==/com.google.android.gms-LvusF2uARKOtBbcaPHdUtQ==/base.apk, primaryAbi=true, abi=x86_64, actualCompilerFilter=speed-profile, status=PERFORMED, dex2oatWallTimeMillis=4549, dex2oatCpuTimeMillis=14550, sizeBytes=3715344, sizeBeforeBytes=3715344}

Typical reasons why ART Service doesn't use the profile include the following:

  • The profile has a wrong filename or it's not next to the APK.
  • The profile is in the wrong format.
  • The profile does not match the APK. (The checksums in the profile don't match the checksums of the .dex files in the APK.)