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, whilepm.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.)