Support profile merging.
Bug: 248318911
Test: m test-art-host-gtest-art_artd_tests
Test: atest ArtServiceTests
Ignore-AOSP-First: ART Services.
Change-Id: I0a6cb02ae31593f68648500e838882236881d349
diff --git a/artd/artd.cc b/artd/artd.cc
index 20dbe16..bf2f7b3 100644
--- a/artd/artd.cc
+++ b/artd/artd.cc
@@ -16,6 +16,7 @@
#include "artd.h"
+#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
@@ -90,9 +91,11 @@
using ::android::base::Error;
using ::android::base::Join;
using ::android::base::make_scope_guard;
+using ::android::base::ReadFileToString;
using ::android::base::Result;
using ::android::base::Split;
using ::android::base::StringReplace;
+using ::android::base::WriteStringToFd;
using ::art::tools::CmdlineBuilder;
using ::ndk::ScopedAStatus;
@@ -287,6 +290,24 @@
return static_cast<ArtdCancellationSignal*>(input);
}
+Result<void> CopyFile(const std::string& src_path, const NewFile& dst_file) {
+ std::string content;
+ if (!ReadFileToString(src_path, &content)) {
+ return Errorf("Failed to read file '{}': {}", src_path, strerror(errno));
+ }
+ if (!WriteStringToFd(content, dst_file.Fd())) {
+ return Errorf("Failed to write file '{}': {}", dst_file.TempPath(), strerror(errno));
+ }
+ if (fsync(dst_file.Fd()) != 0) {
+ return Errorf("Failed to flush file '{}': {}", dst_file.TempPath(), strerror(errno));
+ }
+ if (lseek(dst_file.Fd(), /*offset=*/0, SEEK_SET) != 0) {
+ return Errorf(
+ "Failed to reset the offset for file '{}': {}", dst_file.TempPath(), strerror(errno));
+ }
+ return {};
+}
+
class FdLogger {
public:
void Add(const NewFile& file) { fd_mapping_.emplace_back(file.Fd(), file.TempPath()); }
@@ -403,14 +424,16 @@
args.Add("--apk-fd=%d", dex_file->Fd());
fd_logger.Add(*dex_file);
- LOG(DEBUG) << "Running profman: " << Join(args.Get(), /*separator=*/" ")
- << "\nOpened FDs: " << fd_logger;
+ LOG(INFO) << "Running profman: " << Join(args.Get(), /*separator=*/" ")
+ << "\nOpened FDs: " << fd_logger;
Result<int> result = ExecAndReturnCode(args.Get(), kShortTimeoutSec);
if (!result.ok()) {
return NonFatal("Failed to run profman: " + result.error().message());
}
+ LOG(INFO) << "profman returned code {}"_format(result.value());
+
if (result.value() != ProfmanResult::kSkipCompilationSmallDelta &&
result.value() != ProfmanResult::kSkipCompilationEmptyProfiles) {
return NonFatal("profman returned an unexpected code: {}"_format(result.value()));
@@ -456,14 +479,16 @@
args.Add("--reference-profile-file-fd=%d", dst->Fd());
fd_logger.Add(*dst);
- LOG(DEBUG) << "Running profman: " << Join(args.Get(), /*separator=*/" ")
- << "\nOpened FDs: " << fd_logger;
+ LOG(INFO) << "Running profman: " << Join(args.Get(), /*separator=*/" ")
+ << "\nOpened FDs: " << fd_logger;
Result<int> result = ExecAndReturnCode(args.Get(), kShortTimeoutSec);
if (!result.ok()) {
return NonFatal("Failed to run profman: " + result.error().message());
}
+ LOG(INFO) << "profman returned code {}"_format(result.value());
+
if (result.value() == ProfmanResult::kCopyAndUpdateNoMatch) {
*_aidl_return = false;
return ScopedAStatus::ok();
@@ -497,7 +522,7 @@
std::string profile_path = OR_RETURN_FATAL(BuildProfileOrDmPath(in_profile));
std::error_code ec;
- if (!std::filesystem::remove(profile_path, ec)) {
+ if (!std::filesystem::remove(profile_path, ec) && ec.value() != ENOENT) {
LOG(ERROR) << "Failed to remove '{}': {}"_format(profile_path, ec.message());
}
@@ -518,6 +543,103 @@
return ScopedAStatus::ok();
}
+ndk::ScopedAStatus Artd::mergeProfiles(const std::vector<ProfilePath>& in_profiles,
+ const std::optional<ProfilePath>& in_referenceProfile,
+ OutputProfile* in_outputProfile,
+ const std::string& in_dexFile,
+ bool* _aidl_return) {
+ std::vector<std::string> profile_paths;
+ for (const ProfilePath& profile : in_profiles) {
+ std::string profile_path = OR_RETURN_FATAL(BuildProfileOrDmPath(profile));
+ if (profile.getTag() == ProfilePath::dexMetadataPath) {
+ return Fatal("Does not support DM file, got '{}'"_format(profile_path));
+ }
+ profile_paths.push_back(std::move(profile_path));
+ }
+ std::string output_profile_path =
+ OR_RETURN_FATAL(BuildRefProfilePath(in_outputProfile->profilePath.refProfilePath));
+ OR_RETURN_FATAL(ValidateDexPath(in_dexFile));
+
+ CmdlineBuilder args;
+ FdLogger fd_logger;
+ args.Add(OR_RETURN_FATAL(GetArtExec()))
+ .Add("--drop-capabilities")
+ .Add("--")
+ .Add(OR_RETURN_FATAL(GetProfman()));
+
+ std::vector<std::unique_ptr<File>> profile_files;
+ for (const std::string& profile_path : profile_paths) {
+ Result<std::unique_ptr<File>> profile_file = OpenFileForReading(profile_path);
+ if (!profile_file.ok()) {
+ if (profile_file.error().code() == ENOENT) {
+ // Skip non-existing file.
+ continue;
+ }
+ return NonFatal(
+ "Failed to open profile '{}': {}"_format(profile_path, profile_file.error().message()));
+ }
+ args.Add("--profile-file-fd=%d", profile_file.value()->Fd());
+ fd_logger.Add(*profile_file.value());
+ profile_files.push_back(std::move(profile_file.value()));
+ }
+
+ if (profile_files.empty()) {
+ LOG(INFO) << "Merge skipped because there are no existing profiles";
+ *_aidl_return = false;
+ return ScopedAStatus::ok();
+ }
+
+ std::unique_ptr<NewFile> output_profile_file =
+ OR_RETURN_NON_FATAL(NewFile::Create(output_profile_path, in_outputProfile->fsPermission));
+
+ if (in_referenceProfile.has_value()) {
+ std::string reference_profile_path =
+ OR_RETURN_FATAL(BuildProfileOrDmPath(*in_referenceProfile));
+ if (in_referenceProfile->getTag() == ProfilePath::dexMetadataPath) {
+ return Fatal("Does not support DM file, got '{}'"_format(reference_profile_path));
+ }
+ 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());
+ 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);
+
+ 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"));
+
+ LOG(INFO) << "Running profman: " << Join(args.Get(), /*separator=*/" ")
+ << "\nOpened FDs: " << fd_logger;
+
+ Result<int> result = ExecAndReturnCode(args.Get(), kShortTimeoutSec);
+ if (!result.ok()) {
+ return NonFatal("Failed to run profman: " + result.error().message());
+ }
+
+ LOG(INFO) << "profman returned code {}"_format(result.value());
+
+ if (result.value() == ProfmanResult::kSkipCompilationSmallDelta ||
+ result.value() == ProfmanResult::kSkipCompilationEmptyProfiles) {
+ *_aidl_return = false;
+ return ScopedAStatus::ok();
+ }
+
+ if (result.value() != ProfmanResult::kCompile) {
+ 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();
+ return ScopedAStatus::ok();
+}
+
ndk::ScopedAStatus Artd::getDexoptNeeded(const std::string& in_dexFile,
const std::string& in_instructionSet,
const std::string& in_classLoaderContext,
@@ -709,6 +831,9 @@
}
return NonFatal("Failed to run dex2oat: " + result.error().message());
}
+
+ LOG(INFO) << "dex2oat returned code {}"_format(result.value());
+
if (result.value() != 0) {
return NonFatal("dex2oat returned an unexpected code: %d"_format(result.value()));
}
diff --git a/artd/artd.h b/artd/artd.h
index dd4b022..9c65ed2 100644
--- a/artd/artd.h
+++ b/artd/artd.h
@@ -104,6 +104,13 @@
const aidl::com::android::server::art::ProfilePath& in_profile,
aidl::com::android::server::art::FileVisibility* _aidl_return) override;
+ ndk::ScopedAStatus mergeProfiles(
+ 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,
+ bool* _aidl_return) override;
+
ndk::ScopedAStatus getArtifactsVisibility(
const aidl::com::android::server::art::ArtifactsPath& in_artifactsPath,
aidl::com::android::server::art::FileVisibility* _aidl_return) override;
diff --git a/artd/artd_test.cc b/artd/artd_test.cc
index 63d13c2..f6b24a4 100644
--- a/artd/artd_test.cc
+++ b/artd/artd_test.cc
@@ -16,7 +16,9 @@
#include "artd.h"
+#include <fcntl.h>
#include <sys/types.h>
+#include <unistd.h>
#include <algorithm>
#include <chrono>
@@ -70,6 +72,7 @@
using ::android::base::Error;
using ::android::base::make_scope_guard;
using ::android::base::ParseInt;
+using ::android::base::ReadFdToString;
using ::android::base::ReadFileToString;
using ::android::base::Result;
using ::android::base::ScopeGuard;
@@ -94,6 +97,7 @@
using ::testing::SetArgPointee;
using ::testing::WithArg;
+using CurProfilePath = ProfilePath::CurProfilePath;
using RefProfilePath = ProfilePath::RefProfilePath;
using TmpRefProfilePath = ProfilePath::TmpRefProfilePath;
@@ -112,13 +116,21 @@
EXPECT_EQ(actual_content, expected_content);
}
-// Writes `content` to the FD specified by the `flag`.
-ACTION_P(WriteToFdFlag, flag, content) {
- for (const std::string& arg : arg0) {
+void WriteToFdFlagImpl(const std::vector<std::string> args,
+ const std::string& flag,
+ const std::string& content,
+ bool assume_empty) {
+ for (const std::string& arg : args) {
std::string_view value(arg);
if (android::base::ConsumePrefix(&value, flag)) {
int fd;
ASSERT_TRUE(ParseInt(std::string(value), &fd));
+ if (assume_empty) {
+ ASSERT_EQ(lseek(fd, /*offset=*/0, SEEK_CUR), 0);
+ } else {
+ ASSERT_EQ(ftruncate(fd, /*length=*/0), 0);
+ ASSERT_EQ(lseek(fd, /*offset=*/0, SEEK_SET), 0);
+ }
ASSERT_TRUE(WriteStringToFd(content, fd));
return;
}
@@ -126,6 +138,16 @@
FAIL() << "Flag '{}' not found"_format(flag);
}
+// Writes `content` to the FD specified by the `flag`.
+ACTION_P(WriteToFdFlag, flag, content) {
+ WriteToFdFlagImpl(arg0, flag, content, /*assume_empty=*/true);
+}
+
+// Clears any existing content and writes `content` to the FD specified by the `flag`.
+ACTION_P(ClearAndWriteToFdFlag, flag, content) {
+ WriteToFdFlagImpl(arg0, flag, content, /*assume_empty=*/false);
+}
+
// Matches a flag that starts with `flag` and whose value matches `matcher`.
MATCHER_P2(Flag, flag, matcher, "") {
std::string_view value(arg);
@@ -159,6 +181,19 @@
return ExplainMatchResult(matcher, std::string(path, static_cast<size_t>(len)), result_listener);
}
+// Matches an FD of a file whose content matches `matcher`.
+MATCHER_P(FdHasContent, matcher, "") {
+ int fd;
+ if (!ParseInt(arg, &fd)) {
+ return false;
+ }
+ std::string actual_content;
+ if (!ReadFdToString(fd, &actual_content)) {
+ return false;
+ }
+ return ExplainMatchResult(matcher, actual_content, result_listener);
+}
+
// Matches a container that, when split by `separator`, the first part matches `head_matcher`, and
// the second part matches `tail_matcher`.
MATCHER_P3(WhenSplitBy, separator, head_matcher, tail_matcher, "") {
@@ -259,10 +294,14 @@
clc_2_ = GetTestDexFileName("Nested");
class_loader_context_ = "PCL[{}:{}]"_format(clc_1_, clc_2_);
compiler_filter_ = "speed";
- profile_path_ =
- TmpRefProfilePath{.refProfilePath = RefProfilePath{.packageName = "com.android.foo",
- .profileName = "primary"},
- .id = "12345"};
+ TmpRefProfilePath tmp_ref_profile_path{
+ .refProfilePath =
+ RefProfilePath{.packageName = "com.android.foo", .profileName = "primary"},
+ .id = "12345"};
+ profile_path_ = tmp_ref_profile_path;
+ std::filesystem::create_directories(
+ std::filesystem::path(OR_FATAL(BuildTmpRefProfilePath(tmp_ref_profile_path)))
+ .parent_path());
}
void TearDown() override {
@@ -298,7 +337,7 @@
void CreateFile(const std::string& filename, const std::string& content = "") {
std::filesystem::path path(filename);
std::filesystem::create_directories(path.parent_path());
- WriteStringToFile(content, filename);
+ ASSERT_TRUE(WriteStringToFile(content, filename));
}
std::shared_ptr<Artd> artd_;
@@ -351,9 +390,9 @@
TEST_F(ArtdTest, deleteArtifacts) {
std::string oat_dir = scratch_path_ + "/a/oat/arm64";
std::filesystem::create_directories(oat_dir);
- WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes.
- WriteStringToFile("ab", oat_dir + "/b.vdex"); // 2 bytes.
- WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte.
+ ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/b.odex")); // 4 bytes.
+ ASSERT_TRUE(WriteStringToFile("ab", oat_dir + "/b.vdex")); // 2 bytes.
+ ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/b.art")); // 1 byte.
int64_t result = -1;
EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
@@ -368,8 +407,8 @@
// Missing VDEX file.
std::string oat_dir = android_data_ + "/dalvik-cache/arm64";
std::filesystem::create_directories(oat_dir);
- WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex"); // 4 bytes.
- WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art"); // 1 byte.
+ ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex")); // 4 bytes.
+ ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art")); // 1 byte.
auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);
@@ -402,9 +441,9 @@
TEST_F(ArtdTest, deleteArtifactsPermissionDenied) {
std::string oat_dir = scratch_path_ + "/a/oat/arm64";
std::filesystem::create_directories(oat_dir);
- WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes.
- WriteStringToFile("ab", oat_dir + "/b.vdex"); // 2 bytes.
- WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte.
+ ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/b.odex")); // 4 bytes.
+ ASSERT_TRUE(WriteStringToFile("ab", oat_dir + "/b.vdex")); // 2 bytes.
+ ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/b.art")); // 1 byte.
auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3);
@@ -422,8 +461,8 @@
std::string oat_dir = scratch_path_ + "/a/oat/arm64";
std::filesystem::create_directories(oat_dir);
std::filesystem::create_directories(oat_dir + "/b.vdex");
- WriteStringToFile("abcd", oat_dir + "/b.odex"); // 4 bytes.
- WriteStringToFile("a", oat_dir + "/b.art"); // 1 byte.
+ ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/b.odex")); // 4 bytes.
+ ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/b.art")); // 1 byte.
auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
EXPECT_CALL(mock_logger_,
@@ -1092,6 +1131,11 @@
EXPECT_FALSE(std::filesystem::exists(profile_file));
}
+TEST_F(ArtdTest, deleteProfileDoesNotExist) {
+ std::string profile_file = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
+ EXPECT_TRUE(artd_->deleteProfile(profile_path_.value()).isOk());
+}
+
TEST_F(ArtdTest, deleteProfileFailed) {
auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
EXPECT_CALL(
@@ -1186,6 +1230,123 @@
EXPECT_THAT(status.getMessage(), ContainsRegex(R"re(Failed to get status of .*b\.odex)re"));
}
+TEST_F(ArtdTest, mergeProfiles) {
+ const TmpRefProfilePath& reference_profile_path =
+ profile_path_->get<ProfilePath::tmpRefProfilePath>();
+ std::string reference_profile_file = OR_FATAL(BuildTmpRefProfilePath(reference_profile_path));
+ CreateFile(reference_profile_file, "abc");
+
+ // Doesn't exist.
+ CurProfilePath profile_0_path{
+ .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_0_file = OR_FATAL(BuildCurProfilePath(profile_0_path));
+
+ CurProfilePath profile_1_path{
+ .userId = 1, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_1_file = OR_FATAL(BuildCurProfilePath(profile_1_path));
+ CreateFile(profile_1_file, "def");
+
+ OutputProfile output_profile{.profilePath = reference_profile_path,
+ .fsPermission = FsPermission{.uid = -1, .gid = -1}};
+ output_profile.profilePath.id = "";
+
+ CreateFile(dex_file_);
+
+ EXPECT_CALL(
+ *mock_exec_utils_,
+ DoExecAndReturnCode(
+ WhenSplitBy("--",
+ AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
+ AllOf(Contains(art_root_ + "/bin/profman"),
+ 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_))))),
+ _,
+ _))
+ .WillOnce(DoAll(WithArg<0>(ClearAndWriteToFdFlag("--reference-profile-file-fd=", "merged")),
+ Return(ProfmanResult::kCompile)));
+
+ bool result;
+ EXPECT_TRUE(artd_
+ ->mergeProfiles({profile_0_path, profile_1_path},
+ reference_profile_path,
+ &output_profile,
+ dex_file_,
+ &result)
+ .isOk());
+ EXPECT_TRUE(result);
+ EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
+ CheckContent(OR_FATAL(BuildTmpRefProfilePath(output_profile.profilePath)), "merged");
+}
+
+TEST_F(ArtdTest, mergeProfilesEmptyReferenceProfile) {
+ CurProfilePath profile_0_path{
+ .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_0_file = OR_FATAL(BuildCurProfilePath(profile_0_path));
+ CreateFile(profile_0_file, "def");
+
+ OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpRefProfilePath>(),
+ .fsPermission = FsPermission{.uid = -1, .gid = -1}};
+ output_profile.profilePath.id = "";
+
+ CreateFile(dex_file_);
+
+ EXPECT_CALL(
+ *mock_exec_utils_,
+ DoExecAndReturnCode(
+ WhenSplitBy("--",
+ AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
+ AllOf(Contains(art_root_ + "/bin/profman"),
+ Contains(Flag("--profile-file-fd=", FdOf(profile_0_file))),
+ Contains(Flag("--reference-profile-file-fd=", FdHasContent(""))),
+ Contains(Flag("--apk-fd=", FdOf(dex_file_))))),
+ _,
+ _))
+ .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--reference-profile-file-fd=", "merged")),
+ Return(ProfmanResult::kCompile)));
+
+ bool result;
+ EXPECT_TRUE(
+ artd_->mergeProfiles({profile_0_path}, std::nullopt, &output_profile, dex_file_, &result)
+ .isOk());
+ EXPECT_TRUE(result);
+ EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
+ CheckContent(OR_FATAL(BuildTmpRefProfilePath(output_profile.profilePath)), "merged");
+}
+
+TEST_F(ArtdTest, mergeProfilesProfilesDontExist) {
+ const TmpRefProfilePath& reference_profile_path =
+ profile_path_->get<ProfilePath::tmpRefProfilePath>();
+ std::string reference_profile_file = OR_FATAL(BuildTmpRefProfilePath(reference_profile_path));
+ CreateFile(reference_profile_file, "abc");
+
+ // Doesn't exist.
+ CurProfilePath profile_0_path{
+ .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_0_file = OR_FATAL(BuildCurProfilePath(profile_0_path));
+
+ // Doesn't exist.
+ CurProfilePath profile_1_path{
+ .userId = 1, .packageName = "com.android.foo", .profileName = "primary"};
+ std::string profile_1_file = OR_FATAL(BuildCurProfilePath(profile_1_path));
+
+ OutputProfile output_profile{.profilePath = reference_profile_path,
+ .fsPermission = FsPermission{.uid = -1, .gid = -1}};
+ output_profile.profilePath.id = "";
+
+ 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_FALSE(result);
+ EXPECT_THAT(output_profile.profilePath.id, IsEmpty());
+}
+
} // 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 04de116..7bed2ff 100644
--- a/artd/binder/com/android/server/art/IArtd.aidl
+++ b/artd/binder/com/android/server/art/IArtd.aidl
@@ -63,7 +63,7 @@
void commitTmpProfile(in com.android.server.art.ProfilePath.TmpRefProfilePath profile);
/**
- * Deletes the profile.
+ * Deletes the profile. Does nothing of the profile doesn't exist.
*
* Operates on the whole DM file if given one.
*
@@ -82,6 +82,19 @@
in com.android.server.art.ProfilePath profile);
/**
+ * 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.
+ *
+ * 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);
+
+ /**
* Returns the visibility of the artifacts.
*
* Throws fatal and non-fatal errors.
diff --git a/artd/binder/com/android/server/art/ProfilePath.aidl b/artd/binder/com/android/server/art/ProfilePath.aidl
index e12d94a..afd0fd6 100644
--- a/artd/binder/com/android/server/art/ProfilePath.aidl
+++ b/artd/binder/com/android/server/art/ProfilePath.aidl
@@ -25,6 +25,7 @@
RefProfilePath refProfilePath;
TmpRefProfilePath tmpRefProfilePath;
PrebuiltProfilePath prebuiltProfilePath;
+ CurProfilePath curProfilePath;
/** Represents a profile in the dex metadata file. */
com.android.server.art.DexMetadataPath dexMetadataPath;
@@ -54,4 +55,14 @@
/** The path to the dex file that the profile is next to. */
@utf8InCpp String dexPath;
}
+
+ /** Represents a current profile. */
+ parcelable CurProfilePath {
+ /** The user ID of the user that owns the profile. */
+ int userId;
+ /** The name of the package. */
+ @utf8InCpp String packageName;
+ /** The stem of the profile file */
+ @utf8InCpp String profileName;
+ }
}
diff --git a/artd/path_utils.cc b/artd/path_utils.cc
index 0cebdfb..8314241 100644
--- a/artd/path_utils.cc
+++ b/artd/path_utils.cc
@@ -43,6 +43,7 @@
using ::fmt::literals::operator""_format; // NOLINT
+using CurProfilePath = ProfilePath::CurProfilePath;
using PrebuiltProfilePath = ProfilePath::PrebuiltProfilePath;
using RefProfilePath = ProfilePath::RefProfilePath;
using TmpRefProfilePath = ProfilePath::TmpRefProfilePath;
@@ -164,6 +165,15 @@
return prebuilt_profile_path.dexPath + ".prof";
}
+Result<std::string> BuildCurProfilePath(const CurProfilePath& cur_profile_path) {
+ OR_RETURN(ValidatePathElement(cur_profile_path.packageName, "packageName"));
+ OR_RETURN(ValidatePathElementSubstring(cur_profile_path.profileName, "profileName"));
+ return "{}/misc/profiles/cur/{}/{}/{}.prof"_format(OR_RETURN(GetAndroidDataOrError()),
+ cur_profile_path.userId,
+ cur_profile_path.packageName,
+ cur_profile_path.profileName);
+}
+
Result<std::string> BuildDexMetadataPath(const DexMetadataPath& dex_metadata_path) {
OR_RETURN(ValidateDexPath(dex_metadata_path.dexPath));
return ReplaceFileExtension(dex_metadata_path.dexPath, "dm");
@@ -182,6 +192,8 @@
return BuildTmpRefProfilePath(profile_path.get<ProfilePath::tmpRefProfilePath>());
case ProfilePath::prebuiltProfilePath:
return BuildPrebuiltProfilePath(profile_path.get<ProfilePath::prebuiltProfilePath>());
+ case ProfilePath::curProfilePath:
+ return BuildCurProfilePath(profile_path.get<ProfilePath::curProfilePath>());
case ProfilePath::dexMetadataPath:
return BuildDexMetadataPath(profile_path.get<ProfilePath::dexMetadataPath>());
// No default. All cases should be explicitly handled, or the compilation will fail.
diff --git a/artd/path_utils.h b/artd/path_utils.h
index f3a6359..5b2b1d3 100644
--- a/artd/path_utils.h
+++ b/artd/path_utils.h
@@ -51,6 +51,9 @@
android::base::Result<std::string> BuildPrebuiltProfilePath(
const aidl::com::android::server::art::ProfilePath::PrebuiltProfilePath& prebuilt_profile_path);
+android::base::Result<std::string> BuildCurProfilePath(
+ const aidl::com::android::server::art::ProfilePath::CurProfilePath& cur_profile_path);
+
android::base::Result<std::string> BuildDexMetadataPath(
const aidl::com::android::server::art::DexMetadataPath& dex_metadata_path);
diff --git a/artd/path_utils_test.cc b/artd/path_utils_test.cc
index 29a9cb8..8188000 100644
--- a/artd/path_utils_test.cc
+++ b/artd/path_utils_test.cc
@@ -33,6 +33,7 @@
using ::android::base::testing::HasValue;
using ::android::base::testing::WithMessage;
+using CurProfilePath = ProfilePath::CurProfilePath;
using PrebuiltProfilePath = ProfilePath::PrebuiltProfilePath;
using RefProfilePath = ProfilePath::RefProfilePath;
using TmpRefProfilePath = ProfilePath::TmpRefProfilePath;
@@ -186,6 +187,12 @@
HasValue("/a/b.apk.prof"));
}
+TEST_F(PathUtilsTest, BuildCurProfilePath) {
+ EXPECT_THAT(BuildCurProfilePath(CurProfilePath{
+ .userId = 1, .packageName = "com.android.foo", .profileName = "primary"}),
+ HasValue(android_data_ + "/misc/profiles/cur/1/com.android.foo/primary.prof"));
+}
+
TEST_F(PathUtilsTest, BuildDexMetadataPath) {
EXPECT_THAT(BuildDexMetadataPath(DexMetadataPath{.dexPath = "/a/b.apk"}), HasValue("/a/b.dm"));
}
@@ -207,6 +214,9 @@
HasValue(android_data_ + "/misc/profiles/ref/com.android.foo/primary.prof.12345.tmp"));
EXPECT_THAT(BuildProfileOrDmPath(PrebuiltProfilePath{.dexPath = "/a/b.apk"}),
HasValue("/a/b.apk.prof"));
+ EXPECT_THAT(BuildProfileOrDmPath(CurProfilePath{
+ .userId = 1, .packageName = "com.android.foo", .profileName = "primary"}),
+ HasValue(android_data_ + "/misc/profiles/cur/1/com.android.foo/primary.prof"));
EXPECT_THAT(BuildProfileOrDmPath(DexMetadataPath{.dexPath = "/a/b.apk"}), HasValue("/a/b.dm"));
}
diff --git a/libartservice/service/java/com/android/server/art/AidlUtils.java b/libartservice/service/java/com/android/server/art/AidlUtils.java
index b28e1ff..26d5df8 100644
--- a/libartservice/service/java/com/android/server/art/AidlUtils.java
+++ b/libartservice/service/java/com/android/server/art/AidlUtils.java
@@ -18,6 +18,7 @@
import static com.android.server.art.OutputArtifacts.PermissionSettings;
import static com.android.server.art.OutputArtifacts.PermissionSettings.SeContext;
+import static com.android.server.art.ProfilePath.CurProfilePath;
import static com.android.server.art.ProfilePath.PrebuiltProfilePath;
import static com.android.server.art.ProfilePath.RefProfilePath;
import static com.android.server.art.ProfilePath.TmpRefProfilePath;
@@ -109,6 +110,16 @@
}
@NonNull
+ public static ProfilePath buildProfilePathForCur(
+ int userId, @NonNull String packageName, @NonNull String profileName) {
+ var curProfilePath = new CurProfilePath();
+ curProfilePath.userId = userId;
+ curProfilePath.packageName = packageName;
+ curProfilePath.profileName = profileName;
+ return ProfilePath.curProfilePath(curProfilePath);
+ }
+
+ @NonNull
public static OutputProfile buildOutputProfile(@NonNull String packageName,
@NonNull String profileName, int uid, int gid, boolean isPublic) {
var outputProfile = new OutputProfile();
diff --git a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
index fa2af7a..cb7e9ff 100644
--- a/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
+++ b/libartservice/service/java/com/android/server/art/PrimaryDexOptimizer.java
@@ -36,6 +36,7 @@
import android.os.ServiceSpecificException;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -46,6 +47,7 @@
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.art.wrapper.PackageUserState;
import com.google.auto.value.AutoValue;
@@ -126,7 +128,17 @@
profile = pair.first;
isOtherReadable = pair.second;
}
- // TODO(jiakaiz): Merge profiles.
+ ProfilePath mergedProfile =
+ mergeProfiles(pkgState, dexInfo, uid, sharedGid, profile);
+ if (mergedProfile != null) {
+ if (profile != null
+ && profile.getTag() == ProfilePath.tmpRefProfilePath) {
+ mInjector.getArtd().deleteProfile(profile);
+ }
+ profile = mergedProfile;
+ isOtherReadable = false;
+ profileMerged = true;
+ }
}
if (profile == null) {
// A profile guided optimization with no profile is essentially 'verify',
@@ -140,10 +152,10 @@
}
boolean isProfileGuidedCompilerFilter =
DexFile.isProfileGuidedCompilerFilter(compilerFilter);
- assert isProfileGuidedCompilerFilter == (profile != null);
+ Utils.check(isProfileGuidedCompilerFilter == (profile != null));
boolean canBePublic = !isProfileGuidedCompilerFilter || isOtherReadable;
- assert Utils.implies(needsToBeShared, canBePublic);
+ Utils.check(Utils.implies(needsToBeShared, canBePublic));
PermissionSettings permissionSettings =
getPermissionSettings(sharedGid, canBePublic);
@@ -227,7 +239,18 @@
profile = null;
}
}
- // TODO(jiakaiz): If profileMerged is true, clear current profiles.
+ if (profileMerged) {
+ // Note that this is just an optimization, to reduce the amount of data that
+ // the runtime writes on every profile save. The profile merge result on the
+ // next run won't change regardless of whether the cleanup is done or not
+ // because profman only looks at the diff.
+ // A caveat is that it may delete more than what has been merged, if the
+ // runtime writes additional entries between the merge and the cleanup, but
+ // this is fine because the runtime writes all JITed classes and methods on
+ // every save and the additional entries will likely be written back on the
+ // next save.
+ cleanupCurProfiles(pkgState, dexInfo);
+ }
}
} finally {
if (profile != null && profile.getTag() == ProfilePath.tmpRefProfilePath) {
@@ -503,7 +526,8 @@
}
}
- boolean commitProfileChanges(@NonNull TmpRefProfilePath profile) throws RemoteException {
+ private boolean commitProfileChanges(@NonNull TmpRefProfilePath profile)
+ throws RemoteException {
try {
mInjector.getArtd().commitTmpProfile(profile);
return true;
@@ -518,6 +542,52 @@
}
}
+ @Nullable
+ private ProfilePath mergeProfiles(@NonNull PackageState pkgState,
+ @NonNull DetailedPrimaryDexInfo dexInfo, int uid, int gid,
+ @Nullable ProfilePath referenceProfile) throws RemoteException {
+ String profileName = getProfileName(dexInfo.splitName());
+ OutputProfile output = AidlUtils.buildOutputProfile(
+ pkgState.getPackageName(), profileName, uid, gid, false /* isPublic */);
+
+ try {
+ if (mInjector.getArtd().mergeProfiles(getCurProfiles(pkgState, dexInfo),
+ referenceProfile, output, dexInfo.dexPath())) {
+ return ProfilePath.tmpRefProfilePath(output.profilePath);
+ }
+ } catch (ServiceSpecificException e) {
+ Log.e(TAG,
+ String.format("Failed to merge profiles [packageName = %s, profileName = %s]",
+ pkgState.getPackageName(), getProfileName(dexInfo.splitName())),
+ e);
+ }
+
+ return null;
+ }
+
+ private void cleanupCurProfiles(@NonNull PackageState pkgState,
+ @NonNull DetailedPrimaryDexInfo dexInfo) throws RemoteException {
+ for (ProfilePath profile : getCurProfiles(pkgState, dexInfo)) {
+ mInjector.getArtd().deleteProfile(profile);
+ }
+ }
+
+ @NonNull
+ private List<ProfilePath> getCurProfiles(
+ @NonNull PackageState pkgState, @NonNull DetailedPrimaryDexInfo dexInfo) {
+ List<ProfilePath> profiles = new ArrayList<>();
+ for (UserHandle handle :
+ mInjector.getUserManager().getUserHandles(true /* excludeDying */)) {
+ int userId = handle.getIdentifier();
+ PackageUserState userState = pkgState.getUserStateOrDefault(userId);
+ if (userState.isInstalled()) {
+ profiles.add(AidlUtils.buildProfilePathForCur(
+ userId, pkgState.getPackageName(), getProfileName(dexInfo.splitName())));
+ }
+ }
+ return profiles;
+ }
+
@AutoValue
abstract static class DexoptTarget {
abstract @NonNull DetailedPrimaryDexInfo dexInfo();
@@ -581,6 +651,11 @@
}
@NonNull
+ UserManager getUserManager() {
+ return mContext.getSystemService(UserManager.class);
+ }
+
+ @NonNull
public IArtd getArtd() {
return Utils.getArtd();
}
diff --git a/libartservice/service/java/com/android/server/art/Utils.java b/libartservice/service/java/com/android/server/art/Utils.java
index 134dc9f..66c9704 100644
--- a/libartservice/service/java/com/android/server/art/Utils.java
+++ b/libartservice/service/java/com/android/server/art/Utils.java
@@ -107,6 +107,13 @@
return cond1 ? cond2 : true;
}
+ public static void check(boolean cond) {
+ // This cannot be replaced with `assert` because `assert` is not enabled in Android.
+ if (!cond) {
+ throw new IllegalStateException("Check failed");
+ }
+ }
+
@AutoValue
public abstract static class Abi {
static @NonNull Abi create(
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
index c591274..03c5f97 100644
--- a/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageState.java
@@ -141,4 +141,16 @@
throw new RuntimeException(e);
}
}
+
+ @NonNull
+ public PackageUserState getUserStateOrDefault(int userId) {
+ try {
+ Object userState = mPkgState.getClass()
+ .getMethod("getUserStateOrDefault", int.class)
+ .invoke(mPkgState, userId);
+ return userState != null ? new PackageUserState(userState) : null;
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/PackageUserState.java b/libartservice/service/java/com/android/server/art/wrapper/PackageUserState.java
new file mode 100644
index 0000000..b21db11
--- /dev/null
+++ b/libartservice/service/java/com/android/server/art/wrapper/PackageUserState.java
@@ -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.wrapper;
+
+import android.annotation.NonNull;
+
+/** @hide */
+public class PackageUserState {
+ private final Object mPkgUserState;
+
+ PackageUserState(@NonNull Object pkgUserState) {
+ mPkgUserState = pkgUserState;
+ }
+
+ public boolean isInstalled() {
+ try {
+ return (boolean) mPkgUserState.getClass()
+ .getMethod("isInstalled")
+ .invoke(mPkgUserState);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/libartservice/service/java/com/android/server/art/wrapper/README.md b/libartservice/service/java/com/android/server/art/wrapper/README.md
index 829fc1c..7d8c1f0 100644
--- a/libartservice/service/java/com/android/server/art/wrapper/README.md
+++ b/libartservice/service/java/com/android/server/art/wrapper/README.md
@@ -8,4 +8,5 @@
- `AndroidPackageApi`: `com.android.server.pm.pkg.AndroidPackageApi`
- `PackageManagerLocal`: `com.android.server.pm.PackageManagerLocal`
- `PackageState`: `com.android.server.pm.pkg.PackageState`
+- `PackageUserState`: `com.android.server.pm.pkg.PackageUserState`
- `SharedLibraryInfo`: `android.content.pm.SharedLibraryInfo`
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
index 7cce870..f4ff71d 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTest.java
@@ -81,6 +81,9 @@
private final int mDefaultDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
| DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
+ private final int mBetterOrSameDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
+ | DexoptTrigger.COMPILER_FILTER_IS_SAME
+ | DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE;
private final int mForceDexoptTrigger = DexoptTrigger.COMPILER_FILTER_IS_BETTER
| DexoptTrigger.PRIMARY_BOOT_IMAGE_BECOMES_USABLE
| DexoptTrigger.COMPILER_FILTER_IS_SAME | DexoptTrigger.COMPILER_FILTER_IS_WORSE;
@@ -239,6 +242,72 @@
}
@Test
+ public void testDexoptMergesProfiles() throws Exception {
+ when(mPkgState.getUserStateOrDefault(0 /* userId */)).thenReturn(mPkgUserStateInstalled);
+ when(mPkgState.getUserStateOrDefault(2 /* userId */)).thenReturn(mPkgUserStateInstalled);
+
+ when(mArtd.mergeProfiles(any(), any(), any(), any())).thenReturn(true);
+
+ makeProfileUsable(mRefProfile);
+ when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
+ .thenReturn(FileVisibility.OTHER_READABLE);
+
+ mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+
+ InOrder inOrder = inOrder(mArtd);
+
+ inOrder.verify(mArtd).mergeProfiles(
+ deepEq(List.of(
+ AidlUtils.buildProfilePathForCur(0 /* userId */, PKG_NAME, "primary"),
+ AidlUtils.buildProfilePathForCur(2 /* userId */, PKG_NAME, "primary"))),
+ deepEq(mRefProfile), deepEq(mPrivateOutputProfile), eq(mDexPath));
+
+ // It should use `mBetterOrSameDexoptTrigger` and the merged profile for both ISAs.
+ inOrder.verify(mArtd).getDexoptNeeded(eq(mDexPath), eq("arm64"), any(), eq("speed-profile"),
+ eq(mBetterOrSameDexoptTrigger));
+ checkDexoptWithPrivateProfile(inOrder.verify(mArtd), mDexPath, "arm64",
+ ProfilePath.tmpRefProfilePath(mPrivateOutputProfile.profilePath));
+
+ inOrder.verify(mArtd).getDexoptNeeded(eq(mDexPath), eq("arm"), any(), eq("speed-profile"),
+ eq(mBetterOrSameDexoptTrigger));
+ checkDexoptWithPrivateProfile(inOrder.verify(mArtd), mDexPath, "arm",
+ ProfilePath.tmpRefProfilePath(mPrivateOutputProfile.profilePath));
+
+ inOrder.verify(mArtd).commitTmpProfile(deepEq(mPrivateOutputProfile.profilePath));
+
+ inOrder.verify(mArtd).deleteProfile(
+ deepEq(AidlUtils.buildProfilePathForCur(0 /* userId */, PKG_NAME, "primary")));
+ inOrder.verify(mArtd).deleteProfile(
+ deepEq(AidlUtils.buildProfilePathForCur(2 /* userId */, PKG_NAME, "primary")));
+ }
+
+ @Test
+ public void testDexoptMergesProfilesMergeFailed() throws Exception {
+ when(mPkgState.getUserStateOrDefault(0 /* userId */)).thenReturn(mPkgUserStateInstalled);
+ when(mPkgState.getUserStateOrDefault(2 /* userId */)).thenReturn(mPkgUserStateInstalled);
+
+ when(mArtd.mergeProfiles(any(), any(), any(), any())).thenReturn(false);
+
+ makeProfileUsable(mRefProfile);
+ when(mArtd.getProfileVisibility(deepEq(mRefProfile)))
+ .thenReturn(FileVisibility.OTHER_READABLE);
+
+ mPrimaryDexOptimizer.dexopt(mPkgState, mPkg, mOptimizeParams, mCancellationSignal);
+
+ // It should still use "speed-profile", but with the existing reference profile only.
+ verify(mArtd).getDexoptNeeded(
+ eq(mDexPath), eq("arm64"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
+ checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm64", mRefProfile);
+
+ verify(mArtd).getDexoptNeeded(
+ eq(mDexPath), eq("arm"), any(), eq("speed-profile"), eq(mDefaultDexoptTrigger));
+ checkDexoptWithPublicProfile(verify(mArtd), mDexPath, "arm", mRefProfile);
+
+ verify(mArtd, never()).deleteProfile(any());
+ verify(mArtd, never()).commitTmpProfile(any());
+ }
+
+ @Test
public void testDexoptUsesDmProfile() throws Exception {
makeProfileNotUsable(mRefProfile);
makeProfileNotUsable(mPrebuiltProfile);
diff --git a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
index 488f9fc..0aac42c 100644
--- a/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
+++ b/libartservice/service/javatests/com/android/server/art/PrimaryDexOptimizerTestBase.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
@@ -28,10 +29,12 @@
import android.os.CancellationSignal;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import com.android.server.art.testing.StaticMockitoRule;
import com.android.server.art.wrapper.AndroidPackageApi;
import com.android.server.art.wrapper.PackageState;
+import com.android.server.art.wrapper.PackageUserState;
import dalvik.system.PathClassLoader;
@@ -40,6 +43,7 @@
import org.mockito.Mock;
import java.util.ArrayList;
+import java.util.List;
public class PrimaryDexOptimizerTestBase {
protected static final String PKG_NAME = "com.example.foo";
@@ -50,8 +54,11 @@
@Mock protected PrimaryDexOptimizer.Injector mInjector;
@Mock protected IArtd mArtd;
+ @Mock protected UserManager mUserManager;
protected PackageState mPkgState;
protected AndroidPackageApi mPkg;
+ protected PackageUserState mPkgUserStateNotInstalled;
+ protected PackageUserState mPkgUserStateInstalled;
protected CancellationSignal mCancellationSignal;
protected PrimaryDexOptimizer mPrimaryDexOptimizer;
@@ -61,6 +68,7 @@
lenient().when(mInjector.getArtd()).thenReturn(mArtd);
lenient().when(mInjector.isSystemUiPackage(any())).thenReturn(false);
lenient().when(mInjector.isUsedByOtherApps(any())).thenReturn(false);
+ lenient().when(mInjector.getUserManager()).thenReturn(mUserManager);
lenient()
.when(SystemProperties.get("dalvik.vm.systemuicompilerfilter"))
@@ -71,6 +79,12 @@
lenient().when(SystemProperties.get("dalvik.vm.appimageformat")).thenReturn("lz4");
lenient().when(SystemProperties.get("pm.dexopt.shared")).thenReturn("speed");
+ lenient()
+ .when(mUserManager.getUserHandles(anyBoolean()))
+ .thenReturn(List.of(UserHandle.of(0), UserHandle.of(1), UserHandle.of(2)));
+
+ mPkgUserStateNotInstalled = createPackageUserState(false /* installed */);
+ mPkgUserStateInstalled = createPackageUserState(true /* installed */);
mPkgState = createPackageState();
mPkg = mPkgState.getAndroidPackage();
mCancellationSignal = new CancellationSignal();
@@ -112,11 +126,20 @@
lenient().when(pkgState.isSystem()).thenReturn(false);
lenient().when(pkgState.isUpdatedSystemApp()).thenReturn(false);
lenient().when(pkgState.getUsesLibraryInfos()).thenReturn(new ArrayList<>());
+ lenient()
+ .when(pkgState.getUserStateOrDefault(anyInt()))
+ .thenReturn(mPkgUserStateNotInstalled);
AndroidPackageApi pkg = createPackage();
lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
return pkgState;
}
+ private PackageUserState createPackageUserState(boolean isInstalled) {
+ PackageUserState pkgUserState = mock(PackageUserState.class);
+ lenient().when(pkgUserState.isInstalled()).thenReturn(isInstalled);
+ return pkgUserState;
+ }
+
protected GetDexoptNeededResult dexoptIsNotNeeded() {
var result = new GetDexoptNeededResult();
result.isDexoptNeeded = false;
diff --git a/libartservice/service/javatests/com/android/server/art/UtilsTest.java b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
index a167919..f154800 100644
--- a/libartservice/service/javatests/com/android/server/art/UtilsTest.java
+++ b/libartservice/service/javatests/com/android/server/art/UtilsTest.java
@@ -72,4 +72,14 @@
assertThat(Utils.implies(true, false)).isFalse();
assertThat(Utils.implies(true, true)).isTrue();
}
+
+ @Test
+ public void testCheck() {
+ Utils.check(true);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testCheckFailed() throws Exception {
+ Utils.check(false);
+ }
}
diff --git a/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java b/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java
index e7582b1..9e51c87 100644
--- a/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java
+++ b/libartservice/service/javatests/com/android/server/art/testing/TestingUtils.java
@@ -26,6 +26,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
+import java.util.List;
public final class TestingUtils {
private static final String TAG = "TestingUtils";
@@ -35,7 +36,7 @@
/**
* Recursively compares two objects using reflection. Returns true if the two objects are equal.
* For simplicity, this method only supports types that every field is a primitive type, a
- * string, or a supported type.
+ * string, a {@link List}, or a supported type.
*/
public static boolean deepEquals(
@Nullable Object a, @Nullable Object b, @NonNull StringBuilder errorMsg) {
@@ -48,6 +49,9 @@
a == null ? "null" : "nonnull", b == null ? "null" : "nonnull"));
return false;
}
+ if (a instanceof List && b instanceof List) {
+ return listDeepEquals((List<?>) a, (List<?>) b, errorMsg);
+ }
if (a.getClass() != b.getClass()) {
errorMsg.append(
String.format("Type mismatch: %s != %s", a.getClass(), b.getClass()));
@@ -117,4 +121,19 @@
return errorMsg.toString();
});
}
+
+ private static boolean listDeepEquals(
+ @NonNull List<?> a, @NonNull List<?> b, @NonNull StringBuilder errorMsg) {
+ if (a.size() != b.size()) {
+ errorMsg.append(String.format("List length mismatch: %d != %d", a.size(), b.size()));
+ return false;
+ }
+ for (int i = 0; i < a.size(); i++) {
+ if (!deepEquals(a.get(i), b.get(i), errorMsg)) {
+ errorMsg.insert(0, String.format("Element %d mismatch: ", i));
+ return false;
+ }
+ }
+ return true;
+ };
}
diff --git a/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java b/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java
index c1fe78b..5ef532c 100644
--- a/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java
+++ b/libartservice/service/javatests/com/android/server/art/testing/TestingUtilsTest.java
@@ -25,6 +25,8 @@
import org.junit.runner.RunWith;
import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -85,11 +87,42 @@
TestingUtils.deepEquals(a, b);
}
- @Test(expected = UnsupportedOperationException.class)
- public void testDeepEqualsContainerNotSupported() throws Exception {
+ @Test
+ public void testListDeepEquals() throws Exception {
+ var a = new ArrayList<Integer>();
+ a.add(1);
+ a.add(2);
+ a.add(3);
+ a.add(4);
+ a.add(5);
+ var b = List.of(1, 2, 3, 4, 5);
+ assertThat(TestingUtils.deepEquals(a, b)).isTrue();
+ }
+
+ @Test
+ public void testListDeepEqualsSizeMismatch() throws Exception {
var a = new ArrayList<Integer>();
a.add(1);
var b = new ArrayList<Integer>();
+ b.add(1);
+ b.add(2);
+ assertThat(TestingUtils.deepEquals(a, b)).isFalse();
+ }
+
+ @Test
+ public void testListDeepEqualsElementMismatch() throws Exception {
+ var a = new ArrayList<Integer>();
+ a.add(1);
+ var b = new ArrayList<Integer>();
+ b.add(2);
+ assertThat(TestingUtils.deepEquals(a, b)).isFalse();
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testDeepEqualsOtherContainerNotSupported() throws Exception {
+ var a = new HashSet<Integer>();
+ a.add(1);
+ var b = new HashSet<Integer>();
b.add(2);
TestingUtils.deepEquals(a, b);
}