diff options
author | 2022-09-23 13:14:18 +0100 | |
---|---|---|
committer | 2022-10-05 14:43:59 +0000 | |
commit | ab3f419d42638dd0f51a202ef36ad0dc80ad8674 (patch) | |
tree | 6f328f5b9fffa1190bc6a3e7c1d0facdabc126a0 | |
parent | 624f9159609a5b34bbb2c71aed24740e92d62afe (diff) |
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
19 files changed, 672 insertions, 32 deletions
diff --git a/artd/artd.cc b/artd/artd.cc index 20dbe16f78..bf2f7b3a9a 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::Dirname; 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 @@ Result<ArtdCancellationSignal*> ToArtdCancellationSignal(IArtdCancellationSignal 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 @@ ndk::ScopedAStatus Artd::isProfileUsable(const ProfilePath& in_profile, 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 @@ ndk::ScopedAStatus Artd::copyAndRewriteProfile(const ProfilePath& in_src, 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 @@ ndk::ScopedAStatus Artd::deleteProfile(const ProfilePath& in_profile) { 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 @@ ndk::ScopedAStatus Artd::getArtifactsVisibility(const ArtifactsPath& in_artifact 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 @@ ndk::ScopedAStatus Artd::dexopt( } 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 dd4b02230d..9c65ed245c 100644 --- a/artd/artd.h +++ b/artd/artd.h @@ -104,6 +104,13 @@ class Artd : public aidl::com::android::server::art::BnArtd { 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 63d13c211e..f6b24a401c 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 ::aidl::com::android::server::art::VdexPath; 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::Return; using ::testing::SetArgPointee; using ::testing::WithArg; +using CurProfilePath = ProfilePath::CurProfilePath; using RefProfilePath = ProfilePath::RefProfilePath; using TmpRefProfilePath = ProfilePath::TmpRefProfilePath; @@ -112,13 +116,21 @@ void CheckContent(const std::string& path, const std::string& expected_content) 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 @@ ACTION_P(WriteToFdFlag, flag, content) { 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 @@ MATCHER_P(FdOf, matcher, "") { 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 @@ class ArtdTest : public CommonArtTest { 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 @@ class ArtdTest : public CommonArtTest { 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, isAlive) { 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 @@ TEST_F(ArtdTest, deleteArtifactsMissingFile) { // 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, deleteArtifactsNoFile) { 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 @@ TEST_F(ArtdTest, deleteArtifactsFileIsDir) { 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 @@ TEST_F(ArtdTest, deleteProfile) { 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 @@ TEST_F(ArtdTest, getArtifactsVisibilityPermissionDenied) { 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 04de1167aa..7bed2ff78f 100644 --- a/artd/binder/com/android/server/art/IArtd.aidl +++ b/artd/binder/com/android/server/art/IArtd.aidl @@ -63,7 +63,7 @@ interface IArtd { 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 @@ interface IArtd { 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 e12d94a029..afd0fd6d4b 100644 --- a/artd/binder/com/android/server/art/ProfilePath.aidl +++ b/artd/binder/com/android/server/art/ProfilePath.aidl @@ -25,6 +25,7 @@ union ProfilePath { 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 @@ union ProfilePath { /** 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 0cebdfb375..8314241861 100644 --- a/artd/path_utils.cc +++ b/artd/path_utils.cc @@ -43,6 +43,7 @@ using ::android::base::Result; 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 @@ Result<std::string> BuildPrebuiltProfilePath(const PrebuiltProfilePath& prebuilt 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 @@ Result<std::string> BuildProfileOrDmPath(const ProfilePath& profile_path) { 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 f3a635994a..5b2b1d3c32 100644 --- a/artd/path_utils.h +++ b/artd/path_utils.h @@ -51,6 +51,9 @@ android::base::Result<std::string> BuildTmpRefProfilePath( 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 29a9cb875b..8188000c4e 100644 --- a/artd/path_utils_test.cc +++ b/artd/path_utils_test.cc @@ -33,6 +33,7 @@ using ::android::base::testing::HasError; 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 @@ TEST_F(PathUtilsTest, BuildPrebuiltProfilePath) { 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 @@ TEST_F(PathUtilsTest, BuildProfilePath) { 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 b28e1ffa77..26d5df8da4 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 @@ package com.android.server.art; 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 @@ public final class AidlUtils { } @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 fa2af7a919..cb7e9ff2ab 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.RemoteException; 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.OptimizeParams; 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 @@ public class PrimaryDexOptimizer { 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 @@ public class PrimaryDexOptimizer { } 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 @@ public class PrimaryDexOptimizer { 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 @@ public class PrimaryDexOptimizer { } } - 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 @@ public class PrimaryDexOptimizer { } } + @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 @@ public class PrimaryDexOptimizer { } @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 134dc9f9b7..66c9704093 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 @@ public final class Utils { 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 c591274ad8..03c5f97a1f 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 @@ public class PackageState { 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 0000000000..b21db11593 --- /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 829fc1c7a6..7d8c1f050f 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 @@ The mappings are: - `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 7cce870da4..f4ff71dee8 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 @@ public class PrimaryDexOptimizerTest extends PrimaryDexOptimizerTestBase { 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 @@ public class PrimaryDexOptimizerTest extends PrimaryDexOptimizerTestBase { } @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 488f9fc136..0aac42c3ee 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 com.android.server.art.GetDexoptNeededResult.ArtifactsLocation; 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.content.pm.ApplicationInfo; 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.junit.Rule; 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 @@ public class PrimaryDexOptimizerTestBase { @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 @@ public class PrimaryDexOptimizerTestBase { 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 @@ public class PrimaryDexOptimizerTestBase { 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 @@ public class PrimaryDexOptimizerTestBase { 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 a167919798..f1548003a7 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 @@ public class UtilsTest { 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 e7582b13de..9e51c87b8f 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 com.google.common.truth.Correspondence; 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 @@ public final class TestingUtils { /** * 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 @@ public final class TestingUtils { 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 @@ public final class TestingUtils { 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 c1fe78b3e1..5ef532cd9f 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.Test; import org.junit.runner.RunWith; import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) @@ -85,12 +87,43 @@ public class TestingUtilsTest { 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); } } |