ART services: optimize package - add APIs.
This change adds a method `ArtManagerLocal.optimizePackage` and related
data structures including `OptimizeOptions` and `OptimizeResult`.
Bug: 229268202
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: I09a6cece11b969dcbc922de4e1043fe608fefb59
diff --git a/artd/binder/com/android/server/art/PriorityClass.aidl b/artd/binder/com/android/server/art/PriorityClass.aidl
new file mode 100644
index 0000000..38adfc7
--- /dev/null
+++ b/artd/binder/com/android/server/art/PriorityClass.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * Indicates the priority of an operation. The value affects the resource usage and the process
+ * priority. A higher value may result in faster execution but may consume more resources and
+ * compete for resources with other processes.
+ *
+ * @hide
+ */
+enum PriorityClass {
+ /** Indicates that the operation blocks boot. */
+ BOOT = 100,
+ /**
+ * Indicates that a human is waiting on the result and the operation is more latency sensitive
+ * than usual.
+ */
+ INTERACTIVE_FAST = 80,
+ /** Indicates that a human is waiting on the result. */
+ INTERACTIVE = 60,
+ /** Indicates that the operation runs in background. */
+ BACKGROUND = 40,
+}
diff --git a/libartservice/service/Android.bp b/libartservice/service/Android.bp
index 7bd4c34..49da309 100644
--- a/libartservice/service/Android.bp
+++ b/libartservice/service/Android.bp
@@ -130,7 +130,8 @@
"androidx.test.ext.truth",
"androidx.test.runner",
"artd-aidl-java",
- "mockito-target-minus-junit4",
+ // We need ExtendedMockito to mock static methods.
+ "mockito-target-extended-minus-junit4",
"service-art.impl",
// Statically link against system server to allow us to mock system
// server APIs. This won't work on master-art, but it's fine because we
@@ -138,6 +139,12 @@
"services.core",
],
+ jni_libs: [
+ // The two libraries below are required by ExtendedMockito.
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+
min_sdk_version: "31",
test_suites: ["general-tests"],
diff --git a/libartservice/service/AndroidManifest.xml b/libartservice/service/AndroidManifest.xml
index 921bde9..1c13fc6 100644
--- a/libartservice/service/AndroidManifest.xml
+++ b/libartservice/service/AndroidManifest.xml
@@ -20,7 +20,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.art.tests">
- <application android:label="ArtServiceTests">
+ <!-- android:debuggable is required by ExtendedMockito. -->
+ <application android:label="ArtServiceTests" android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
diff --git a/libartservice/service/AndroidTest.xml b/libartservice/service/AndroidTest.xml
new file mode 100644
index 0000000..7a47ca3
--- /dev/null
+++ b/libartservice/service/AndroidTest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Config for ART Services test cases">
+ <option name="test-suite-tag" value="apct" />
+
+ <!-- This test needs access to system APIs for mainline modules. -->
+ <option name="hidden-api-checks" value="false"/>
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="ArtServiceTests.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+ <option name="package" value="com.android.server.art.tests"/>
+ </test>
+
+ <!-- Only run tests if the device under test is SDK version 31 (Android 12) or above. -->
+ <!-- TODO(jiakaiz): Change this to U once `ro.build.version.sdk` is bumped. -->
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" />
+</configuration>
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index e7e011d..9b2e9f8 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -23,11 +23,12 @@
import static com.android.server.art.model.OptimizationStatus.DexFileOptimizationStatus;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.content.Context;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.util.Log;
@@ -36,6 +37,8 @@
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.DeleteResult;
import com.android.server.art.model.OptimizationStatus;
+import com.android.server.art.model.OptimizeOptions;
+import com.android.server.art.model.OptimizeResult;
import com.android.server.art.wrapper.AndroidPackageApi;
import com.android.server.art.wrapper.PackageManagerLocal;
import com.android.server.art.wrapper.PackageState;
@@ -61,13 +64,20 @@
@NonNull private final Injector mInjector;
+ // TODO(b/236954191): Deprecate this.
public ArtManagerLocal() {
this(new Injector());
}
+ // TODO(b/236954191): Expose this.
+ /** @hide */
+ public ArtManagerLocal(@NonNull Context context) {
+ this(new Injector(context));
+ }
+
/** @hide */
@VisibleForTesting
- public ArtManagerLocal(Injector injector) {
+ public ArtManagerLocal(@NonNull Injector injector) {
mInjector = injector;
}
@@ -215,6 +225,20 @@
}
}
+ /** @hide */
+ @NonNull
+ public OptimizeResult optimizePackage(@NonNull PackageDataSnapshot snapshot,
+ @NonNull String packageName, @NonNull OptimizeOptions options) {
+ if (!options.isForPrimaryDex() && !options.isForSecondaryDex()) {
+ throw new IllegalArgumentException("Nothing to optimize");
+ }
+
+ PackageState pkgState = getPackageStateOrThrow(snapshot, packageName);
+ AndroidPackageApi pkg = getPackageOrThrow(pkgState);
+
+ return mInjector.getDexOptHelper().dexopt(snapshot, pkgState, pkg, options);
+ }
+
private PackageState getPackageStateOrThrow(
@NonNull PackageDataSnapshot snapshot, @NonNull String packageName) {
PackageState pkgState = mInjector.getPackageManagerLocal().getPackageState(
@@ -240,9 +264,16 @@
*/
@VisibleForTesting
public static class Injector {
- private final PackageManagerLocal mPackageManagerLocal;
+ @Nullable private final Context mContext;
+ @Nullable private final PackageManagerLocal mPackageManagerLocal;
Injector() {
+ this(null /* context */);
+ }
+
+ Injector(@Nullable Context context) {
+ mContext = context;
+
PackageManagerLocal packageManagerLocal = null;
try {
packageManagerLocal = PackageManagerLocal.getInstance();
@@ -257,16 +288,28 @@
mPackageManagerLocal = packageManagerLocal;
}
+ // TODO(b/236954191): Make this @NonNull.
+ @Nullable
+ public Context getContext() {
+ return mContext;
+ }
+
+ @NonNull
public PackageManagerLocal getPackageManagerLocal() {
+ if (mPackageManagerLocal == null) {
+ throw new IllegalStateException("PackageManagerLocal is null");
+ }
return mPackageManagerLocal;
}
+ @NonNull
public IArtd getArtd() {
- IArtd artd = IArtd.Stub.asInterface(ServiceManager.waitForService("artd"));
- if (artd == null) {
- throw new IllegalStateException("Unable to connect to artd");
- }
- return artd;
+ return Utils.getArtd();
+ }
+
+ @NonNull
+ public DexOptHelper getDexOptHelper() {
+ return new DexOptHelper(getContext());
}
}
}
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 9a49aae..1146aa0 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -25,6 +25,8 @@
import com.android.server.art.model.ArtFlags;
import com.android.server.art.model.DeleteResult;
import com.android.server.art.model.OptimizationStatus;
+import com.android.server.art.model.OptimizeOptions;
+import com.android.server.art.model.OptimizeResult;
import com.android.server.art.wrapper.PackageManagerLocal;
import com.android.server.pm.snapshot.PackageDataSnapshot;
@@ -53,12 +55,13 @@
PrintWriter pw = getOutPrintWriter();
PackageDataSnapshot snapshot = mPackageManagerLocal.snapshot();
switch (cmd) {
- case "delete-optimized-artifacts":
+ case "delete-optimized-artifacts": {
DeleteResult result = mArtManagerLocal.deleteOptimizedArtifacts(
snapshot, getNextArgRequired(), ArtFlags.defaultDeleteFlags());
pw.printf("Freed %d bytes\n", result.getFreedBytes());
return 0;
- case "get-optimization-status":
+ }
+ case "get-optimization-status": {
OptimizationStatus optimizationStatus = mArtManagerLocal.getOptimizationStatus(
snapshot, getNextArgRequired(), ArtFlags.defaultGetStatusFlags());
for (DexFileOptimizationStatus status :
@@ -70,6 +73,41 @@
status.getLocationDebugString());
}
return 0;
+ }
+ case "optimize-package": {
+ var optionsBuilder = new OptimizeOptions.Builder("cmdline");
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "-m":
+ optionsBuilder.setCompilerFilter(getNextArgRequired());
+ break;
+ case "-f":
+ optionsBuilder.setForce(true);
+ break;
+ default:
+ pw.println("Error: Unknown option: " + opt);
+ return 1;
+ }
+ }
+ OptimizeResult result = mArtManagerLocal.optimizePackage(
+ snapshot, getNextArgRequired(), optionsBuilder.build());
+ switch (result.getFinalStatus()) {
+ case OptimizeResult.OPTIMIZE_SKIPPED:
+ pw.println("SKIPPED");
+ break;
+ case OptimizeResult.OPTIMIZE_PERFORMED:
+ pw.println("PERFORMED");
+ break;
+ case OptimizeResult.OPTIMIZE_FAILED:
+ pw.println("FAILED");
+ break;
+ case OptimizeResult.OPTIMIZE_CANCELLED:
+ pw.println("CANCELLED");
+ break;
+ }
+ return 0;
+ }
default:
// Handles empty, help, and invalid commands.
return handleDefaultCommands(cmd);
@@ -89,14 +127,20 @@
pw.println(" help or -h");
pw.println(" Print this help text.");
// TODO(jiakaiz): Also do operations for secondary dex'es by default.
- pw.println(" delete-optimized-artifacts <package-name>");
+ pw.println(" delete-optimized-artifacts PACKAGE_NAME");
pw.println(" Delete the optimized artifacts of a package.");
pw.println(" By default, the command only deletes the optimized artifacts of primary "
+ "dex'es.");
- pw.println(" get-optimization-status <package-name>");
+ pw.println(" get-optimization-status PACKAGE_NAME");
pw.println(" Print the optimization status of a package.");
pw.println(" By default, the command only prints the optimization status of primary "
+ "dex'es.");
+ pw.println(" optimize-package [-m COMPILER_FILTER] [-f] PACKAGE_NAME");
+ pw.println(" Optimize a package.");
+ pw.println(" By default, the command only optimizes primary dex'es.");
+ pw.println(" Options:");
+ pw.println(" -m Set the compiler filter.");
+ pw.println(" -f Force compilation.");
}
private void enforceRoot() {
diff --git a/libartservice/service/java/com/android/server/art/DexOptHelper.java b/libartservice/service/java/com/android/server/art/DexOptHelper.java
new file mode 100644
index 0000000..38dec8c
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/DexOptHelper.java
@@ -0,0 +1,82 @@
+/*
+ * 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.OptimizeResult.DexFileOptimizeResult;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.art.model.OptimizeOptions;
+import com.android.server.art.model.OptimizeResult;
+import com.android.server.art.wrapper.AndroidPackageApi;
+import com.android.server.art.wrapper.PackageState;
+import com.android.server.pm.snapshot.PackageDataSnapshot;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A helper class to handle dexopt.
+ *
+ * It talks to other components (e.g., PowerManager) and dispatches tasks to dex optimizers.
+ *
+ * @hide
+ */
+public class DexOptHelper {
+ private static final String TAG = "DexoptHelper";
+
+ @NonNull private final Injector mInjector;
+
+ public DexOptHelper(@Nullable Context context) {
+ this(new Injector(context));
+ }
+
+ @VisibleForTesting
+ public DexOptHelper(@NonNull Injector injector) {
+ mInjector = injector;
+ }
+
+ /**
+ * DO NOT use this method directly. Use {@link
+ * ArtManagerLocal#optimizePackage(PackageDataSnapshot, String, OptimizeOptions)}.
+ */
+ @NonNull
+ public OptimizeResult dexopt(@NonNull PackageDataSnapshot snapshot,
+ @NonNull PackageState pkgState, @NonNull AndroidPackageApi pkg,
+ @NonNull OptimizeOptions options) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Injector pattern for testing purpose.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class Injector {
+ // TODO(b/236954191): Make this @NonNull.
+ @Nullable private final Context mContext;
+
+ Injector(@Nullable Context context) {
+ mContext = context;
+ }
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/ReasonMapping.java b/libartservice/service/java/com/android/server/art/ReasonMapping.java
new file mode 100644
index 0000000..6e9f56f
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/ReasonMapping.java
@@ -0,0 +1,126 @@
+/*
+ * 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.os.SystemProperties;
+import android.text.TextUtils;
+
+import dalvik.system.DexFile;
+
+/**
+ * Maps a compilation reason to a compiler filter and a priority class.
+ *
+ * @hide
+ */
+public class ReasonMapping {
+ private ReasonMapping() {}
+
+ /** Optimizing apps on the first boot. */
+ public static final String REASON_FIRST_BOOT = "first-boot";
+ /** Optimizing apps on the next boot after an OTA. */
+ public static final String REASON_BOOT_AFTER_OTA = "boot-after-ota";
+ /** Installing an app after user presses the "install"/"update" button. */
+ public static final String REASON_INSTALL = "install";
+ /** Optimizing apps in the background. */
+ public static final String REASON_BG_DEXOPT = "bg-dexopt";
+ /** Invoked by cmdline. */
+ public static final String REASON_CMDLINE = "cmdline";
+ /** Downgrading the compiler filter when an app is not used for a long time. */
+ public static final String REASON_INACTIVE = "inactive";
+
+ // Reasons for Play Install Hints (go/install-hints).
+ public static final String REASON_INSTALL_FAST = "install-fast";
+ public static final String REASON_INSTALL_BULK = "install-bulk";
+ public static final String REASON_INSTALL_BULK_SECONDARY = "install-bulk-secondary";
+ public static final String REASON_INSTALL_BULK_DOWNGRADED = "install-bulk-downgraded";
+ public static final String REASON_INSTALL_BULK_SECONDARY_DOWNGRADED =
+ "install-bulk-secondary-downgraded";
+
+ /**
+ * Loads the compiler filter from the system property for the given reason and checks for
+ * validity.
+ *
+ * @throws IllegalArgumentException if the reason is invalid
+ * @throws IllegalStateException if the system property value is invalid
+ *
+ * @hide
+ */
+ @NonNull
+ public static String getCompilerFilterForReason(@NonNull String reason) {
+ String value = SystemProperties.get("pm.dexopt." + reason);
+ if (TextUtils.isEmpty(value)) {
+ throw new IllegalArgumentException("No compiler filter for reason '" + reason + "'");
+ }
+ if (!Utils.isValidArtServiceCompilerFilter(value)) {
+ throw new IllegalStateException(
+ "Got invalid compiler filter '" + value + "' for reason '" + reason + "'");
+ }
+ return value;
+ }
+
+ /**
+ * Loads the compiler filter from the system property for:
+ * - shared libraries
+ * - apps used by other apps without a dex metadata file
+ *
+ * @throws IllegalStateException if the system property value is invalid
+ *
+ * @hide
+ */
+ @NonNull
+ public static String getCompilerFilterForShared() {
+ // "shared" is technically not a compilation reason, but the compiler filter is defined as a
+ // system property as if "shared" is a reason.
+ String value = getCompilerFilterForReason("shared");
+ if (DexFile.isProfileGuidedCompilerFilter(value)) {
+ throw new IllegalStateException(
+ "Compiler filter for 'shared' must not be profile guided, got '" + value + "'");
+ }
+ return value;
+ }
+
+ /**
+ * Returns the priority for the given reason.
+ *
+ * @throws IllegalArgumentException if the reason is invalid
+ * @see PriorityClass
+ *
+ * @hide
+ */
+ public static @PriorityClass byte getPriorityClassForReason(@NonNull String reason) {
+ switch (reason) {
+ case REASON_FIRST_BOOT:
+ case REASON_BOOT_AFTER_OTA:
+ return PriorityClass.BOOT;
+ case REASON_INSTALL_FAST:
+ return PriorityClass.INTERACTIVE_FAST;
+ case REASON_INSTALL:
+ case REASON_CMDLINE:
+ return PriorityClass.INTERACTIVE;
+ case REASON_BG_DEXOPT:
+ case REASON_INACTIVE:
+ case REASON_INSTALL_BULK:
+ case REASON_INSTALL_BULK_SECONDARY:
+ case REASON_INSTALL_BULK_DOWNGRADED:
+ case REASON_INSTALL_BULK_SECONDARY_DOWNGRADED:
+ return PriorityClass.BACKGROUND;
+ default:
+ throw new IllegalArgumentException("No priority class for reason '" + reason + "'");
+ }
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 9c67a0f..b30d208 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -18,12 +18,15 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.ServiceManager;
import android.util.SparseArray;
import com.android.server.art.ArtifactsPath;
+import com.android.server.art.model.OptimizeOptions;
import com.android.server.art.wrapper.PackageManagerLocal;
import com.android.server.art.wrapper.PackageState;
+import dalvik.system.DexFile;
import dalvik.system.VMRuntime;
import java.util.Collection;
@@ -81,4 +84,21 @@
public static boolean isInDalvikCache(@NonNull PackageState pkg) {
return pkg.isSystem() && !pkg.isUpdatedSystemApp();
}
+
+ /** Returns true if the given string is a valid compiler filter. */
+ public static boolean isValidArtServiceCompilerFilter(@NonNull String compilerFilter) {
+ if (compilerFilter.equals(OptimizeOptions.COMPILER_FILTER_NOOP)) {
+ return true;
+ }
+ return DexFile.isValidCompilerFilter(compilerFilter);
+ }
+
+ @NonNull
+ public static IArtd getArtd() {
+ IArtd artd = IArtd.Stub.asInterface(ServiceManager.waitForService("artd"));
+ if (artd == null) {
+ throw new IllegalStateException("Unable to connect to artd");
+ }
+ return artd;
+ }
}
diff --git a/libartservice/service/java/com/android/server/art/model/OptimizeOptions.java b/libartservice/service/java/com/android/server/art/model/OptimizeOptions.java
new file mode 100644
index 0000000..6e897ec
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/OptimizeOptions.java
@@ -0,0 +1,204 @@
+/*
+ * 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.model;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.text.TextUtils;
+
+import com.android.server.art.PriorityClass;
+import com.android.server.art.ReasonMapping;
+import com.android.server.art.Utils;
+
+/** @hide */
+public class OptimizeOptions {
+ public static final class Builder {
+ private OptimizeOptions mOptions = new OptimizeOptions();
+
+ /**
+ * Creates a builder.
+ *
+ * @param reason See {@link #setReason(String)}.
+ */
+ public Builder(@NonNull String reason) {
+ setReason(reason);
+ }
+
+ /** Whether to generate optimized artifacts for primary dex'es. Default: true. */
+ public Builder setForPrimaryDex(boolean value) {
+ mOptions.mIsForPrimaryDex = value;
+ return this;
+ }
+
+ /** Whether to generate optimized artifacts for secondary dex'es. Default: false. */
+ public Builder setForSecondaryDex(boolean value) {
+ mOptions.mIsForSecondaryDex = value;
+ return this;
+ }
+
+ /** Whether to optimize dependency packages as well. Default: false. */
+ public Builder setIncludesDependencies(boolean value) {
+ mOptions.mIncludesDependencies = value;
+ return this;
+ }
+
+ /**
+ * The target compiler filter. Note that the compiler filter might be adjusted before the
+ * execution based on factors like whether the profile is available or whether the app is
+ * used by other apps. If not set, the default compiler filter for the given reason will be
+ * used.
+ */
+ public Builder setCompilerFilter(@NonNull String value) {
+ mOptions.mCompilerFilter = value;
+ return this;
+ }
+
+ /**
+ * The priority of the operation. If not set, the default priority class for the given
+ * reason will be used.
+ *
+ * @see PriorityClass
+ */
+ public Builder setPriorityClass(@PriorityClass byte value) {
+ mOptions.mPriorityClass = value;
+ return this;
+ }
+
+ /**
+ * Compilation reason. Can be a string defined in {@link ReasonMapping} or a custom string.
+ *
+ * If the value is a string defined in {@link ReasonMapping}, it determines the compiler
+ * filter and/or the priority class, if those values are not explicitly set.
+ *
+ * If the value is a custom string, the priority class and the compiler filter must be
+ * explicitly set.
+ */
+ public Builder setReason(@NonNull String value) {
+ mOptions.mReason = value;
+ return this;
+ }
+
+ /**
+ * Whether the intention is to downgrade the compiler filter. If true, the compilation will
+ * be skipped if the target compiler filter is better than or equal to the compiler filter
+ * of the existing optimized artifacts, or optimized artifacts do not exist.
+ */
+ public Builder setShouldDowngrade(boolean value) {
+ mOptions.mShouldDowngrade = value;
+ return this;
+ }
+
+ /**
+ * Whether to force compilation. If true, the compilation will be performed regardless of
+ * any existing optimized artifacts.
+ */
+ public Builder setForce(boolean value) {
+ mOptions.mForce = value;
+ return this;
+ }
+
+ /**
+ * Returns the built object.
+ *
+ * @throws IllegalArgumentException if the built options would be invalid
+ */
+ public OptimizeOptions build() {
+ if (mOptions.mReason.isEmpty()) {
+ throw new IllegalArgumentException("Reason must not be empty");
+ }
+
+ if (mOptions.mCompilerFilter.isEmpty()) {
+ mOptions.mCompilerFilter =
+ ReasonMapping.getCompilerFilterForReason(mOptions.mReason);
+ } else if (!Utils.isValidArtServiceCompilerFilter(mOptions.mCompilerFilter)) {
+ throw new IllegalArgumentException(
+ "Invalid compiler filter '" + mOptions.mCompilerFilter + "'");
+ }
+
+ if (mOptions.mPriorityClass == -1) {
+ mOptions.mPriorityClass = ReasonMapping.getPriorityClassForReason(mOptions.mReason);
+ } else if (mOptions.mPriorityClass < 0 || mOptions.mPriorityClass > 100) {
+ throw new IllegalArgumentException("Invalid priority class "
+ + mOptions.mPriorityClass + ". Must be between 0 and 100");
+ }
+
+ return mOptions;
+ }
+ }
+
+ /**
+ * A value indicating that dexopt shouldn't be run. This value is consumed by ART Services and
+ * is not propagated to dex2oat.
+ */
+ public static final String COMPILER_FILTER_NOOP = "skip";
+
+ private boolean mIsForPrimaryDex = true;
+ private boolean mIsForSecondaryDex = false;
+ private boolean mIncludesDependencies = false;
+ private @NonNull String mCompilerFilter = "";
+ private @PriorityClass byte mPriorityClass = -1;
+ private @NonNull String mReason = "";
+ private boolean mShouldDowngrade = false;
+ private boolean mForce = false;
+
+ private OptimizeOptions() {}
+
+ /** Whether to generate optimized artifacts for primary dex'es. */
+ public boolean isForPrimaryDex() {
+ return mIsForPrimaryDex;
+ }
+
+ /** Whether to generate optimized artifacts for secondary dex'es. */
+ public boolean isForSecondaryDex() {
+ return mIsForSecondaryDex;
+ }
+
+ /** Whether to optimize dependency packages as well. */
+ public boolean getIncludesDependencies() {
+ return mIncludesDependencies;
+ }
+
+ /** The target compiler filter. */
+ public @NonNull String getCompilerFilter() {
+ return mCompilerFilter;
+ }
+
+ /** The priority class. */
+ public @PriorityClass byte getPriorityClass() {
+ return mPriorityClass;
+ }
+
+ /**
+ * The compilation reason.
+ *
+ * DO NOT directly use the string value to determine the resource usage and the process
+ * priority. Use {@link #getPriorityClass}.
+ */
+ public @NonNull String getReason() {
+ return mReason;
+ }
+
+ /** Whether the intention is to downgrade the compiler filter. */
+ public boolean getShouldDowngrade() {
+ return mShouldDowngrade;
+ }
+
+ /** Whether to force compilation. */
+ public boolean getForce() {
+ return mForce;
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/model/OptimizeResult.java b/libartservice/service/java/com/android/server/art/model/OptimizeResult.java
new file mode 100644
index 0000000..efebbf2
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/model/OptimizeResult.java
@@ -0,0 +1,127 @@
+/*
+ * 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.model;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.Immutable;
+
+import java.util.List;
+
+/** @hide */
+@Immutable
+public class OptimizeResult {
+ // Possible values of {@link #OptimizeStatus}.
+ // A larger number means a higher priority. If multiple dex files are processed, the final
+ // status will be the one with the highest priority.
+ public static final int OPTIMIZE_SKIPPED = 10;
+ public static final int OPTIMIZE_PERFORMED = 20;
+ public static final int OPTIMIZE_FAILED = 30;
+ public static final int OPTIMIZE_CANCELLED = 40;
+
+ /** @hide */
+ @IntDef(prefix = {"OPTIMIZE_"},
+ value = {OPTIMIZE_SKIPPED, OPTIMIZE_FAILED, OPTIMIZE_PERFORMED, OPTIMIZE_CANCELLED})
+ public @interface OptimizeStatus {}
+
+ private final @NonNull String mPackageName;
+ private final @NonNull String mRequestedCompilerFilter;
+ private final @NonNull String mReason;
+ private final @NonNull List<DexFileOptimizeResult> mDexFileOptimizeResults;
+
+ public OptimizeResult(@NonNull String packageName, @NonNull String requestedCompilerFilter,
+ @NonNull String reason, @NonNull List<DexFileOptimizeResult> dexFileOptimizeResults) {
+ mPackageName = packageName;
+ mRequestedCompilerFilter = requestedCompilerFilter;
+ mReason = reason;
+ mDexFileOptimizeResults = dexFileOptimizeResults;
+ }
+
+ /** The package name. */
+ public @NonNull String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * The requested compiler filter. Note that the compiler filter might be adjusted before the
+ * execution based on factors like whether the profile is available or whether the app is
+ * used by other apps.
+ *
+ * @see DexFileOptimizeResult#getActualCompilerFilter.
+ */
+ public @NonNull String getRequestedCompilerFilter() {
+ return mRequestedCompilerFilter;
+ }
+
+ /** The compilation reason. */
+ public @NonNull String getReason() {
+ return mReason;
+ }
+
+ /** The final status. */
+ public @OptimizeStatus int getFinalStatus() {
+ return mDexFileOptimizeResults.stream()
+ .mapToInt(result -> result.getStatus())
+ .max()
+ .orElse(OPTIMIZE_SKIPPED);
+ }
+
+ /** The result of each individual dex file. */
+ @NonNull
+ public List<DexFileOptimizeResult> getDexFileOptimizeResults() {
+ return mDexFileOptimizeResults;
+ }
+
+ /** Describes the result of a dex file. */
+ @Immutable
+ public static class DexFileOptimizeResult {
+ private final @NonNull String mDexFile;
+ private final @NonNull String mInstructionSet;
+ private final @NonNull String mActualCompilerFilter;
+ private final @OptimizeStatus int mStatus;
+
+ /** @hide */
+ public DexFileOptimizeResult(@NonNull String dexFile, @NonNull String instructionSet,
+ @NonNull String compilerFilter, @OptimizeStatus int status) {
+ mDexFile = dexFile;
+ mInstructionSet = instructionSet;
+ mActualCompilerFilter = compilerFilter;
+ mStatus = status;
+ }
+
+ /** The absolute path to the dex file. */
+ public @NonNull String getDexFile() {
+ return mDexFile;
+ }
+
+ /** The instruction set. */
+ public @NonNull String getInstructionSet() {
+ return mInstructionSet;
+ }
+
+ /** The actual compiler filter. */
+ public @NonNull String getActualCompilerFilter() {
+ return mActualCompilerFilter;
+ }
+
+ /** The status of optimizing this dex file. */
+ public @OptimizeStatus int getStatus() {
+ return mStatus;
+ }
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index 78843e3..afaa3ee 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -26,6 +26,7 @@
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
@@ -37,6 +38,8 @@
import com.android.server.art.model.DeleteResult;
import com.android.server.art.model.OptimizationStatus;
+import com.android.server.art.model.OptimizeOptions;
+import com.android.server.art.model.OptimizeResult;
import com.android.server.art.wrapper.AndroidPackageApi;
import com.android.server.art.wrapper.PackageManagerLocal;
import com.android.server.art.wrapper.PackageState;
@@ -66,7 +69,9 @@
@Mock private ArtManagerLocal.Injector mInjector;
@Mock private PackageManagerLocal mPackageManagerLocal;
@Mock private IArtd mArtd;
+ @Mock private DexOptHelper mDexOptHelper;
private PackageState mPkgState;
+ private AndroidPackageApi mPkg;
// True if the primary dex'es are in a readonly partition.
@Parameter(0) public boolean mIsInReadonlyPartition;
@@ -80,10 +85,15 @@
@Before
public void setUp() throws Exception {
+ // Use `lenient()` to suppress `UnnecessaryStubbingException` thrown by the strict stubs.
+ // These are the default test setups. They may or may not be used depending on the code path
+ // that each test case examines.
lenient().when(mInjector.getPackageManagerLocal()).thenReturn(mPackageManagerLocal);
lenient().when(mInjector.getArtd()).thenReturn(mArtd);
+ lenient().when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper);
mPkgState = createPackageState();
+ mPkg = mPkgState.getAndroidPackage();
lenient()
.when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME)))
.thenReturn(mPkgState);
@@ -207,6 +217,35 @@
}
}
+ @Test
+ public void testOptimizePackage() throws Exception {
+ var options = new OptimizeOptions.Builder("install").build();
+ var result = mock(OptimizeResult.class);
+
+ when(mDexOptHelper.dexopt(any(), same(mPkgState), same(mPkg), same(options)))
+ .thenReturn(result);
+
+ assertThat(mArtManagerLocal.optimizePackage(
+ mock(PackageDataSnapshot.class), PKG_NAME, options))
+ .isSameInstanceAs(result);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testOptimizePackagePackageNotFound() throws Exception {
+ when(mPackageManagerLocal.getPackageState(any(), anyInt(), eq(PKG_NAME))).thenReturn(null);
+
+ mArtManagerLocal.optimizePackage(mock(PackageDataSnapshot.class), PKG_NAME,
+ new OptimizeOptions.Builder("install").build());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testOptimizePackageNoPackage() throws Exception {
+ when(mPkgState.getAndroidPackage()).thenReturn(null);
+
+ mArtManagerLocal.optimizePackage(mock(PackageDataSnapshot.class), PKG_NAME,
+ new OptimizeOptions.Builder("install").build());
+ }
+
private AndroidPackageApi createPackage() {
AndroidPackageApi pkg = mock(AndroidPackageApi.class);
diff --git a/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java b/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java
new file mode 100644
index 0000000..776cb0f
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/ReasonMappingTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.os.SystemProperties;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.art.testing.StaticMockitoRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ReasonMappingTest {
+ @Rule public StaticMockitoRule mockitoRule = new StaticMockitoRule(SystemProperties.class);
+
+ @Test
+ public void testGetCompilerFilterForReason() {
+ when(SystemProperties.get("pm.dexopt.foo")).thenReturn("speed");
+ assertThat(ReasonMapping.getCompilerFilterForReason("foo")).isEqualTo("speed");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testGetCompilerFilterForReasonInvalidFilter() throws Exception {
+ when(SystemProperties.get("pm.dexopt.foo")).thenReturn("invalid-filter");
+ ReasonMapping.getCompilerFilterForReason("foo");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetCompilerFilterForReasonInvalidReason() throws Exception {
+ ReasonMapping.getCompilerFilterForReason("foo");
+ }
+
+ @Test
+ public void testGetCompilerFilterForShared() {
+ when(SystemProperties.get("pm.dexopt.shared")).thenReturn("speed");
+ assertThat(ReasonMapping.getCompilerFilterForShared()).isEqualTo("speed");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testGetCompilerFilterForSharedProfileGuidedFilter() throws Exception {
+ when(SystemProperties.get("pm.dexopt.shared")).thenReturn("speed-profile");
+ ReasonMapping.getCompilerFilterForShared();
+ }
+
+ @Test
+ public void testGetPriorityClassForReason() throws Exception {
+ assertThat(ReasonMapping.getPriorityClassForReason("install"))
+ .isEqualTo(PriorityClass.INTERACTIVE);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetPriorityClassForReasonInvalidReason() throws Exception {
+ ReasonMapping.getPriorityClassForReason("foo");
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/model/OptimizeOptionsTest.java b/libartservice/service/javatests/com/android/server/art/model/OptimizeOptionsTest.java
new file mode 100644
index 0000000..47515c7
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/model/OptimizeOptionsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.art.PriorityClass;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class OptimizeOptionsTest {
+ @Test
+ public void testBuild() {
+ new OptimizeOptions.Builder("install").build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBuildEmptyReason() {
+ new OptimizeOptions.Builder("").build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBuildInvalidCompilerFilter() {
+ new OptimizeOptions.Builder("install").setCompilerFilter("invalid").build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBuildInvalidPriorityClass() {
+ new OptimizeOptions.Builder("install").setPriorityClass((byte) 101).build();
+ }
+
+ @Test
+ public void testBuildCustomReason() {
+ new OptimizeOptions.Builder("custom")
+ .setCompilerFilter("speed")
+ .setPriorityClass((byte) 90)
+ .build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBuildCustomReasonEmptyCompilerFilter() {
+ new OptimizeOptions.Builder("custom").setPriorityClass(PriorityClass.INTERACTIVE).build();
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testBuildCustomReasonEmptyPriorityClass() {
+ new OptimizeOptions.Builder("custom").setCompilerFilter("speed").build();
+ }
+}
diff --git a/libartservice/service/javatests/com/android/server/art/testing/StaticMockitoRule.java b/libartservice/service/javatests/com/android/server/art/testing/StaticMockitoRule.java
new file mode 100644
index 0000000..f6e1107
--- /dev/null
+++ b/libartservice/service/javatests/com/android/server/art/testing/StaticMockitoRule.java
@@ -0,0 +1,64 @@
+/*
+ * 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.testing;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
+
+import org.junit.rules.MethodRule;
+import org.junit.runners.model.FrameworkMethod;
+import org.junit.runners.model.Statement;
+import org.mockito.quality.Strictness;
+
+/**
+ * Similar to {@link MockitoRule}, but uses {@StaticMockitoSession}, which allows mocking static
+ * methods.
+ */
+public class StaticMockitoRule implements MethodRule {
+ private Class<?>[] mClasses;
+
+ public StaticMockitoRule(Class<?>... classes) {
+ mClasses = classes;
+ }
+
+ @Override
+ public Statement apply(Statement base, FrameworkMethod method, Object target) {
+ return new Statement() {
+ public void evaluate() throws Throwable {
+ StaticMockitoSessionBuilder builder =
+ mockitoSession()
+ .name(target.getClass().getSimpleName() + "." + method.getName())
+ .initMocks(target)
+ .strictness(Strictness.STRICT_STUBS);
+
+ for (Class<?> clazz : mClasses) {
+ builder.mockStatic(clazz);
+ }
+
+ StaticMockitoSession session = builder.startMocking();
+
+ try {
+ base.evaluate();
+ } finally {
+ session.finishMocking();
+ }
+ }
+ };
+ }
+}