Implement shell command "dump-profiles".
This change does not add any new API because the functionality is only
used by the shell command.
This change also changes the behavior of "snapshot-app-profile" and
"snapshot-boot-image-profile" to pave the way to eventually take over
the legacy PM shell commands.
Bug: 261564086
Test: `adb shell pm art dump-profiles` with a package that has multiple
splits.
Test: `adb shell pm art snapshot-app-profile` with a package that has
multiple splits.
Ignore-AOSP-First: ART Services.
Change-Id: I67435d0ba63a655e58fe1186bddea3b16e2fa23a
diff --git a/artd/artd.cc b/artd/artd.cc
index 73208a6..2e55537 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -588,6 +588,9 @@
for (const std::string& dex_file : in_dexFiles) {
OR_RETURN_FATAL(ValidateDexPath(dex_file));
}
+ if (in_options.forceMerge + in_options.dumpOnly + in_options.dumpClassesAndMethods > 1) {
+ return Fatal("Only one of 'forceMerge', 'dumpOnly', and 'dumpClassesAndMethods' can be set");
+ }
CmdlineBuilder args;
FdLogger fd_logger;
@@ -622,6 +625,11 @@
OR_RETURN_NON_FATAL(NewFile::Create(output_profile_path, in_outputProfile->fsPermission));
if (in_referenceProfile.has_value()) {
+ if (in_options.forceMerge || in_options.dumpOnly || in_options.dumpClassesAndMethods) {
+ return Fatal(
+ "Reference profile must not be set when 'forceMerge', 'dumpOnly', or "
+ "'dumpClassesAndMethods' is set");
+ }
std::string reference_profile_path =
OR_RETURN_FATAL(BuildProfileOrDmPath(*in_referenceProfile));
if (in_referenceProfile->getTag() == ProfilePath::dexMetadataPath) {
@@ -630,8 +638,12 @@
OR_RETURN_NON_FATAL(CopyFile(reference_profile_path, *output_profile_file));
}
- // profman is ok with this being an empty file when in_referenceProfile isn't set.
- args.Add("--reference-profile-file-fd=%d", output_profile_file->Fd());
+ if (in_options.dumpOnly || in_options.dumpClassesAndMethods) {
+ args.Add("--dump-output-to-fd=%d", output_profile_file->Fd());
+ } else {
+ // profman is ok with this being an empty file when in_referenceProfile isn't set.
+ args.Add("--reference-profile-file-fd=%d", output_profile_file->Fd());
+ }
fd_logger.Add(*output_profile_file);
std::vector<std::unique_ptr<File>> dex_files;
@@ -642,12 +654,16 @@
dex_files.push_back(std::move(dex_file));
}
- args.AddIfNonEmpty("--min-new-classes-percent-change=%s",
- props_->GetOrEmpty("dalvik.vm.bgdexopt.new-classes-percent"))
- .AddIfNonEmpty("--min-new-methods-percent-change=%s",
- props_->GetOrEmpty("dalvik.vm.bgdexopt.new-methods-percent"))
- .AddIf(in_options.forceMerge, "--force-merge")
- .AddIf(in_options.forBootImage, "--boot-image-merge");
+ if (in_options.dumpOnly || in_options.dumpClassesAndMethods) {
+ args.Add(in_options.dumpOnly ? "--dump-only" : "--dump-classes-and-methods");
+ } else {
+ args.AddIfNonEmpty("--min-new-classes-percent-change=%s",
+ props_->GetOrEmpty("dalvik.vm.bgdexopt.new-classes-percent"))
+ .AddIfNonEmpty("--min-new-methods-percent-change=%s",
+ props_->GetOrEmpty("dalvik.vm.bgdexopt.new-methods-percent"))
+ .AddIf(in_options.forceMerge, "--force-merge")
+ .AddIf(in_options.forBootImage, "--boot-image-merge");
+ }
LOG(INFO) << "Running profman: " << Join(args.Get(), /*separator=*/" ")
<< "\nOpened FDs: " << fd_logger;
@@ -666,7 +682,9 @@
}
ProfmanResult::ProcessingResult expected_result =
- in_options.forceMerge ? ProfmanResult::kSuccess : ProfmanResult::kCompile;
+ (in_options.forceMerge || in_options.dumpOnly || in_options.dumpClassesAndMethods) ?
+ ProfmanResult::kSuccess :
+ ProfmanResult::kCompile;
if (result.value() != expected_result) {
return NonFatal("profman returned an unexpected code: {}"_format(result.value()));
}
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index e28342b..8eab8ad 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -1614,7 +1614,7 @@
EXPECT_THAT(output_profile.profilePath.tmpPath, IsEmpty());
}
-TEST_F(ArtdTest, mergeProfilesWithOptions) {
+TEST_F(ArtdTest, mergeProfilesWithOptionsForceMerge) {
PrimaryCurProfilePath profile_0_path{
.userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
@@ -1649,6 +1649,82 @@
EXPECT_THAT(output_profile.profilePath.tmpPath, Not(IsEmpty()));
}
+TEST_F(ArtdTest, mergeProfilesWithOptionsDumpOnly) {
+ PrimaryCurProfilePath profile_0_path{
+ .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
+ CreateFile(profile_0_file, "def");
+
+ OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpProfilePath>(),
+ .fsPermission = FsPermission{.uid = -1, .gid = -1}};
+ output_profile.profilePath.id = "";
+ output_profile.profilePath.tmpPath = "";
+
+ CreateFile(dex_file_);
+
+ EXPECT_CALL(*mock_exec_utils_,
+ DoExecAndReturnCode(
+ WhenSplitBy("--",
+ _,
+ AllOf(Contains("--dump-only"),
+ Not(Contains(Flag("--reference-profile-file-fd=", _))))),
+ _,
+ _))
+ .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--dump-output-to-fd=", "dump")),
+ Return(ProfmanResult::kSuccess)));
+
+ bool result;
+ EXPECT_TRUE(artd_
+ ->mergeProfiles({profile_0_path},
+ std::nullopt,
+ &output_profile,
+ {dex_file_},
+ {.dumpOnly = true},
+ &result)
+ .isOk());
+ EXPECT_TRUE(result);
+ EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
+ CheckContent(output_profile.profilePath.tmpPath, "dump");
+}
+
+TEST_F(ArtdTest, mergeProfilesWithOptionsDumpClassesAndMethods) {
+ PrimaryCurProfilePath profile_0_path{
+ .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
+ CreateFile(profile_0_file, "def");
+
+ OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpProfilePath>(),
+ .fsPermission = FsPermission{.uid = -1, .gid = -1}};
+ output_profile.profilePath.id = "";
+ output_profile.profilePath.tmpPath = "";
+
+ CreateFile(dex_file_);
+
+ EXPECT_CALL(*mock_exec_utils_,
+ DoExecAndReturnCode(
+ WhenSplitBy("--",
+ _,
+ AllOf(Contains("--dump-classes-and-methods"),
+ Not(Contains(Flag("--reference-profile-file-fd=", _))))),
+ _,
+ _))
+ .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--dump-output-to-fd=", "dump")),
+ Return(ProfmanResult::kSuccess)));
+
+ bool result;
+ EXPECT_TRUE(artd_
+ ->mergeProfiles({profile_0_path},
+ std::nullopt,
+ &output_profile,
+ {dex_file_},
+ {.dumpClassesAndMethods = true},
+ &result)
+ .isOk());
+ EXPECT_TRUE(result);
+ EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
+ CheckContent(output_profile.profilePath.tmpPath, "dump");
+}
+
} // namespace
} // namespace artd
} // namespace art
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 237f8a9..4e45657 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -88,6 +88,11 @@
* writes the merge result to `outputProfile` and fills `outputProfile.profilePath.id` and
* `outputProfile.profilePath.tmpPath` if a merge has been performed.
*
+ * When `options.forceMerge`, `options.dumpOnly`, or `options.dumpClassesAndMethods` is set,
+ * `referenceProfile` must not be set. I.e., all inputs must be provided by `profiles`. This is
+ * because the merge will always happen, and hence no reference profile is needed to calculate
+ * the diff.
+ *
* Throws fatal and non-fatal errors.
*/
boolean mergeProfiles(in List<com.android.server.art.ProfilePath> profiles,
diff --git a/artd/binder/com/android/server/art/MergeProfileOptions.aidl b/artd/binder/com/android/server/art/MergeProfileOptions.aidl
index fb7db80..2d007f9 100644
--- a/artd/binder/com/android/server/art/MergeProfileOptions.aidl
+++ b/artd/binder/com/android/server/art/MergeProfileOptions.aidl
@@ -32,4 +32,8 @@
boolean forceMerge;
/** --boot-image-merge */
boolean forBootImage;
+ /** --dump-only */
+ boolean dumpOnly;
+ /** --dump-classes-and-methods */
+ boolean dumpClassesAndMethods;
}
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index bf77989..61cfc16 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -549,6 +549,32 @@
public ParcelFileDescriptor snapshotAppProfile(
@NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
@Nullable String splitName) throws SnapshotProfileException {
+ var options = new MergeProfileOptions();
+ options.forceMerge = true;
+ return snapshotOrDumpAppProfile(snapshot, packageName, splitName, options);
+ }
+
+ /**
+ * Same as above, but outputs in text format.
+ *
+ * @hide
+ */
+ @NonNull
+ public ParcelFileDescriptor dumpAppProfile(
+ @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
+ @Nullable String splitName, boolean dumpClassesAndMethods)
+ throws SnapshotProfileException {
+ var options = new MergeProfileOptions();
+ options.dumpOnly = !dumpClassesAndMethods;
+ options.dumpClassesAndMethods = dumpClassesAndMethods;
+ return snapshotOrDumpAppProfile(snapshot, packageName, splitName, options);
+ }
+
+ @NonNull
+ private ParcelFileDescriptor snapshotOrDumpAppProfile(
+ @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
+ @Nullable String splitName, @NonNull MergeProfileOptions options)
+ throws SnapshotProfileException {
PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
PrimaryDexInfo dexInfo = PrimaryDexUtils.getDexInfoBySplitName(pkg, splitName);
@@ -561,8 +587,7 @@
OutputProfile output = PrimaryDexUtils.buildOutputProfile(
pkgState, dexInfo, Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */);
- return mergeProfilesAndGetFd(
- profiles, output, List.of(dexInfo.dexPath()), false /* forBootImage */);
+ return mergeProfilesAndGetFd(profiles, output, List.of(dexInfo.dexPath()), options);
}
/**
@@ -619,7 +644,10 @@
.flatMap(classpath -> Arrays.stream(classpath.split(":")))
.collect(Collectors.toList());
- return mergeProfilesAndGetFd(profiles, output, dexPaths, true /* forBootImage */);
+ var options = new MergeProfileOptions();
+ options.forceMerge = true;
+ options.forBootImage = true;
+ return mergeProfilesAndGetFd(profiles, output, dexPaths, options);
}
/**
@@ -733,13 +761,9 @@
@NonNull
private ParcelFileDescriptor mergeProfilesAndGetFd(@NonNull List<ProfilePath> profiles,
- @NonNull OutputProfile output, @NonNull List<String> dexPaths, boolean forBootImage)
- throws SnapshotProfileException {
+ @NonNull OutputProfile output, @NonNull List<String> dexPaths,
+ @NonNull MergeProfileOptions options) throws SnapshotProfileException {
try {
- var options = new MergeProfileOptions();
- options.forceMerge = true;
- options.forBootImage = forBootImage;
-
boolean hasContent = false;
try {
hasContent = mInjector.getArtd().mergeProfiles(
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index c282e8c..378412c 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -19,6 +19,7 @@
import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
import static com.android.server.art.ArtManagerLocal.SnapshotProfileException;
+import static com.android.server.art.PrimaryDexUtils.PrimaryDexInfo;
import static com.android.server.art.model.ArtFlags.OptimizeFlags;
import static com.android.server.art.model.OptimizationStatus.DexContainerFileOptimizationStatus;
import static com.android.server.art.model.OptimizeResult.DexContainerFileOptimizeResult;
@@ -31,6 +32,10 @@
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructStat;
+import android.util.Log;
import androidx.annotation.RequiresApi;
@@ -43,6 +48,8 @@
import com.android.server.art.model.OptimizeParams;
import com.android.server.art.model.OptimizeResult;
import com.android.server.pm.PackageManagerLocal;
+import com.android.server.pm.pkg.AndroidPackage;
+import com.android.server.pm.pkg.PackageState;
import libcore.io.Streams;
@@ -51,6 +58,8 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -66,6 +75,9 @@
public final class ArtShellCommand extends BasicShellCommandHandler {
private static final String TAG = "ArtShellCommand";
+ /** The default location for profile dumps. */
+ private final static String PROFILE_DEBUG_LOCATION = "/data/misc/profman";
+
private final ArtManagerLocal mArtManagerLocal;
private final PackageManagerLocal mPackageManagerLocal;
private final DexUseManagerLocal mDexUseManager;
@@ -270,26 +282,66 @@
}
}
case "snapshot-app-profile": {
- String outputPath = getNextArgRequired();
+ String packageName = getNextArgRequired();
+ String splitName = getNextArg();
+ String outputRelativePath = String.format("%s%s.prof", packageName,
+ splitName != null ? String.format("-split_%s.apk", splitName) : "");
ParcelFileDescriptor fd;
try {
- fd = mArtManagerLocal.snapshotAppProfile(
- snapshot, getNextArgRequired(), getNextOption());
+ fd = mArtManagerLocal.snapshotAppProfile(snapshot, packageName, splitName);
} catch (SnapshotProfileException e) {
throw new RuntimeException(e);
}
- writeFdContentsToFile(fd, outputPath);
+ writeProfileFdContentsToFile(fd, outputRelativePath);
return 0;
}
case "snapshot-boot-image-profile": {
- String outputPath = getNextArgRequired();
+ String outputRelativePath = "android.prof";
ParcelFileDescriptor fd;
try {
fd = mArtManagerLocal.snapshotBootImageProfile(snapshot);
} catch (SnapshotProfileException e) {
throw new RuntimeException(e);
}
- writeFdContentsToFile(fd, outputPath);
+ writeProfileFdContentsToFile(fd, outputRelativePath);
+ return 0;
+ }
+ case "dump-profiles": {
+ boolean dumpClassesAndMethods = false;
+ String opt;
+ while ((opt = getNextOption()) != null) {
+ switch (opt) {
+ case "--dump-classes-and-methods": {
+ dumpClassesAndMethods = true;
+ break;
+ }
+ default:
+ pw.println("Error: Unknown option: " + opt);
+ return 1;
+ }
+ }
+ String packageName = getNextArgRequired();
+ PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+ AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+ for (PrimaryDexInfo dexInfo : PrimaryDexUtils.getDexInfo(pkg)) {
+ if (!dexInfo.hasCode()) {
+ continue;
+ }
+ String profileName = PrimaryDexUtils.getProfileName(dexInfo.splitName());
+ // The path is intentionally inconsistent with the one for
+ // "snapshot-app-profile". The is to match the behavior of the legacy PM
+ // shell command.
+ String outputRelativePath =
+ String.format("%s-%s.prof.txt", packageName, profileName);
+ ParcelFileDescriptor fd;
+ try {
+ fd = mArtManagerLocal.dumpAppProfile(snapshot, packageName,
+ dexInfo.splitName(), dumpClassesAndMethods);
+ } catch (SnapshotProfileException e) {
+ throw new RuntimeException(e);
+ }
+ writeProfileFdContentsToFile(fd, outputRelativePath);
+ }
return 0;
}
default:
@@ -375,11 +427,22 @@
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.");
- pw.println(" snapshot-app-profile OUTPUT_PATH PACKAGE_NAME [SPLIT_NAME]");
- pw.println(" Snapshot the profile of the given app and save it to the output path.");
- pw.println(" If SPLIT_NAME is empty, the command snapshots the base APK.");
- pw.println(" snapshot-boot-image-profile OUTPUT_PATH");
- pw.println(" Snapshot the boot image profile and save it to the output path.");
+ pw.println(" snapshot-app-profile PACKAGE_NAME [SPLIT_NAME]");
+ pw.println(" Snapshot the profile of the given app and save it to");
+ pw.println(" '" + PROFILE_DEBUG_LOCATION + "'.");
+ pw.println(" If SPLIT_NAME is empty, the command is for the base APK, and the output");
+ pw.println(" filename is 'PACKAGE_NAME.prof'. Otherwise, the command is for the given");
+ pw.println(" split, and the output filename is");
+ pw.println(" 'PACKAGE_NAME-split_SPLIT_NAME.apk.prof'.");
+ pw.println(" snapshot-boot-image-profile");
+ pw.println(" Snapshot the boot image profile and save it to");
+ pw.println(" '" + PROFILE_DEBUG_LOCATION + "/android.prof'.");
+ pw.println(" dump-profiles [--dump-classes-and-methods] PACKAGE_NAME");
+ pw.println(" Dump the profiles of the given app in text format and save the outputs to");
+ pw.println(" '" + PROFILE_DEBUG_LOCATION + "'.");
+ pw.println(" The profile of the base APK is dumped to 'PACKAGE_NAME-primary.prof.txt'");
+ pw.println(" The profile of a split APK is dumped to");
+ pw.println(" 'PACKAGE_NAME-SPLIT_NAME.split.prof.txt'");
}
private void enforceRoot() {
@@ -423,12 +486,30 @@
}
}
- private void writeFdContentsToFile(
- @NonNull ParcelFileDescriptor fd, @NonNull String outputPath) {
+ private void writeProfileFdContentsToFile(
+ @NonNull ParcelFileDescriptor fd, @NonNull String outputRelativePath) {
+ try {
+ StructStat st = Os.stat(PROFILE_DEBUG_LOCATION);
+ if (st.st_uid != Process.SYSTEM_UID || st.st_gid != Process.SHELL_UID
+ || (st.st_mode & 0007) != 0) {
+ throw new RuntimeException(
+ String.format("%s has wrong permissions: uid=%d, gid=%d, mode=%o",
+ PROFILE_DEBUG_LOCATION, st.st_uid, st.st_gid, st.st_mode));
+ }
+ } catch (ErrnoException e) {
+ throw new RuntimeException("Unable to stat " + PROFILE_DEBUG_LOCATION, e);
+ }
+ Path outputPath = Paths.get(PROFILE_DEBUG_LOCATION, outputRelativePath);
try (InputStream inputStream = new AutoCloseInputStream(fd);
- OutputStream outputStream = new FileOutputStream(outputPath)) {
+ FileOutputStream outputStream = new FileOutputStream(outputPath.toFile())) {
+ // The system server doesn't have the permission to chown the file to "shell", so we
+ // make it readable by everyone and put it in a directory that is only accessible by
+ // "shell", which is created by system/core/rootdir/init.rc. The permissions are
+ // verified by the code above.
+ Os.fchmod(outputStream.getFD(), 0644);
Streams.copy(inputStream, outputStream);
- } catch (IOException e) {
+ } catch (IOException | ErrnoException e) {
+ Utils.deleteIfExistsSafe(outputPath);
throw new RuntimeException(e);
}
}
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
index 5af9f0a..ada6488 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
@@ -324,7 +324,7 @@
}
@NonNull
- private static String getProfileName(@Nullable String splitName) {
+ public static String getProfileName(@Nullable String splitName) {
return splitName == null ? "primary" : splitName + ".split";
}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 64a33ab..f104f98 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -23,6 +23,7 @@
import android.os.SystemProperties;
import android.os.UserManager;
import android.text.TextUtils;
+import android.util.Log;
import android.util.SparseArray;
import com.android.server.art.model.OptimizeParams;
@@ -35,6 +36,9 @@
import com.google.auto.value.AutoValue;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -47,6 +51,7 @@
/** @hide */
public final class Utils {
+ public static final String TAG = "ArtServiceUtils";
public static final String PLATFORM_PACKAGE_NAME = "android";
private Utils() {}
@@ -284,6 +289,14 @@
return Math.max(lastUsedAtMs, lastFirstInstallTimeMs);
}
+ public static void deleteIfExistsSafe(@NonNull Path path) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to delete file '" + path + "'", e);
+ }
+ }
+
@AutoValue
public abstract static class Abi {
static @NonNull Abi create(
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index 44b0498..f23644e 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -641,6 +641,30 @@
}
@Test
+ public void testDumpAppProfile() throws Exception {
+ var options = new MergeProfileOptions();
+ options.dumpOnly = true;
+
+ when(mArtd.mergeProfiles(any(), isNull(), any(), any(), deepEq(options)))
+ .thenReturn(false); // A non-empty merge is tested in `testSnapshotAppProfile`.
+
+ ParcelFileDescriptor fd = mArtManagerLocal.dumpAppProfile(
+ mSnapshot, PKG_NAME, null /* splitName */, false /* dumpClassesAndMethods */);
+ }
+
+ @Test
+ public void testDumpAppProfileDumpClassesAndMethods() throws Exception {
+ var options = new MergeProfileOptions();
+ options.dumpClassesAndMethods = true;
+
+ when(mArtd.mergeProfiles(any(), isNull(), any(), any(), deepEq(options)))
+ .thenReturn(false); // A non-empty merge is tested in `testSnapshotAppProfile`.
+
+ ParcelFileDescriptor fd = mArtManagerLocal.dumpAppProfile(
+ mSnapshot, PKG_NAME, null /* splitName */, true /* dumpClassesAndMethods */);
+ }
+
+ @Test
public void testSnapshotBootImageProfile() throws Exception {
// `lenient()` is required to allow mocking the same method multiple times.
lenient().when(Constants.getenv("BOOTCLASSPATH")).thenReturn("bcp0:bcp1");