Support snapshotting profiles.
Bug: 258486155
Test: atest ArtServiceTests
Test: adb shell pm art snapshot-app-profile /data/system/1.prof com.google.android.youtube
Test: adb shell pm art snapshot-boot-image-profile /data/system/1.prof
Ignore-AOSP-First: ART Services.
Change-Id: I81220ca563ac24100056083bc4a4a8661c0dc1d0
diff --git a/artd/artd.cc b/artd/artd.cc
index b97ed6a..208e5cc 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -83,6 +83,7 @@
using ::aidl::com::android::server::art::GetDexoptNeededResult;
using ::aidl::com::android::server::art::GetOptimizationStatusResult;
using ::aidl::com::android::server::art::IArtdCancellationSignal;
+using ::aidl::com::android::server::art::MergeProfileOptions;
using ::aidl::com::android::server::art::OutputArtifacts;
using ::aidl::com::android::server::art::OutputProfile;
using ::aidl::com::android::server::art::PriorityClass;
@@ -511,6 +512,7 @@
OR_RETURN_NON_FATAL(dst->Keep());
*_aidl_return = true;
in_dst->profilePath.id = dst->TempId();
+ in_dst->profilePath.tmpPath = dst->TempPath();
return ScopedAStatus::ok();
}
@@ -570,7 +572,8 @@
ndk::ScopedAStatus Artd::mergeProfiles(const std::vector<ProfilePath>& in_profiles,
const std::optional<ProfilePath>& in_referenceProfile,
OutputProfile* in_outputProfile,
- const std::string& in_dexFile,
+ const std::vector<std::string>& in_dexFiles,
+ const MergeProfileOptions& in_options,
bool* _aidl_return) {
std::vector<std::string> profile_paths;
for (const ProfilePath& profile : in_profiles) {
@@ -582,7 +585,9 @@
}
std::string output_profile_path =
OR_RETURN_FATAL(BuildFinalProfilePath(in_outputProfile->profilePath));
- OR_RETURN_FATAL(ValidateDexPath(in_dexFile));
+ for (const std::string& dex_file : in_dexFiles) {
+ OR_RETURN_FATAL(ValidateDexPath(dex_file));
+ }
CmdlineBuilder args;
FdLogger fd_logger;
@@ -629,14 +634,20 @@
args.Add("--reference-profile-file-fd=%d", output_profile_file->Fd());
fd_logger.Add(*output_profile_file);
- std::unique_ptr<File> dex_file = OR_RETURN_NON_FATAL(OpenFileForReading(in_dexFile));
- args.Add("--apk-fd=%d", dex_file->Fd());
- fd_logger.Add(*dex_file);
+ std::vector<std::unique_ptr<File>> dex_files;
+ for (const std::string& dex_path : in_dexFiles) {
+ std::unique_ptr<File> dex_file = OR_RETURN_NON_FATAL(OpenFileForReading(dex_path));
+ args.Add("--apk-fd=%d", dex_file->Fd());
+ fd_logger.Add(*dex_file);
+ 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"));
+ 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;
@@ -654,13 +665,16 @@
return ScopedAStatus::ok();
}
- if (result.value() != ProfmanResult::kCompile) {
+ ProfmanResult::ProcessingResult expected_result =
+ in_options.forceMerge ? ProfmanResult::kSuccess : ProfmanResult::kCompile;
+ if (result.value() != expected_result) {
return NonFatal("profman returned an unexpected code: {}"_format(result.value()));
}
OR_RETURN_NON_FATAL(output_profile_file->Keep());
*_aidl_return = true;
in_outputProfile->profilePath.id = output_profile_file->TempId();
+ in_outputProfile->profilePath.tmpPath = output_profile_file->TempPath();
return ScopedAStatus::ok();
}
diff --git a/artd/artd.h b/artd/artd.h
index c024087..e90d541 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -109,7 +109,8 @@
const std::vector<aidl::com::android::server::art::ProfilePath>& in_profiles,
const std::optional<aidl::com::android::server::art::ProfilePath>& in_referenceProfile,
aidl::com::android::server::art::OutputProfile* in_outputProfile,
- const std::string& in_dexFile,
+ const std::vector<std::string>& in_dexFiles,
+ const aidl::com::android::server::art::MergeProfileOptions& in_options,
bool* _aidl_return) override;
ndk::ScopedAStatus getArtifactsVisibility(
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index e1480cd..f14a608 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -1063,6 +1063,7 @@
CreateFile(src_file, "abc");
OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
dst.profilePath.id = "";
+ dst.profilePath.tmpPath = "";
CreateFile(dex_file_);
@@ -1084,7 +1085,9 @@
EXPECT_TRUE(artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result).isOk());
EXPECT_TRUE(result);
EXPECT_THAT(dst.profilePath.id, Not(IsEmpty()));
- CheckContent(OR_FATAL(BuildTmpProfilePath(dst.profilePath)), "def");
+ std::string real_path = OR_FATAL(BuildTmpProfilePath(dst.profilePath));
+ EXPECT_EQ(dst.profilePath.tmpPath, real_path);
+ CheckContent(real_path, "def");
}
TEST_F(ArtdTest, copyAndRewriteProfileFalse) {
@@ -1093,6 +1096,7 @@
CreateFile(src_file, "abc");
OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
dst.profilePath.id = "";
+ dst.profilePath.tmpPath = "";
CreateFile(dex_file_);
@@ -1102,6 +1106,8 @@
bool result;
EXPECT_TRUE(artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result).isOk());
EXPECT_FALSE(result);
+ EXPECT_THAT(dst.profilePath.id, IsEmpty());
+ EXPECT_THAT(dst.profilePath.tmpPath, IsEmpty());
}
TEST_F(ArtdTest, copyAndRewriteProfileNotFound) {
@@ -1110,10 +1116,13 @@
const TmpProfilePath& src = profile_path_->get<ProfilePath::tmpProfilePath>();
OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
dst.profilePath.id = "";
+ dst.profilePath.tmpPath = "";
bool result;
EXPECT_TRUE(artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result).isOk());
EXPECT_FALSE(result);
+ EXPECT_THAT(dst.profilePath.id, IsEmpty());
+ EXPECT_THAT(dst.profilePath.tmpPath, IsEmpty());
}
TEST_F(ArtdTest, copyAndRewriteProfileFailed) {
@@ -1122,6 +1131,7 @@
CreateFile(src_file, "abc");
OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
dst.profilePath.id = "";
+ dst.profilePath.tmpPath = "";
CreateFile(dex_file_);
@@ -1133,6 +1143,8 @@
EXPECT_FALSE(status.isOk());
EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC);
EXPECT_THAT(status.getMessage(), HasSubstr("profman returned an unexpected code: 100"));
+ EXPECT_THAT(dst.profilePath.id, IsEmpty());
+ EXPECT_THAT(dst.profilePath.tmpPath, IsEmpty());
}
TEST_F(ArtdTest, commitTmpProfile) {
@@ -1334,8 +1346,12 @@
OutputProfile output_profile{.profilePath = reference_profile_path,
.fsPermission = FsPermission{.uid = -1, .gid = -1}};
output_profile.profilePath.id = "";
+ output_profile.profilePath.tmpPath = "";
- CreateFile(dex_file_);
+ std::string dex_file_1 = scratch_path_ + "/a/b.apk";
+ std::string dex_file_2 = scratch_path_ + "/a/c.apk";
+ CreateFile(dex_file_1);
+ CreateFile(dex_file_2);
EXPECT_CALL(
*mock_exec_utils_,
@@ -1346,7 +1362,10 @@
Not(Contains(Flag("--profile-file-fd=", FdOf(profile_0_file)))),
Contains(Flag("--profile-file-fd=", FdOf(profile_1_file))),
Contains(Flag("--reference-profile-file-fd=", FdHasContent("abc"))),
- Contains(Flag("--apk-fd=", FdOf(dex_file_))))),
+ Contains(Flag("--apk-fd=", FdOf(dex_file_1))),
+ Contains(Flag("--apk-fd=", FdOf(dex_file_2))),
+ Not(Contains("--force-merge")),
+ Not(Contains("--boot-image-merge")))),
_,
_))
.WillOnce(DoAll(WithArg<0>(ClearAndWriteToFdFlag("--reference-profile-file-fd=", "merged")),
@@ -1357,12 +1376,15 @@
->mergeProfiles({profile_0_path, profile_1_path},
reference_profile_path,
&output_profile,
- dex_file_,
+ {dex_file_1, dex_file_2},
+ /*in_options=*/{},
&result)
.isOk());
EXPECT_TRUE(result);
EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
- CheckContent(OR_FATAL(BuildTmpProfilePath(output_profile.profilePath)), "merged");
+ std::string real_path = OR_FATAL(BuildTmpProfilePath(output_profile.profilePath));
+ EXPECT_EQ(output_profile.profilePath.tmpPath, real_path);
+ CheckContent(real_path, "merged");
}
TEST_F(ArtdTest, mergeProfilesEmptyReferenceProfile) {
@@ -1374,6 +1396,7 @@
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_);
@@ -1392,12 +1415,17 @@
Return(ProfmanResult::kCompile)));
bool result;
- EXPECT_TRUE(
- artd_->mergeProfiles({profile_0_path}, std::nullopt, &output_profile, dex_file_, &result)
- .isOk());
+ EXPECT_TRUE(artd_
+ ->mergeProfiles({profile_0_path},
+ std::nullopt,
+ &output_profile,
+ {dex_file_},
+ /*in_options=*/{},
+ &result)
+ .isOk());
EXPECT_TRUE(result);
EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
- CheckContent(OR_FATAL(BuildTmpProfilePath(output_profile.profilePath)), "merged");
+ EXPECT_THAT(output_profile.profilePath.tmpPath, Not(IsEmpty()));
}
TEST_F(ArtdTest, mergeProfilesProfilesDontExist) {
@@ -1418,17 +1446,59 @@
OutputProfile output_profile{.profilePath = reference_profile_path,
.fsPermission = FsPermission{.uid = -1, .gid = -1}};
output_profile.profilePath.id = "";
+ output_profile.profilePath.tmpPath = "";
CreateFile(dex_file_);
EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode).Times(0);
bool result;
- EXPECT_TRUE(
- artd_->mergeProfiles({profile_0_path}, std::nullopt, &output_profile, dex_file_, &result)
- .isOk());
+ EXPECT_TRUE(artd_
+ ->mergeProfiles({profile_0_path},
+ std::nullopt,
+ &output_profile,
+ {dex_file_},
+ /*in_options=*/{},
+ &result)
+ .isOk());
EXPECT_FALSE(result);
EXPECT_THAT(output_profile.profilePath.id, IsEmpty());
+ EXPECT_THAT(output_profile.profilePath.tmpPath, IsEmpty());
+}
+
+TEST_F(ArtdTest, mergeProfilesWithOptions) {
+ 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("--force-merge"), Contains("--boot-image-merge"))),
+ _,
+ _))
+ .WillOnce(Return(ProfmanResult::kSuccess));
+
+ bool result;
+ EXPECT_TRUE(artd_
+ ->mergeProfiles({profile_0_path},
+ std::nullopt,
+ &output_profile,
+ {dex_file_},
+ {.forceMerge = true, .forBootImage = true},
+ &result)
+ .isOk());
+ EXPECT_TRUE(result);
+ EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
+ EXPECT_THAT(output_profile.profilePath.tmpPath, Not(IsEmpty()));
}
} // namespace
diff --git a/artd/binder/com/android/server/art/IArtd.aidl b/artd/binder/com/android/server/art/IArtd.aidl
index 19c1d66..237f8a9 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -85,14 +85,16 @@
* Merges profiles. Both `profiles` and `referenceProfile` are inputs, while the difference is
* that `referenceProfile` is also used as the reference to calculate the diff. `profiles` that
* don't exist are skipped, while `referenceProfile`, if provided, must exist. Returns true,
- * writes the merge result to `outputProfile` and fills `outputProfile.profilePath.id` if a
- * merge has been performed.
+ * writes the merge result to `outputProfile` and fills `outputProfile.profilePath.id` and
+ * `outputProfile.profilePath.tmpPath` if a merge has been performed.
*
* Throws fatal and non-fatal errors.
*/
boolean mergeProfiles(in List<com.android.server.art.ProfilePath> profiles,
in @nullable com.android.server.art.ProfilePath referenceProfile,
- inout com.android.server.art.OutputProfile outputProfile, @utf8InCpp String dexFile);
+ inout com.android.server.art.OutputProfile outputProfile,
+ in @utf8InCpp List<String> dexFiles,
+ in com.android.server.art.MergeProfileOptions options);
/**
* Returns the visibility of the artifacts.
diff --git a/artd/binder/com/android/server/art/MergeProfileOptions.aidl b/artd/binder/com/android/server/art/MergeProfileOptions.aidl
new file mode 100644
index 0000000..fb7db80
--- /dev/null
+++ b/artd/binder/com/android/server/art/MergeProfileOptions.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * Miscellaneous options for merging profiles. Every field corresponds to a profman command line
+ * flag.
+ *
+ * DO NOT add fields for flags that artd can determine directly with trivial logic. That includes
+ * static flags, and flags that only depend on system properties or other passed parameters.
+ *
+ * All fields are required.
+ *
+ * @hide
+ */
+parcelable MergeProfileOptions {
+ /** --force-merge */
+ boolean forceMerge;
+ /** --boot-image-merge */
+ boolean forBootImage;
+}
diff --git a/artd/binder/com/android/server/art/ProfilePath.aidl b/artd/binder/com/android/server/art/ProfilePath.aidl
index fd413a9..43df531 100644
--- a/artd/binder/com/android/server/art/ProfilePath.aidl
+++ b/artd/binder/com/android/server/art/ProfilePath.aidl
@@ -90,5 +90,7 @@
WritableProfilePath finalPath;
/** A unique identifier to distinguish this temporary file from others. Filled by artd. */
@utf8InCpp String id;
+ /** The path to the temporary file. Filled by artd. */
+ @utf8InCpp String tmpPath;
}
}
diff --git a/libartservice/service/api/system-server-current.txt b/libartservice/service/api/system-server-current.txt
index 0eb1738..eea0da2 100644
--- a/libartservice/service/api/system-server-current.txt
+++ b/libartservice/service/api/system-server-current.txt
@@ -20,6 +20,8 @@
method public int scheduleBackgroundDexoptJob();
method public void setOptimizePackagesCallback(@NonNull java.util.concurrent.Executor, @NonNull com.android.server.art.ArtManagerLocal.OptimizePackagesCallback);
method public void setScheduleBackgroundDexoptJobCallback(@NonNull java.util.concurrent.Executor, @NonNull com.android.server.art.ArtManagerLocal.ScheduleBackgroundDexoptJobCallback);
+ method @NonNull public android.os.ParcelFileDescriptor snapshotAppProfile(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot, @NonNull String, @Nullable String) throws com.android.server.art.ArtManagerLocal.SnapshotProfileException;
+ method @NonNull public android.os.ParcelFileDescriptor snapshotBootImageProfile(@NonNull com.android.server.pm.PackageManagerLocal.FilteredSnapshot) throws com.android.server.art.ArtManagerLocal.SnapshotProfileException;
method public void startBackgroundDexoptJob();
method public void unscheduleBackgroundDexoptJob();
}
@@ -36,6 +38,10 @@
method public void onOverrideJobInfo(@NonNull android.app.job.JobInfo.Builder);
}
+ public static class ArtManagerLocal.SnapshotProfileException extends java.lang.Exception {
+ ctor public ArtManagerLocal.SnapshotProfileException(@NonNull Throwable);
+ }
+
public class ArtModuleServiceInitializer {
method public static void setArtModuleServiceManager(@NonNull android.os.ArtModuleServiceManager);
}
diff --git a/libartservice/service/java/com/android/server/art/AidlUtils.java b/libartservice/service/java/com/android/server/art/AidlUtils.java
index 21563aea..f38000d 100644
--- a/libartservice/service/java/com/android/server/art/AidlUtils.java
+++ b/libartservice/service/java/com/android/server/art/AidlUtils.java
@@ -149,6 +149,7 @@
outputProfile.profilePath = new TmpProfilePath();
outputProfile.profilePath.finalPath = finalPath;
outputProfile.profilePath.id = ""; // Will be filled by artd.
+ outputProfile.profilePath.tmpPath = ""; // Will be filled by artd.
outputProfile.fsPermission = buildFsPermission(uid, gid, isPublic);
return outputProfile;
}
diff --git a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
index 90f027a..7a5aa61 100644
--- a/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
+++ b/libartservice/service/java/com/android/server/art/ArtManagerLocal.java
@@ -37,8 +37,11 @@
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
+import android.os.UserManager;
+import android.text.TextUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalManagerRegistry;
@@ -53,13 +56,17 @@
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
+import java.io.File;
+import java.io.FileNotFoundException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
/**
* This class provides a system API for functionality provided by the ART module.
@@ -75,6 +82,8 @@
@SystemApi(client = SystemApi.Client.SYSTEM_SERVER)
public final class ArtManagerLocal {
private static final String TAG = "ArtService";
+ private static final String[] CLASSPATHS_FOR_BOOT_IMAGE_PROFILE = {
+ "BOOTCLASSPATH", "SYSTEMSERVERCLASSPATH", "STANDALONE_SYSTEMSERVER_JARS"};
@NonNull private final Injector mInjector;
@@ -122,7 +131,8 @@
* Uses the default flags ({@link ArtFlags#defaultDeleteFlags()}).
*
* @throws IllegalArgumentException if the package is not found or the flags are illegal
- * @throws IllegalStateException if an internal error occurs
+ * @throws IllegalStateException if the operation encounters an error that should never happen
+ * (e.g., an internal logic error).
*/
@NonNull
public DeleteResult deleteOptimizedArtifacts(
@@ -182,7 +192,8 @@
* Uses the default flags ({@link ArtFlags#defaultGetStatusFlags()}).
*
* @throws IllegalArgumentException if the package is not found or the flags are illegal
- * @throws IllegalStateException if an internal error occurs
+ * @throws IllegalStateException if the operation encounters an error that should never happen
+ * (e.g., an internal logic error).
*/
@NonNull
public OptimizationStatus getOptimizationStatus(
@@ -254,7 +265,8 @@
* #addOptimizePackageDoneCallback(Executor, OptimizePackageDoneCallback)} are called.
*
* @throws IllegalArgumentException if the package is not found or the params are illegal
- * @throws IllegalStateException if an internal error occurs
+ * @throws IllegalStateException if the operation encounters an error that should never happen
+ * (e.g., an internal logic error).
*/
@NonNull
public OptimizeResult optimizePackage(@NonNull PackageManagerLocal.FilteredSnapshot snapshot,
@@ -311,7 +323,8 @@
* @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
* @param reason determines the default list of packages and options
* @param cancellationSignal provides the ability to cancel this operation
- * @throws IllegalStateException if an internal error occurs, or the callback set by {@link
+ * @throws IllegalStateException if the operation encounters an error that should never happen
+ * (e.g., an internal logic error), or the callback set by {@link
* #setOptimizePackagesCallback(Executor, OptimizePackagesCallback)} provides invalid
* params.
*
@@ -511,6 +524,111 @@
}
/**
+ * Snapshots the profile of the given app split. The profile snapshot is the aggregation of all
+ * existing profiles of the app split (all current user profiles and the reference profile).
+ *
+ * @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
+ * @param packageName the name of the app that owns the profile
+ * @param splitName see {@link AndroidPackageSplit#getName()}
+ * @return the file descriptor of the snapshot. It doesn't have any path associated with it. The
+ * caller is responsible for closing it. Note that the content may be empty.
+ * @throws IllegalArgumentException if the package or the split is not found
+ * @throws IllegalStateException if the operation encounters an error that should never happen
+ * (e.g., an internal logic error).
+ * @throws SnapshotProfileException if the operation encounters an error that the caller should
+ * handle (e.g., an I/O error, a sub-process crash).
+ */
+ @NonNull
+ public ParcelFileDescriptor snapshotAppProfile(
+ @NonNull PackageManagerLocal.FilteredSnapshot snapshot, @NonNull String packageName,
+ @Nullable String splitName) throws SnapshotProfileException {
+ PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, packageName);
+ AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+
+ PrimaryDexInfo dexInfo;
+ if (splitName == null) {
+ dexInfo = PrimaryDexUtils.getDexInfo(pkg).get(0);
+ } else {
+ dexInfo = PrimaryDexUtils.getDexInfo(pkg)
+ .stream()
+ .filter(info -> splitName.equals(info.splitName()))
+ .findFirst()
+ .orElseThrow(() -> {
+ return new IllegalArgumentException(
+ String.format("Split '%s' not found", splitName));
+ });
+ }
+
+ List<ProfilePath> profiles = new ArrayList<>();
+ profiles.add(PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo));
+ profiles.addAll(
+ PrimaryDexUtils.getCurProfiles(mInjector.getUserManager(), pkgState, dexInfo));
+
+ OutputProfile output = PrimaryDexUtils.buildOutputProfile(
+ pkgState, dexInfo, Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */);
+
+ return mergeProfilesAndGetFd(
+ profiles, output, List.of(dexInfo.dexPath()), false /* forBootImage */);
+ }
+
+ /**
+ * Snapshots the boot image profile
+ * (https://source.android.com/docs/core/bootloader/boot-image-profiles). The profile snapshot
+ * is the aggregation of all existing profiles on the device (all current user profiles and
+ * reference profiles) of all apps and the system server filtered by applicable classpaths.
+ *
+ * @param snapshot the snapshot from {@link PackageManagerLocal} to operate on
+ * @return the file descriptor of the snapshot. It doesn't have any path associated with it. The
+ * caller is responsible for closing it. Note that the content may be empty.
+ * @throws IllegalStateException if the operation encounters an error that should never happen
+ * (e.g., an internal logic error).
+ * @throws SnapshotProfileException if the operation encounters an error that the caller should
+ * handle (e.g., an I/O error, a sub-process crash).
+ */
+ @NonNull
+ public ParcelFileDescriptor snapshotBootImageProfile(
+ @NonNull PackageManagerLocal.FilteredSnapshot snapshot)
+ throws SnapshotProfileException {
+ List<ProfilePath> profiles = new ArrayList<>();
+
+ // System server profiles.
+ PackageState pkgState = Utils.getPackageStateOrThrow(snapshot, Utils.PLATFORM_PACKAGE_NAME);
+ AndroidPackage pkg = Utils.getPackageOrThrow(pkgState);
+ PrimaryDexInfo dexInfo = PrimaryDexUtils.getDexInfo(pkg).get(0);
+ profiles.add(PrimaryDexUtils.buildRefProfilePath(pkgState, dexInfo));
+ profiles.addAll(
+ PrimaryDexUtils.getCurProfiles(mInjector.getUserManager(), pkgState, dexInfo));
+
+ // App profiles.
+ snapshot.forAllPackageStates((appPkgState) -> {
+ // Hibernating apps can still provide useful profile contents, so skip the hibernation
+ // check.
+ if (Utils.canOptimizePackage(appPkgState, null /* appHibernationManager */)) {
+ AndroidPackage appPkg = Utils.getPackageOrThrow(appPkgState);
+ for (PrimaryDexInfo appDexInfo : PrimaryDexUtils.getDexInfo(appPkg)) {
+ if (!appDexInfo.hasCode()) {
+ continue;
+ }
+ profiles.add(PrimaryDexUtils.buildRefProfilePath(appPkgState, appDexInfo));
+ profiles.addAll(PrimaryDexUtils.getCurProfiles(
+ mInjector.getUserManager(), appPkgState, appDexInfo));
+ }
+ }
+ });
+
+ OutputProfile output = AidlUtils.buildOutputProfileForPrimary(Utils.PLATFORM_PACKAGE_NAME,
+ "primary", Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */);
+
+ List<String> dexPaths = Arrays.stream(CLASSPATHS_FOR_BOOT_IMAGE_PROFILE)
+ .map(envVar -> Constants.getenv(envVar))
+ .filter(classpath -> !TextUtils.isEmpty(classpath))
+ .flatMap(classpath -> Arrays.stream(classpath.split(":")))
+ .collect(Collectors.toList());
+
+ return mergeProfilesAndGetFd(profiles, output, dexPaths, true /* forBootImage */);
+ }
+
+ /**
* Should be used by {@link BackgroundDexOptJobService} ONLY.
*
* @hide
@@ -532,6 +650,44 @@
return packages;
}
+ @NonNull
+ private ParcelFileDescriptor mergeProfilesAndGetFd(@NonNull List<ProfilePath> profiles,
+ @NonNull OutputProfile output, @NonNull List<String> dexPaths, boolean forBootImage)
+ throws SnapshotProfileException {
+ try {
+ var options = new MergeProfileOptions();
+ options.forceMerge = true;
+ options.forBootImage = forBootImage;
+
+ boolean hasContent = false;
+ try {
+ hasContent = mInjector.getArtd().mergeProfiles(
+ profiles, null /* referenceProfile */, output, dexPaths, options);
+ } catch (ServiceSpecificException e) {
+ throw new SnapshotProfileException(e);
+ }
+
+ String path = hasContent ? output.profilePath.tmpPath : "/dev/null";
+ ParcelFileDescriptor fd;
+ try {
+ fd = ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(
+ String.format("Failed to open profile snapshot '%s'", path), e);
+ }
+
+ if (hasContent) {
+ // This is done on the open file so that only the FD keeps a reference to its
+ // contents.
+ mInjector.getArtd().deleteProfile(ProfilePath.tmpProfilePath(output.profilePath));
+ }
+
+ return fd;
+ } catch (RemoteException e) {
+ throw new IllegalStateException("An error occurred when calling artd", e);
+ }
+ }
+
public interface OptimizePackagesCallback {
/**
* Mutates {@code builder} to override the default params for {@link
@@ -569,6 +725,13 @@
void onOptimizePackageDone(@NonNull OptimizeResult result);
}
+ /** Represents an error that happens when snapshotting profiles. */
+ public static class SnapshotProfileException extends Exception {
+ public SnapshotProfileException(@NonNull Throwable cause) {
+ super(cause);
+ }
+ }
+
/**
* Injector pattern for testing purpose.
*
@@ -629,5 +792,10 @@
public BackgroundDexOptJob getBackgroundDexOptJob() {
return Objects.requireNonNull(mBgDexOptJob);
}
+
+ @NonNull
+ public UserManager getUserManager() {
+ return Objects.requireNonNull(mContext.getSystemService(UserManager.class));
+ }
}
}
diff --git a/libartservice/service/java/com/android/server/art/ArtShellCommand.java b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
index 5540ac3..cc9d7ae 100644
--- a/libartservice/service/java/com/android/server/art/ArtShellCommand.java
+++ b/libartservice/service/java/com/android/server/art/ArtShellCommand.java
@@ -16,6 +16,9 @@
package com.android.server.art;
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
+
+import static com.android.server.art.ArtManagerLocal.SnapshotProfileException;
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;
@@ -25,6 +28,7 @@
import android.annotation.NonNull;
import android.os.Binder;
import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
import android.os.Process;
import com.android.internal.annotations.GuardedBy;
@@ -36,6 +40,12 @@
import com.android.server.art.model.OptimizeResult;
import com.android.server.pm.PackageManagerLocal;
+import libcore.io.Streams;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@@ -219,6 +229,29 @@
return 1;
}
}
+ case "snapshot-app-profile": {
+ String outputPath = getNextArgRequired();
+ ParcelFileDescriptor fd;
+ try {
+ fd = mArtManagerLocal.snapshotAppProfile(
+ snapshot, getNextArgRequired(), getNextOption());
+ } catch (SnapshotProfileException e) {
+ throw new RuntimeException(e);
+ }
+ writeFdContentsToFile(fd, outputPath);
+ return 0;
+ }
+ case "snapshot-boot-image-profile": {
+ String outputPath = getNextArgRequired();
+ ParcelFileDescriptor fd;
+ try {
+ fd = mArtManagerLocal.snapshotBootImageProfile(snapshot);
+ } catch (SnapshotProfileException e) {
+ throw new RuntimeException(e);
+ }
+ writeFdContentsToFile(fd, outputPath);
+ return 0;
+ }
default:
// Handles empty, help, and invalid commands.
return handleDefaultCommands(cmd);
@@ -294,6 +327,11 @@
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.");
}
private void enforceRoot() {
@@ -337,6 +375,16 @@
}
}
+ private void writeFdContentsToFile(
+ @NonNull ParcelFileDescriptor fd, @NonNull String outputPath) {
+ try (InputStream inputStream = new AutoCloseInputStream(fd);
+ OutputStream outputStream = new FileOutputStream(outputPath)) {
+ Streams.copy(inputStream, outputStream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
private static class WithCancellationSignal implements AutoCloseable {
@NonNull private final CancellationSignal mSignal = new CancellationSignal();
@NonNull private final String mJobId;
diff --git a/libartservice/service/java/com/android/server/art/Constants.java b/libartservice/service/java/com/android/server/art/Constants.java
index 10953c7..2d2d757 100644
--- a/libartservice/service/java/com/android/server/art/Constants.java
+++ b/libartservice/service/java/com/android/server/art/Constants.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
+import android.system.Os;
/**
* A mockable wrapper class for device-specific constants.
@@ -49,4 +50,9 @@
// the native one.
return Build.SUPPORTED_32_BIT_ABIS.length > 0 ? Build.SUPPORTED_32_BIT_ABIS[0] : null;
}
+
+ @Nullable
+ public static String getenv(@NonNull String name) {
+ return Os.getenv(name);
+ }
}
diff --git a/libartservice/service/java/com/android/server/art/DexOptimizer.java b/libartservice/service/java/com/android/server/art/DexOptimizer.java
index 51443c6..13afa0b 100644
--- a/libartservice/service/java/com/android/server/art/DexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/DexOptimizer.java
@@ -490,8 +490,8 @@
OutputProfile output = buildOutputProfile(dexInfo, false /* isPublic */);
try {
- if (mInjector.getArtd().mergeProfiles(
- getCurProfiles(dexInfo), referenceProfile, output, dexInfo.dexPath())) {
+ if (mInjector.getArtd().mergeProfiles(getCurProfiles(dexInfo), referenceProfile, output,
+ List.of(dexInfo.dexPath()), new MergeProfileOptions())) {
return ProfilePath.tmpProfilePath(output.profilePath);
}
} catch (ServiceSpecificException e) {
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
index 67fdced..5ef3d6a 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
@@ -38,7 +38,6 @@
import com.android.server.pm.PackageManagerLocal;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageState;
-import com.android.server.pm.pkg.PackageUserState;
import dalvik.system.DexFile;
@@ -170,8 +169,7 @@
@Override
@NonNull
protected ProfilePath buildRefProfilePath(@NonNull DetailedPrimaryDexInfo dexInfo) {
- String profileName = getProfileName(dexInfo.splitName());
- return AidlUtils.buildProfilePathForPrimaryRef(mPkgState.getPackageName(), profileName);
+ return PrimaryDexUtils.buildRefProfilePath(mPkgState, dexInfo);
}
@Override
@@ -188,25 +186,14 @@
@NonNull
protected OutputProfile buildOutputProfile(
@NonNull DetailedPrimaryDexInfo dexInfo, boolean isPublic) {
- String profileName = getProfileName(dexInfo.splitName());
- return AidlUtils.buildOutputProfileForPrimary(
- mPkgState.getPackageName(), profileName, Process.SYSTEM_UID, mSharedGid, isPublic);
+ return PrimaryDexUtils.buildOutputProfile(
+ mPkgState, dexInfo, Process.SYSTEM_UID, mSharedGid, isPublic);
}
@Override
@NonNull
protected List<ProfilePath> getCurProfiles(@NonNull DetailedPrimaryDexInfo dexInfo) {
- List<ProfilePath> profiles = new ArrayList<>();
- for (UserHandle handle :
- mInjector.getUserManager().getUserHandles(true /* excludeDying */)) {
- int userId = handle.getIdentifier();
- PackageUserState userState = mPkgState.getStateForUser(handle);
- if (userState.isInstalled()) {
- profiles.add(AidlUtils.buildProfilePathForPrimaryCur(
- userId, mPkgState.getPackageName(), getProfileName(dexInfo.splitName())));
- }
- }
- return profiles;
+ return PrimaryDexUtils.getCurProfiles(mInjector.getUserManager(), mPkgState, dexInfo);
}
@Override
@@ -221,9 +208,4 @@
|| !TextUtils.isEmpty(mPkg.getStaticSharedLibraryName())
|| !mPkg.getLibraryNames().isEmpty();
}
-
- @NonNull
- private String getProfileName(@Nullable String splitName) {
- return splitName == null ? "primary" : splitName + ".split";
- }
}
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
index 3ffaf54..7053b08 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexUtils.java
@@ -18,6 +18,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.text.TextUtils;
import com.android.internal.annotations.Immutable;
@@ -25,6 +27,7 @@
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.PackageUserState;
import com.android.server.pm.pkg.SharedLibrary;
import dalvik.system.DelegateLastClassLoader;
@@ -272,6 +275,41 @@
return pkg.isIsolatedSplitLoading() && pkg.getSplits().size() > 1;
}
+ @NonNull
+ public static ProfilePath buildRefProfilePath(
+ @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) {
+ String profileName = getProfileName(dexInfo.splitName());
+ return AidlUtils.buildProfilePathForPrimaryRef(pkgState.getPackageName(), profileName);
+ }
+
+ @NonNull
+ public static OutputProfile buildOutputProfile(@NonNull PackageState pkgState,
+ @NonNull PrimaryDexInfo dexInfo, int uid, int gid, boolean isPublic) {
+ String profileName = getProfileName(dexInfo.splitName());
+ return AidlUtils.buildOutputProfileForPrimary(
+ pkgState.getPackageName(), profileName, uid, gid, isPublic);
+ }
+
+ @NonNull
+ public static List<ProfilePath> getCurProfiles(@NonNull UserManager userManager,
+ @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) {
+ List<ProfilePath> profiles = new ArrayList<>();
+ for (UserHandle handle : userManager.getUserHandles(true /* excludeDying */)) {
+ int userId = handle.getIdentifier();
+ PackageUserState userState = pkgState.getStateForUser(handle);
+ if (userState.isInstalled()) {
+ profiles.add(AidlUtils.buildProfilePathForPrimaryCur(
+ userId, pkgState.getPackageName(), getProfileName(dexInfo.splitName())));
+ }
+ }
+ return profiles;
+ }
+
+ @NonNull
+ private static String getProfileName(@Nullable String splitName) {
+ return splitName == null ? "primary" : splitName + ".split";
+ }
+
/** Basic information about a primary dex file (either the base APK or a split APK). */
@Immutable
public static class PrimaryDexInfo {
diff --git a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
index f0aa7e5..85a7b87 100644
--- a/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
+++ b/libartservice/service/javatests/com/android/server/art/ArtManagerLocalTest.java
@@ -16,6 +16,8 @@
package com.android.server.art;
+import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
+
import static com.android.server.art.model.OptimizationStatus.DexContainerFileOptimizationStatus;
import static com.android.server.art.testing.TestingUtils.deepEq;
@@ -25,10 +27,13 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.argThat;
+import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -36,8 +41,12 @@
import android.apphibernation.AppHibernationManager;
import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.os.UserManager;
import androidx.test.filters.SmallTest;
@@ -51,6 +60,7 @@
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.AndroidPackageSplit;
import com.android.server.pm.pkg.PackageState;
+import com.android.server.pm.pkg.PackageUserState;
import org.junit.Before;
import org.junit.Rule;
@@ -61,6 +71,11 @@
import org.junit.runners.Parameterized.Parameters;
import org.mockito.Mock;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@@ -82,6 +97,7 @@
@Mock private IArtd mArtd;
@Mock private DexOptHelper mDexOptHelper;
@Mock private AppHibernationManager mAppHibernationManager;
+ @Mock private UserManager mUserManager;
private PackageState mPkgState;
private AndroidPackage mPkg;
private Config mConfig;
@@ -108,6 +124,7 @@
lenient().when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper);
lenient().when(mInjector.getConfig()).thenReturn(mConfig);
lenient().when(mInjector.getAppHibernationManager()).thenReturn(mAppHibernationManager);
+ lenient().when(mInjector.getUserManager()).thenReturn(mUserManager);
lenient().when(SystemProperties.get(eq("pm.dexopt.install"))).thenReturn("speed-profile");
lenient().when(SystemProperties.get(eq("pm.dexopt.bg-dexopt"))).thenReturn("speed-profile");
@@ -128,6 +145,10 @@
lenient().when(mAppHibernationManager.isHibernatingGlobally(any())).thenReturn(false);
lenient().when(mAppHibernationManager.isOatArtifactDeletionEnabled()).thenReturn(true);
+ lenient()
+ .when(mUserManager.getUserHandles(anyBoolean()))
+ .thenReturn(List.of(UserHandle.of(0), UserHandle.of(1)));
+
lenient().when(mPackageManagerLocal.withFilteredSnapshot()).thenReturn(mSnapshot);
List<PackageState> pkgStates = createPackageStates();
for (PackageState pkgState : pkgStates) {
@@ -387,6 +408,146 @@
mArtManagerLocal.optimizePackages(mSnapshot, "bg-dexopt", cancellationSignal);
}
+ @Test
+ public void testSnapshotAppProfile() throws Exception {
+ var options = new MergeProfileOptions();
+ options.forceMerge = true;
+ options.forBootImage = false;
+
+ File tempFile = File.createTempFile("primary", ".prof");
+ tempFile.deleteOnExit();
+
+ when(mArtd.mergeProfiles(
+ deepEq(List.of(AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME, "primary"))),
+ isNull(),
+ deepEq(AidlUtils.buildOutputProfileForPrimary(PKG_NAME, "primary",
+ Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */)),
+ deepEq(List.of("/data/app/foo/base.apk")), deepEq(options)))
+ .thenAnswer(invocation -> {
+ try (var writer = new FileWriter(tempFile)) {
+ writer.write("snapshot");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ var output = invocation.<OutputProfile>getArgument(2);
+ output.profilePath.tmpPath = tempFile.getPath();
+ return true;
+ });
+
+ ParcelFileDescriptor fd =
+ mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME, null /* splitName */);
+
+ verify(mArtd).deleteProfile(
+ argThat(profile -> profile.getTmpProfilePath().tmpPath.equals(tempFile.getPath())));
+
+ try (InputStream inputStream = new AutoCloseInputStream(fd)) {
+ String contents = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ assertThat(contents).isEqualTo("snapshot");
+ }
+ }
+
+ @Test
+ public void testSnapshotAppProfileSplit() throws Exception {
+ when(mArtd.mergeProfiles(deepEq(List.of(AidlUtils.buildProfilePathForPrimaryRef(
+ PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME, "split_0.split"))),
+ isNull(),
+ deepEq(AidlUtils.buildOutputProfileForPrimary(PKG_NAME, "split_0.split",
+ Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */)),
+ deepEq(List.of("/data/app/foo/split_0.apk")), any()))
+ .thenReturn(false);
+
+ mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME, "split_0");
+ }
+
+ @Test
+ public void testSnapshotAppProfileEmpty() throws Exception {
+ when(mArtd.mergeProfiles(any(), any(), any(), any(), any())).thenReturn(false);
+
+ ParcelFileDescriptor fd =
+ mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME, null /* splitName */);
+
+ verify(mArtd, never()).deleteProfile(any());
+
+ try (InputStream inputStream = new AutoCloseInputStream(fd)) {
+ assertThat(inputStream.readAllBytes()).isEmpty();
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSnapshotAppProfilePackageNotFound() throws Exception {
+ when(mSnapshot.getPackageState(anyString())).thenReturn(null);
+
+ mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME, null /* splitName */);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSnapshotAppProfileNoPackage() throws Exception {
+ when(mPkgState.getAndroidPackage()).thenReturn(null);
+
+ mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME, null /* splitName */);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSnapshotAppProfileSplitNotFound() throws Exception {
+ mArtManagerLocal.snapshotAppProfile(mSnapshot, PKG_NAME, "non-existent-split");
+ }
+
+ @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");
+ lenient().when(Constants.getenv("SYSTEMSERVERCLASSPATH")).thenReturn("sscp0:sscp1");
+ lenient().when(Constants.getenv("STANDALONE_SYSTEMSERVER_JARS")).thenReturn("sssj0:sssj1");
+
+ var options = new MergeProfileOptions();
+ options.forceMerge = true;
+ options.forBootImage = true;
+
+ when(mArtd.mergeProfiles(
+ deepEq(List.of(AidlUtils.buildProfilePathForPrimaryRef("android", "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, "android", "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, "android", "primary"),
+ AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME, "split_0.split"),
+ AidlUtils.buildProfilePathForPrimaryRef(PKG_NAME_SYS_UI, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME_SYS_UI, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME_SYS_UI, "primary"),
+ AidlUtils.buildProfilePathForPrimaryRef(
+ PKG_NAME_HIBERNATING, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 0 /* userId */, PKG_NAME_HIBERNATING, "primary"),
+ AidlUtils.buildProfilePathForPrimaryCur(
+ 1 /* userId */, PKG_NAME_HIBERNATING, "primary"))),
+ isNull(),
+ deepEq(AidlUtils.buildOutputProfileForPrimary("android", "primary",
+ Process.SYSTEM_UID, Process.SYSTEM_UID, false /* isPublic */)),
+ deepEq(List.of("bcp0", "bcp1", "sscp0", "sscp1", "sssj0", "sssj1")),
+ deepEq(options)))
+ .thenReturn(false); // A non-empty merge is tested in `testSnapshotAppProfile`.
+
+ mArtManagerLocal.snapshotBootImageProfile(mSnapshot);
+ }
+
private AndroidPackage createPackage(boolean multiSplit) {
AndroidPackage pkg = mock(AndroidPackage.class);
@@ -413,6 +574,12 @@
return pkg;
}
+ private PackageUserState createPackageUserState() {
+ PackageUserState pkgUserState = mock(PackageUserState.class);
+ lenient().when(pkgUserState.isInstalled()).thenReturn(true);
+ return pkgUserState;
+ }
+
private PackageState createPackageState(
String packageName, int appId, boolean hasPackage, boolean multiSplit) {
PackageState pkgState = mock(PackageState.class);
@@ -431,6 +598,9 @@
lenient().when(pkgState.getAndroidPackage()).thenReturn(null);
}
+ PackageUserState pkgUserState = createPackageUserState();
+ lenient().when(pkgState.getStateForUser(any())).thenReturn(pkgUserState);
+
return pkgState;
}
@@ -441,7 +611,8 @@
PackageState sysUiPkgState = createPackageState(
PKG_NAME_SYS_UI, 1234 /* appId */, true /* hasPackage */, false /* multiSplit */);
- // This should not be optimized because it's hibernating.
+ // This should not be optimized because it's hibernating. However, it should be included
+ // when snapshotting boot image profile.
PackageState pkgHibernatingState = createPackageState(PKG_NAME_HIBERNATING,
10002 /* appId */, true /* hasPackage */, false /* multiSplit */);
lenient()
diff --git a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
index 7fa2dfc..2dc4b5b 100644
--- a/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/DexUseManagerTest.java
@@ -230,6 +230,7 @@
if (saveAndLoad) {
File tempFile = File.createTempFile("dex-use", ".pb");
+ tempFile.deleteOnExit();
mDexUseManager.save(tempFile.getPath());
mDexUseManager.clear();
mDexUseManager.load(tempFile.getPath());
@@ -363,6 +364,7 @@
if (saveAndLoad) {
File tempFile = File.createTempFile("dex-use", ".pb");
+ tempFile.deleteOnExit();
mDexUseManager.save(tempFile.getPath());
mDexUseManager.clear();
mDexUseManager.load(tempFile.getPath());
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
index 5347776..5b98cad 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
@@ -92,6 +92,8 @@
| DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE
| DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE;
+ private final MergeProfileOptions mMergeProfileOptions = new MergeProfileOptions();
+
private final DexoptResult mDexoptResult = createDexoptResult(false /* cancelled */);
private PrimaryDexOptimizer mPrimaryDexOptimizer;
@@ -282,7 +284,7 @@
when(mPkgState.getStateForUser(eq(UserHandle.of(0)))).thenReturn(mPkgUserStateInstalled);
when(mPkgState.getStateForUser(eq(UserHandle.of(2)))).thenReturn(mPkgUserStateInstalled);
- when(mArtd.mergeProfiles(any(), any(), any(), any())).thenReturn(true);
+ when(mArtd.mergeProfiles(any(), any(), any(), any(), any())).thenReturn(true);
makeProfileUsable(mRefProfile);
when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
@@ -297,7 +299,8 @@
0 /* userId */, PKG_NAME, "primary"),
AidlUtils.buildProfilePathForPrimaryCur(
2 /* userId */, PKG_NAME, "primary"))),
- deepEq(mRefProfile), deepEq(mPrivateOutputProfile), eq(mDexPath));
+ deepEq(mRefProfile), deepEq(mPrivateOutputProfile), deepEq(List.of(mDexPath)),
+ deepEq(mMergeProfileOptions));
// It should use `mBetterOrSameDexoptTrigger` and the merged profile for both ISAs.
inOrder.verify(mArtd).getDexoptNeeded(eq(mDexPath), eq("arm64"), any(), eq("speed-profile"),
@@ -325,7 +328,7 @@
when(mPkgState.getStateForUser(eq(UserHandle.of(0)))).thenReturn(mPkgUserStateInstalled);
when(mPkgState.getStateForUser(eq(UserHandle.of(2)))).thenReturn(mPkgUserStateInstalled);
- when(mArtd.mergeProfiles(any(), any(), any(), any())).thenReturn(false);
+ when(mArtd.mergeProfiles(any(), any(), any(), any(), any())).thenReturn(false);
makeProfileUsable(mRefProfile);
when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
diff --git a/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java
index 250ea8e..e6e62b9 100644
--- a/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/SecondaryDexOptimizerTest.java
@@ -95,6 +95,8 @@
| DexoptTrigger.COMPILER_FILTER_IS_SAME
| DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ private final MergeProfileOptions mMergeProfileOptions = new MergeProfileOptions();
+
@Rule
public StaticMockitoRule mockitoRule =
new StaticMockitoRule(SystemProperties.class, Constants.class);
@@ -182,7 +184,8 @@
// It should use profile for dex 1.
verify(mArtd).mergeProfiles(deepEq(List.of(mDex1CurProfile)), deepEq(mDex1RefProfile),
- deepEq(mDex1PrivateOutputProfile), eq(DEX_1));
+ deepEq(mDex1PrivateOutputProfile), deepEq(List.of(DEX_1)),
+ deepEq(mMergeProfileOptions));
verify(mArtd).getDexoptNeeded(
eq(DEX_1), eq("arm64"), any(), eq("speed-profile"), eq(mBetterOrSameDexoptTrigger));
@@ -196,7 +199,7 @@
// It should use "speed" for dex 2 for both ISAs and make the artifacts public.
verify(mArtd, never()).isProfileUsable(deepEq(mDex2RefProfile), any());
- verify(mArtd, never()).mergeProfiles(any(), deepEq(mDex2RefProfile), any(), any());
+ verify(mArtd, never()).mergeProfiles(any(), deepEq(mDex2RefProfile), any(), any(), any());
verify(mArtd).getDexoptNeeded(
eq(DEX_2), eq("arm64"), any(), eq("speed"), eq(mDefaultDexoptTrigger));
@@ -211,7 +214,7 @@
// It should use "verify" for dex 3 and make the artifacts private.
verify(mArtd, never()).isProfileUsable(deepEq(mDex3RefProfile), any());
- verify(mArtd, never()).mergeProfiles(any(), deepEq(mDex3RefProfile), any(), any());
+ verify(mArtd, never()).mergeProfiles(any(), deepEq(mDex3RefProfile), any(), any(), any());
verify(mArtd).getDexoptNeeded(
eq(DEX_3), eq("arm64"), isNull(), eq("verify"), eq(mDefaultDexoptTrigger));
@@ -295,7 +298,7 @@
.when(mArtd.getProfileVisibility(deepEq(mDex3RefProfile)))
.thenReturn(FileVisibility.NOT_OTHER_READABLE);
- lenient().when(mArtd.mergeProfiles(any(), any(), any(), any())).thenReturn(true);
+ lenient().when(mArtd.mergeProfiles(any(), any(), any(), any(), any())).thenReturn(true);
}
private GetDexoptNeededResult dexoptIsNeeded() {