| /* |
| * Copyright (C) 2020 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. |
| */ |
| |
| #include "odrefresh.h" |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <inttypes.h> |
| #include <limits.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <sysexits.h> |
| #include <time.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <cstdarg> |
| #include <cstdint> |
| #include <cstdlib> |
| #include <fstream> |
| #include <initializer_list> |
| #include <iosfwd> |
| #include <iostream> |
| #include <iterator> |
| #include <memory> |
| #include <optional> |
| #include <ostream> |
| #include <sstream> |
| #include <string> |
| #include <string_view> |
| #include <type_traits> |
| #include <unordered_map> |
| #include <utility> |
| #include <vector> |
| |
| #include "android-base/file.h" |
| #include "android-base/logging.h" |
| #include "android-base/macros.h" |
| #include "android-base/properties.h" |
| #include "android-base/stringprintf.h" |
| #include "android-base/strings.h" |
| #include "android/log.h" |
| #include "arch/instruction_set.h" |
| #include "base/file_utils.h" |
| #include "base/globals.h" |
| #include "base/macros.h" |
| #include "base/os.h" |
| #include "base/string_view_cpp20.h" |
| #include "base/unix_file/fd_file.h" |
| #include "com_android_apex.h" |
| #include "com_android_art.h" |
| #include "dex/art_dex_file_loader.h" |
| #include "dexoptanalyzer.h" |
| #include "exec_utils.h" |
| #include "log/log.h" |
| #include "odr_artifacts.h" |
| #include "odr_common.h" |
| #include "odr_compilation_log.h" |
| #include "odr_config.h" |
| #include "odr_fs_utils.h" |
| #include "odr_metrics.h" |
| #include "odrefresh/odrefresh.h" |
| #include "palette/palette.h" |
| #include "palette/palette_types.h" |
| |
| namespace art { |
| namespace odrefresh { |
| |
| namespace apex = com::android::apex; |
| namespace art_apex = com::android::art; |
| |
| namespace { |
| |
| // Name of cache info file in the ART Apex artifact cache. |
| constexpr const char* kCacheInfoFile = "cache-info.xml"; |
| |
| // Maximum execution time for odrefresh from start to end. |
| constexpr time_t kMaximumExecutionSeconds = 300; |
| |
| // Maximum execution time for any child process spawned. |
| constexpr time_t kMaxChildProcessSeconds = 90; |
| |
| // Extra execution time for any child process spawned in a VM. |
| constexpr time_t kExtraChildProcessSecondsInVm = 60; |
| |
| void EraseFiles(const std::vector<std::unique_ptr<File>>& files) { |
| for (auto& file : files) { |
| file->Erase(/*unlink=*/true); |
| } |
| } |
| |
| // Moves `files` to the directory `output_directory_path`. |
| // |
| // If any of the files cannot be moved, then all copies of the files are removed from both |
| // the original location and the output location. |
| // |
| // Returns true if all files are moved, false otherwise. |
| bool MoveOrEraseFiles(const std::vector<std::unique_ptr<File>>& files, |
| std::string_view output_directory_path) { |
| std::vector<std::unique_ptr<File>> output_files; |
| for (auto& file : files) { |
| const std::string file_basename(android::base::Basename(file->GetPath())); |
| const std::string output_file_path = Concatenate({output_directory_path, "/", file_basename}); |
| const std::string input_file_path = file->GetPath(); |
| |
| output_files.emplace_back(OS::CreateEmptyFileWriteOnly(output_file_path.c_str())); |
| if (output_files.back() == nullptr) { |
| PLOG(ERROR) << "Failed to open " << QuotePath(output_file_path); |
| output_files.pop_back(); |
| EraseFiles(output_files); |
| EraseFiles(files); |
| return false; |
| } |
| |
| static constexpr mode_t kFileMode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; |
| if (fchmod(output_files.back()->Fd(), kFileMode) != 0) { |
| PLOG(ERROR) << "Could not set file mode on " << QuotePath(output_file_path); |
| EraseFiles(output_files); |
| EraseFiles(files); |
| return false; |
| } |
| |
| const size_t file_bytes = file->GetLength(); |
| if (!output_files.back()->Copy(file.get(), /*offset=*/0, file_bytes)) { |
| PLOG(ERROR) << "Failed to copy " << QuotePath(file->GetPath()) << " to " |
| << QuotePath(output_file_path); |
| EraseFiles(output_files); |
| EraseFiles(files); |
| return false; |
| } |
| |
| if (!file->Erase(/*unlink=*/true)) { |
| PLOG(ERROR) << "Failed to erase " << QuotePath(file->GetPath()); |
| EraseFiles(output_files); |
| EraseFiles(files); |
| return false; |
| } |
| |
| if (output_files.back()->FlushCloseOrErase() != 0) { |
| PLOG(ERROR) << "Failed to flush and close file " << QuotePath(output_file_path); |
| EraseFiles(output_files); |
| EraseFiles(files); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Gets the `ApexInfo` associated with the currently active ART APEX. |
| std::optional<apex::ApexInfo> GetArtApexInfo(const std::vector<apex::ApexInfo>& info_list) { |
| auto it = std::find_if(info_list.begin(), info_list.end(), [](const apex::ApexInfo& info) { |
| return info.getModuleName() == "com.android.art"; |
| }); |
| return it != info_list.end() ? std::make_optional(*it) : std::nullopt; |
| } |
| |
| // Returns cache provenance information based on the current APEX version and filesystem |
| // information. |
| art_apex::ModuleInfo GenerateModuleInfo(const apex::ApexInfo& apex_info) { |
| // The lastUpdateMillis is an addition to ApexInfoList.xsd to support samegrade installs. |
| int64_t last_update_millis = |
| apex_info.hasLastUpdateMillis() ? apex_info.getLastUpdateMillis() : 0; |
| return art_apex::ModuleInfo{apex_info.getModuleName(), |
| apex_info.getVersionCode(), |
| apex_info.getVersionName(), |
| last_update_millis}; |
| } |
| |
| // Returns cache provenance information for all APEXes. |
| std::vector<art_apex::ModuleInfo> GenerateModuleInfoList( |
| const std::vector<apex::ApexInfo>& apex_info_list) { |
| std::vector<art_apex::ModuleInfo> module_info_list; |
| std::transform(apex_info_list.begin(), |
| apex_info_list.end(), |
| std::back_inserter(module_info_list), |
| GenerateModuleInfo); |
| return module_info_list; |
| } |
| |
| bool CheckComponents(const std::vector<art_apex::Component>& expected_components, |
| const std::vector<art_apex::Component>& actual_components, |
| std::string* error_msg) { |
| if (expected_components.size() != actual_components.size()) { |
| return false; |
| } |
| |
| for (size_t i = 0; i < expected_components.size(); ++i) { |
| const art_apex::Component& expected = expected_components[i]; |
| const art_apex::Component& actual = actual_components[i]; |
| |
| if (expected.getFile() != actual.getFile()) { |
| *error_msg = android::base::StringPrintf("Component %zu file differs ('%s' != '%s')", |
| i, |
| expected.getFile().c_str(), |
| actual.getFile().c_str()); |
| return false; |
| } |
| if (expected.getSize() != actual.getSize()) { |
| *error_msg = |
| android::base::StringPrintf("Component %zu size differs (%" PRIu64 " != %" PRIu64 ")", |
| i, |
| expected.getSize(), |
| actual.getSize()); |
| return false; |
| } |
| if (expected.getChecksums() != actual.getChecksums()) { |
| *error_msg = android::base::StringPrintf("Component %zu checksums differ ('%s' != '%s')", |
| i, |
| expected.getChecksums().c_str(), |
| actual.getChecksums().c_str()); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| std::vector<art_apex::Component> GenerateComponents(const std::vector<std::string>& jars) { |
| std::vector<art_apex::Component> components; |
| |
| ArtDexFileLoader loader; |
| for (const std::string& path : jars) { |
| struct stat sb; |
| if (stat(path.c_str(), &sb) == -1) { |
| PLOG(ERROR) << "Failed to get component: " << QuotePath(path); |
| return {}; |
| } |
| |
| std::vector<uint32_t> checksums; |
| std::vector<std::string> dex_locations; |
| std::string error_msg; |
| if (!loader.GetMultiDexChecksums(path.c_str(), &checksums, &dex_locations, &error_msg)) { |
| LOG(ERROR) << "Failed to get components: " << error_msg; |
| return {}; |
| } |
| |
| std::ostringstream oss; |
| for (size_t i = 0; i < checksums.size(); ++i) { |
| if (i != 0) { |
| oss << ';'; |
| } |
| oss << android::base::StringPrintf("%08x", checksums[i]); |
| } |
| const std::string checksum = oss.str(); |
| |
| components.emplace_back(art_apex::Component{path, static_cast<uint64_t>(sb.st_size), checksum}); |
| } |
| |
| return components; |
| } |
| |
| // Checks whether a group of artifacts exists. Returns true if all are present, false otherwise. |
| bool ArtifactsExist(const OdrArtifacts& artifacts, |
| bool check_art_file, |
| /*out*/ std::string* error_msg) { |
| std::vector<const char*> paths{artifacts.OatPath().c_str(), artifacts.VdexPath().c_str()}; |
| if (check_art_file) { |
| paths.push_back(artifacts.ImagePath().c_str()); |
| } |
| for (const char* path : paths) { |
| if (!OS::FileExists(path)) { |
| if (errno == EACCES) { |
| PLOG(ERROR) << "Failed to stat() " << path; |
| } |
| *error_msg = "Missing file: " + QuotePath(path); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| void AddDex2OatCommonOptions(/*inout*/ std::vector<std::string>* args) { |
| args->emplace_back("--android-root=out/empty"); |
| args->emplace_back("--abort-on-hard-verifier-error"); |
| args->emplace_back("--no-abort-on-soft-verifier-error"); |
| args->emplace_back("--compilation-reason=boot"); |
| args->emplace_back("--image-format=lz4"); |
| args->emplace_back("--force-determinism"); |
| args->emplace_back("--resolve-startup-const-strings=true"); |
| } |
| |
| void AddDex2OatConcurrencyArguments(/*inout*/ std::vector<std::string>* args) { |
| static constexpr std::pair<const char*, const char*> kPropertyArgPairs[] = { |
| std::make_pair("dalvik.vm.boot-dex2oat-cpu-set", "--cpu-set="), |
| std::make_pair("dalvik.vm.boot-dex2oat-threads", "-j"), |
| }; |
| for (auto property_arg_pair : kPropertyArgPairs) { |
| auto [property, arg] = property_arg_pair; |
| std::string value = android::base::GetProperty(property, {}); |
| if (!value.empty()) { |
| args->push_back(arg + value); |
| } |
| } |
| } |
| |
| void AddDex2OatDebugInfo(/*inout*/ std::vector<std::string>* args) { |
| args->emplace_back("--generate-mini-debug-info"); |
| args->emplace_back("--strip"); |
| } |
| |
| void AddDex2OatInstructionSet(/*inout*/ std::vector<std::string>* args, InstructionSet isa) { |
| const char* isa_str = GetInstructionSetString(isa); |
| args->emplace_back(Concatenate({"--instruction-set=", isa_str})); |
| } |
| |
| void AddDex2OatProfileAndCompilerFilter( |
| /*inout*/ std::vector<std::string>* args, |
| /*inout*/ std::vector<std::unique_ptr<File>>* output_files, |
| const std::string& profile_path) { |
| std::unique_ptr<File> profile_file(OS::OpenFileForReading(profile_path.c_str())); |
| if (profile_file && profile_file->IsOpened()) { |
| args->emplace_back(android::base::StringPrintf("--profile-file-fd=%d", profile_file->Fd())); |
| args->emplace_back("--compiler-filter=speed-profile"); |
| output_files->push_back(std::move(profile_file)); |
| } else { |
| args->emplace_back("--compiler-filter=speed"); |
| } |
| } |
| |
| bool AddBootClasspathFds(/*inout*/ std::vector<std::string>& args, |
| /*inout*/ std::vector<std::unique_ptr<File>>& output_files, |
| const std::vector<std::string>& bcp_jars) { |
| auto bcp_fds = std::vector<std::string>(); |
| for (const std::string& jar : bcp_jars) { |
| std::unique_ptr<File> jar_file(OS::OpenFileForReading(jar.c_str())); |
| if (!jar_file || !jar_file->IsValid()) { |
| LOG(ERROR) << "Failed to open a BCP jar " << jar; |
| return false; |
| } |
| bcp_fds.push_back(std::to_string(jar_file->Fd())); |
| output_files.push_back(std::move(jar_file)); |
| } |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back(Concatenate({"-Xbootclasspathfds:", android::base::Join(bcp_fds, ':')})); |
| return true; |
| } |
| |
| void AddCompiledBootClasspathFdsIfAny( |
| /*inout*/ std::vector<std::string>& args, |
| /*inout*/ std::vector<std::unique_ptr<File>>& output_files, |
| const std::vector<std::string>& bcp_jars, |
| const InstructionSet isa) { |
| std::vector<std::string> bcp_image_fds; |
| std::vector<std::string> bcp_oat_fds; |
| std::vector<std::string> bcp_vdex_fds; |
| std::vector<std::unique_ptr<File>> opened_files; |
| bool added_any = false; |
| for (const std::string& jar : bcp_jars) { |
| std::string image_path = GetApexDataBootImage(jar); |
| image_path = image_path.empty() ? "" : GetSystemImageFilename(image_path.c_str(), isa); |
| std::unique_ptr<File> image_file(OS::OpenFileForReading(image_path.c_str())); |
| if (image_file && image_file->IsValid()) { |
| bcp_image_fds.push_back(std::to_string(image_file->Fd())); |
| opened_files.push_back(std::move(image_file)); |
| added_any = true; |
| } else { |
| bcp_image_fds.push_back("-1"); |
| } |
| |
| std::string oat_path = ReplaceFileExtension(image_path, "oat"); |
| std::unique_ptr<File> oat_file(OS::OpenFileForReading(oat_path.c_str())); |
| if (oat_file && oat_file->IsValid()) { |
| bcp_oat_fds.push_back(std::to_string(oat_file->Fd())); |
| opened_files.push_back(std::move(oat_file)); |
| added_any = true; |
| } else { |
| bcp_oat_fds.push_back("-1"); |
| } |
| |
| std::string vdex_path = ReplaceFileExtension(image_path, "vdex"); |
| std::unique_ptr<File> vdex_file(OS::OpenFileForReading(vdex_path.c_str())); |
| if (vdex_file && vdex_file->IsValid()) { |
| bcp_vdex_fds.push_back(std::to_string(vdex_file->Fd())); |
| opened_files.push_back(std::move(vdex_file)); |
| added_any = true; |
| } else { |
| bcp_vdex_fds.push_back("-1"); |
| } |
| } |
| // Add same amount of FDs as BCP JARs, or none. |
| if (added_any) { |
| std::move(opened_files.begin(), opened_files.end(), std::back_inserter(output_files)); |
| |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back( |
| Concatenate({"-Xbootclasspathimagefds:", android::base::Join(bcp_image_fds, ':')})); |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back( |
| Concatenate({"-Xbootclasspathoatfds:", android::base::Join(bcp_oat_fds, ':')})); |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back( |
| Concatenate({"-Xbootclasspathvdexfds:", android::base::Join(bcp_vdex_fds, ':')})); |
| } |
| } |
| |
| std::string GetStagingLocation(const std::string& staging_dir, const std::string& path) { |
| return Concatenate({staging_dir, "/", android::base::Basename(path)}); |
| } |
| |
| std::string JoinFilesAsFDs(const std::vector<std::unique_ptr<File>>& files, char delimiter) { |
| std::stringstream output; |
| bool is_first = true; |
| for (const auto& f : files) { |
| if (is_first) { |
| is_first = false; |
| } else { |
| output << delimiter; |
| } |
| output << std::to_string(f->Fd()); |
| } |
| return output.str(); |
| } |
| |
| WARN_UNUSED bool CheckCompilationSpace() { |
| // Check the available storage space against an arbitrary threshold because dex2oat does not |
| // report when it runs out of storage space and we do not want to completely fill |
| // the users data partition. |
| // |
| // We do not have a good way of pre-computing the required space for a compilation step, but |
| // typically observe 16MB as the largest size of an AOT artifact. Since there are three |
| // AOT artifacts per compilation step - an image file, executable file, and a verification |
| // data file - the threshold is three times 16MB. |
| static constexpr uint64_t kMinimumSpaceForCompilation = 3 * 16 * 1024 * 1024; |
| |
| uint64_t bytes_available; |
| const std::string& art_apex_data_path = GetArtApexData(); |
| if (!GetFreeSpace(art_apex_data_path, &bytes_available)) { |
| return false; |
| } |
| |
| if (bytes_available < kMinimumSpaceForCompilation) { |
| LOG(WARNING) << "Low space for " << QuotePath(art_apex_data_path) << " (" << bytes_available |
| << " bytes)"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| std::string GetBootImage() { |
| // Typically "/apex/com.android.art/javalib/boot.art". |
| return GetArtRoot() + "/javalib/boot.art"; |
| } |
| |
| } // namespace |
| |
| OnDeviceRefresh::OnDeviceRefresh(const OdrConfig& config) |
| : OnDeviceRefresh(config, |
| Concatenate({kOdrefreshArtifactDirectory, "/", kCacheInfoFile}), |
| std::make_unique<ExecUtils>()) {} |
| |
| OnDeviceRefresh::OnDeviceRefresh(const OdrConfig& config, |
| const std::string& cache_info_filename, |
| std::unique_ptr<ExecUtils> exec_utils) |
| : config_{config}, |
| cache_info_filename_{cache_info_filename}, |
| start_time_{time(nullptr)}, |
| exec_utils_{std::move(exec_utils)} { |
| for (const std::string& jar : android::base::Split(config_.GetDex2oatBootClasspath(), ":")) { |
| // Boot class path extensions are those not in the ART APEX. Updatable APEXes should not |
| // have DEX files in the DEX2OATBOOTCLASSPATH. At the time of writing i18n is a non-updatable |
| // APEX and so does appear in the DEX2OATBOOTCLASSPATH. |
| if (!LocationIsOnArtModule(jar)) { |
| boot_extension_compilable_jars_.emplace_back(jar); |
| } |
| } |
| |
| systemserver_compilable_jars_ = android::base::Split(config_.GetSystemServerClasspath(), ":"); |
| boot_classpath_jars_ = android::base::Split(config_.GetBootClasspath(), ":"); |
| |
| max_child_process_seconds_ = kMaxChildProcessSeconds; |
| if (config_.UseCompilationOs()) { |
| // Give extra time to all of the Comp OS cases for simplicity. But really, this is only needed |
| // on cuttlefish, where we run Comp OS in a slow, *nested* VM. |
| // TODO(197531822): Remove once we can give the VM more CPU and/or improve the file caching. |
| max_child_process_seconds_ += kExtraChildProcessSecondsInVm; |
| } |
| } |
| |
| time_t OnDeviceRefresh::GetExecutionTimeUsed() const { |
| return time(nullptr) - start_time_; |
| } |
| |
| time_t OnDeviceRefresh::GetExecutionTimeRemaining() const { |
| return kMaximumExecutionSeconds - GetExecutionTimeUsed(); |
| } |
| |
| time_t OnDeviceRefresh::GetSubprocessTimeout() const { |
| return std::max(GetExecutionTimeRemaining(), max_child_process_seconds_); |
| } |
| |
| std::optional<std::vector<apex::ApexInfo>> OnDeviceRefresh::GetApexInfoList() const { |
| std::optional<apex::ApexInfoList> info_list = |
| apex::readApexInfoList(config_.GetApexInfoListFile().c_str()); |
| if (!info_list.has_value()) { |
| return std::nullopt; |
| } |
| |
| std::vector<apex::ApexInfo> filtered_info_list; |
| std::copy_if(info_list->getApexInfo().begin(), |
| info_list->getApexInfo().end(), |
| std::back_inserter(filtered_info_list), |
| [](const apex::ApexInfo& info) { return info.getIsActive(); }); |
| return filtered_info_list; |
| } |
| |
| std::optional<art_apex::CacheInfo> OnDeviceRefresh::ReadCacheInfo() const { |
| return art_apex::read(cache_info_filename_.c_str()); |
| } |
| |
| void OnDeviceRefresh::WriteCacheInfo() const { |
| if (OS::FileExists(cache_info_filename_.c_str())) { |
| if (unlink(cache_info_filename_.c_str()) != 0) { |
| PLOG(ERROR) << "Failed to unlink() file " << QuotePath(cache_info_filename_); |
| } |
| } |
| |
| const std::string dir_name = android::base::Dirname(cache_info_filename_); |
| if (!EnsureDirectoryExists(dir_name)) { |
| LOG(ERROR) << "Could not create directory: " << QuotePath(dir_name); |
| return; |
| } |
| |
| std::optional<std::vector<apex::ApexInfo>> apex_info_list = GetApexInfoList(); |
| if (!apex_info_list.has_value()) { |
| LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no APEX info"; |
| return; |
| } |
| |
| std::optional<apex::ApexInfo> art_apex_info = GetArtApexInfo(apex_info_list.value()); |
| if (!art_apex_info.has_value()) { |
| LOG(ERROR) << "Could not update " << QuotePath(cache_info_filename_) << " : no ART APEX info"; |
| return; |
| } |
| |
| art_apex::ModuleInfo art_module_info = GenerateModuleInfo(art_apex_info.value()); |
| std::vector<art_apex::ModuleInfo> module_info_list = |
| GenerateModuleInfoList(apex_info_list.value()); |
| |
| std::optional<std::vector<art_apex::Component>> bcp_components = |
| GenerateBootClasspathComponents(); |
| if (!bcp_components.has_value()) { |
| LOG(ERROR) << "No boot classpath components."; |
| return; |
| } |
| |
| std::optional<std::vector<art_apex::Component>> bcp_compilable_components = |
| GenerateBootExtensionCompilableComponents(); |
| if (!bcp_compilable_components.has_value()) { |
| LOG(ERROR) << "No boot classpath extension compilable components."; |
| return; |
| } |
| |
| std::optional<std::vector<art_apex::Component>> system_server_components = |
| GenerateSystemServerComponents(); |
| if (!system_server_components.has_value()) { |
| LOG(ERROR) << "No system_server extension components."; |
| return; |
| } |
| |
| std::ofstream out(cache_info_filename_.c_str()); |
| art_apex::CacheInfo info({art_module_info}, |
| {art_apex::ModuleInfoList(module_info_list)}, |
| {art_apex::Classpath(bcp_components.value())}, |
| {art_apex::Classpath(bcp_compilable_components.value())}, |
| {art_apex::Classpath(system_server_components.value())}); |
| |
| art_apex::write(out, info); |
| } |
| |
| void OnDeviceRefresh::ReportNextBootAnimationProgress(uint32_t current_compilation) const { |
| uint32_t number_of_compilations = |
| config_.GetBootExtensionIsas().size() + systemserver_compilable_jars_.size(); |
| // We arbitrarily show progress until 90%, expecting that our compilations |
| // take a large chunk of boot time. |
| uint32_t value = (90 * current_compilation) / number_of_compilations; |
| android::base::SetProperty("service.bootanim.progress", std::to_string(value)); |
| } |
| |
| std::vector<art_apex::Component> OnDeviceRefresh::GenerateBootClasspathComponents() const { |
| return GenerateComponents(boot_classpath_jars_); |
| } |
| |
| std::vector<art_apex::Component> OnDeviceRefresh::GenerateBootExtensionCompilableComponents() |
| const { |
| return GenerateComponents(boot_extension_compilable_jars_); |
| } |
| |
| std::vector<art_apex::Component> OnDeviceRefresh::GenerateSystemServerComponents() const { |
| return GenerateComponents(systemserver_compilable_jars_); |
| } |
| |
| std::string OnDeviceRefresh::GetBootImageExtensionImage(bool on_system) const { |
| CHECK(!boot_extension_compilable_jars_.empty()); |
| const std::string leading_jar = boot_extension_compilable_jars_[0]; |
| if (on_system) { |
| const std::string jar_name = android::base::Basename(leading_jar); |
| const std::string image_name = ReplaceFileExtension(jar_name, "art"); |
| // Typically "/system/framework/boot-framework.art". |
| return Concatenate({GetAndroidRoot(), "/framework/boot-", image_name}); |
| } else { |
| // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/boot-framework.art". |
| return GetApexDataBootImage(leading_jar); |
| } |
| } |
| |
| std::string OnDeviceRefresh::GetBootImageExtensionImagePath(bool on_system, |
| const InstructionSet isa) const { |
| // Typically "/data/misc/apexdata/com.android.art/dalvik-cache/<isa>/boot-framework.art". |
| return GetSystemImageFilename(GetBootImageExtensionImage(on_system).c_str(), isa); |
| } |
| |
| std::string OnDeviceRefresh::GetSystemServerImagePath(bool on_system, |
| const std::string& jar_path) const { |
| if (on_system) { |
| // TODO(b/194150908): Define a path for "preopted" APEX artifacts. |
| const std::string jar_name = android::base::Basename(jar_path); |
| const std::string image_name = ReplaceFileExtension(jar_name, "art"); |
| const char* isa_str = GetInstructionSetString(config_.GetSystemServerIsa()); |
| // Typically "/system/framework/oat/<isa>/services.art". |
| return Concatenate({GetAndroidRoot(), "/framework/oat/", isa_str, "/", image_name}); |
| } else { |
| // Typically |
| // "/data/misc/apexdata/.../dalvik-cache/<isa>/system@framework@services.jar@classes.art". |
| const std::string image = GetApexDataImage(jar_path.c_str()); |
| return GetSystemImageFilename(image.c_str(), config_.GetSystemServerIsa()); |
| } |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::RemoveBootExtensionArtifactsFromData(InstructionSet isa) const { |
| if (config_.GetDryRun()) { |
| LOG(INFO) << "Removal of bcp extension artifacts on /data skipped (dry-run)."; |
| return true; |
| } |
| |
| const std::string apexdata_image_location = |
| GetBootImageExtensionImagePath(/*on_system=*/false, isa); |
| LOG(INFO) << "Removing boot class path artifacts on /data for " |
| << QuotePath(apexdata_image_location); |
| return RemoveArtifacts(OdrArtifacts::ForBootImageExtension(apexdata_image_location)); |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::RemoveSystemServerArtifactsFromData() const { |
| if (config_.GetDryRun()) { |
| LOG(INFO) << "Removal of system_server artifacts on /data skipped (dry-run)."; |
| return true; |
| } |
| |
| bool success = true; |
| for (const std::string& jar_path : systemserver_compilable_jars_) { |
| const std::string image_location = GetSystemServerImagePath(/*on_system=*/false, jar_path); |
| const OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location); |
| LOG(INFO) << "Removing system_server artifacts on /data for " << QuotePath(jar_path); |
| success &= RemoveArtifacts(artifacts); |
| } |
| return success; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::RemoveArtifacts(const OdrArtifacts& artifacts) const { |
| bool success = true; |
| for (const auto& location : {artifacts.ImagePath(), artifacts.OatPath(), artifacts.VdexPath()}) { |
| if (config_.GetDryRun()) { |
| LOG(INFO) << "Removing " << QuotePath(location) << " (dry-run)."; |
| continue; |
| } |
| |
| if (OS::FileExists(location.c_str()) && unlink(location.c_str()) != 0) { |
| PLOG(ERROR) << "Failed to remove: " << QuotePath(location); |
| success = false; |
| } |
| } |
| return success; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::RemoveArtifactsDirectory() const { |
| if (config_.GetDryRun()) { |
| LOG(INFO) << "Directory " << QuotePath(kOdrefreshArtifactDirectory) |
| << " and contents would be removed (dry-run)."; |
| return true; |
| } |
| return RemoveDirectory(kOdrefreshArtifactDirectory); |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::BootExtensionArtifactsExist( |
| bool on_system, |
| const InstructionSet isa, |
| /*out*/ std::string* error_msg) const { |
| const std::string apexdata_image_location = GetBootImageExtensionImagePath(on_system, isa); |
| const OdrArtifacts artifacts = OdrArtifacts::ForBootImageExtension(apexdata_image_location); |
| return ArtifactsExist(artifacts, /*check_art_file=*/true, error_msg); |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::SystemServerArtifactsExist(bool on_system, |
| /*out*/ std::string* error_msg) const { |
| for (const std::string& jar_path : systemserver_compilable_jars_) { |
| // Temporarily skip checking APEX jar artifacts on system to prevent compilation on the first |
| // boot. Currently, APEX jar artifacts can never be found on system because we don't preopt |
| // them. |
| // TODO(b/194150908): Preopt APEX jars for system server and put the artifacts on /system. |
| if (on_system && StartsWith(jar_path, "/apex")) { |
| continue; |
| } |
| const std::string image_location = GetSystemServerImagePath(on_system, jar_path); |
| const OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location); |
| // .art files are optional and are not generated for all jars by the build system. |
| const bool check_art_file = !on_system; |
| if (!ArtifactsExist(artifacts, check_art_file, error_msg)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::CheckBootExtensionArtifactsAreUpToDate( |
| OdrMetrics& metrics, |
| const InstructionSet isa, |
| const apex::ApexInfo& art_apex_info, |
| const std::optional<art_apex::CacheInfo>& cache_info, |
| /*out*/ bool* cleanup_required) const { |
| std::string error_msg; |
| |
| if (art_apex_info.getIsFactory()) { |
| LOG(INFO) << "Factory ART APEX mounted."; |
| |
| // ART is not updated, so we can use the artifacts on /system. Check if they exist. |
| if (BootExtensionArtifactsExist(/*on_system=*/true, isa, &error_msg)) { |
| // We don't need the artifacts on /data since we can use those on /system. |
| *cleanup_required = true; |
| return true; |
| } |
| |
| LOG(INFO) << "Incomplete boot extension artifacts on /system. " << error_msg; |
| LOG(INFO) << "Checking cache."; |
| } |
| |
| if (!cache_info.has_value()) { |
| // If the cache info file does not exist, it means on-device compilation has not been done |
| // before. |
| PLOG(INFO) << "No prior cache-info file: " << QuotePath(cache_info_filename_); |
| metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| // Check whether the current cache ART module info differs from the current ART module info. |
| const art_apex::ModuleInfo* cached_art_info = cache_info->getFirstArtModuleInfo(); |
| |
| if (cached_art_info == nullptr) { |
| LOG(INFO) << "Missing ART APEX info from cache-info."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| if (cached_art_info->getVersionCode() != art_apex_info.getVersionCode()) { |
| LOG(INFO) << "ART APEX version code mismatch (" << cached_art_info->getVersionCode() |
| << " != " << art_apex_info.getVersionCode() << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| if (cached_art_info->getVersionName() != art_apex_info.getVersionName()) { |
| LOG(INFO) << "ART APEX version name mismatch (" << cached_art_info->getVersionName() |
| << " != " << art_apex_info.getVersionName() << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| // Check lastUpdateMillis for samegrade installs. If `cached_art_info` is missing the |
| // lastUpdateMillis field then it is not current with the schema used by this binary so treat |
| // it as a samegrade update. Otherwise check whether the lastUpdateMillis changed. |
| const int64_t cached_art_last_update_millis = |
| cached_art_info->hasLastUpdateMillis() ? cached_art_info->getLastUpdateMillis() : -1; |
| if (cached_art_last_update_millis != art_apex_info.getLastUpdateMillis()) { |
| LOG(INFO) << "ART APEX last update time mismatch (" << cached_art_last_update_millis |
| << " != " << art_apex_info.getLastUpdateMillis() << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| // Check boot class components. |
| // |
| // This checks the size and checksums of odrefresh compilable files on the DEX2OATBOOTCLASSPATH |
| // (the Odrefresh constructor determines which files are compilable). If the number of files |
| // there changes, or their size or checksums change then compilation will be triggered. |
| // |
| // The boot class components may change unexpectedly, for example an OTA could update |
| // framework.jar. |
| const std::vector<art_apex::Component> expected_bcp_compilable_components = |
| GenerateBootExtensionCompilableComponents(); |
| if (expected_bcp_compilable_components.size() != 0 && |
| (!cache_info->hasDex2oatBootClasspath() || |
| !cache_info->getFirstDex2oatBootClasspath()->hasComponent())) { |
| LOG(INFO) << "Missing Dex2oatBootClasspath components."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| const std::vector<art_apex::Component>& bcp_compilable_components = |
| cache_info->getFirstDex2oatBootClasspath()->getComponent(); |
| if (!CheckComponents(expected_bcp_compilable_components, bcp_compilable_components, &error_msg)) { |
| LOG(INFO) << "Dex2OatClasspath components mismatch: " << error_msg; |
| metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| // Cache info looks good, check all compilation artifacts exist. |
| if (!BootExtensionArtifactsExist(/*on_system=*/false, isa, &error_msg)) { |
| LOG(INFO) << "Incomplete boot extension artifacts. " << error_msg; |
| metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| *cleanup_required = false; |
| return true; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::CheckSystemServerArtifactsAreUpToDate( |
| OdrMetrics& metrics, |
| const std::vector<apex::ApexInfo>& apex_info_list, |
| const std::optional<art_apex::CacheInfo>& cache_info, |
| /*out*/ bool* cleanup_required) const { |
| std::string error_msg; |
| |
| if (std::all_of(apex_info_list.begin(), |
| apex_info_list.end(), |
| [](const apex::ApexInfo& apex_info) { return apex_info.getIsFactory(); })) { |
| LOG(INFO) << "Factory APEXes mounted."; |
| |
| // APEXes are not updated, so we can use the artifacts on /system. Check if they exist. |
| if (SystemServerArtifactsExist(/*on_system=*/true, &error_msg)) { |
| // We don't need the artifacts on /data since we can use those on /system. |
| *cleanup_required = true; |
| return true; |
| } |
| |
| LOG(INFO) << "Incomplete system server artifacts on /system. " << error_msg; |
| LOG(INFO) << "Checking cache."; |
| } |
| |
| if (!cache_info.has_value()) { |
| // If the cache info file does not exist, it means on-device compilation has not been done |
| // before. |
| PLOG(INFO) << "No prior cache-info file: " << QuotePath(cache_info_filename_); |
| metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| // Check whether the current cached module info differs from the current module info. |
| const art_apex::ModuleInfoList* cached_module_info_list = cache_info->getFirstModuleInfoList(); |
| |
| if (cached_module_info_list == nullptr) { |
| LOG(INFO) << "Missing APEX info list from cache-info."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| std::unordered_map<std::string, const art_apex::ModuleInfo*> cached_module_info_map; |
| for (const art_apex::ModuleInfo& module_info : cached_module_info_list->getModuleInfo()) { |
| if (!module_info.hasName()) { |
| LOG(INFO) << "Unexpected module info from cache-info. Missing module name."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kUnknown); |
| *cleanup_required = true; |
| return false; |
| } |
| cached_module_info_map[module_info.getName()] = &module_info; |
| } |
| |
| for (const apex::ApexInfo& current_apex_info : apex_info_list) { |
| auto it = cached_module_info_map.find(current_apex_info.getModuleName()); |
| if (it == cached_module_info_map.end()) { |
| LOG(INFO) << "Missing APEX info from cache-info (" << current_apex_info.getModuleName() |
| << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| const art_apex::ModuleInfo* cached_module_info = it->second; |
| |
| if (cached_module_info->getVersionCode() != current_apex_info.getVersionCode()) { |
| LOG(INFO) << "APEX (" << current_apex_info.getModuleName() << ") version code mismatch (" |
| << cached_module_info->getVersionCode() |
| << " != " << current_apex_info.getVersionCode() << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| if (cached_module_info->getVersionName() != current_apex_info.getVersionName()) { |
| LOG(INFO) << "APEX (" << current_apex_info.getModuleName() << ") version name mismatch (" |
| << cached_module_info->getVersionName() |
| << " != " << current_apex_info.getVersionName() << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| if (!cached_module_info->hasLastUpdateMillis() || |
| cached_module_info->getLastUpdateMillis() != current_apex_info.getLastUpdateMillis()) { |
| LOG(INFO) << "APEX (" << current_apex_info.getModuleName() << ") last update time mismatch (" |
| << cached_module_info->getLastUpdateMillis() |
| << " != " << current_apex_info.getLastUpdateMillis() << ")."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch); |
| *cleanup_required = true; |
| return false; |
| } |
| } |
| |
| // Check system server components. |
| // |
| // This checks the size and checksums of odrefresh compilable files on the |
| // SYSTEMSERVERCLASSPATH (the Odrefresh constructor determines which files are compilable). If |
| // the number of files there changes, or their size or checksums change then compilation will be |
| // triggered. |
| // |
| // The system_server components may change unexpectedly, for example an OTA could update |
| // services.jar. |
| const std::vector<art_apex::Component> expected_system_server_components = |
| GenerateSystemServerComponents(); |
| if (expected_system_server_components.size() != 0 && |
| (!cache_info->hasSystemServerClasspath() || |
| !cache_info->getFirstSystemServerClasspath()->hasComponent())) { |
| LOG(INFO) << "Missing SystemServerClasspath components."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| const std::vector<art_apex::Component>& system_server_components = |
| cache_info->getFirstSystemServerClasspath()->getComponent(); |
| if (!CheckComponents(expected_system_server_components, system_server_components, &error_msg)) { |
| LOG(INFO) << "SystemServerClasspath components mismatch: " << error_msg; |
| metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| const std::vector<art_apex::Component> expected_bcp_components = |
| GenerateBootClasspathComponents(); |
| if (expected_bcp_components.size() != 0 && |
| (!cache_info->hasBootClasspath() || !cache_info->getFirstBootClasspath()->hasComponent())) { |
| LOG(INFO) << "Missing BootClasspath components."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged); |
| *cleanup_required = true; |
| return false; |
| } |
| |
| const std::vector<art_apex::Component>& bcp_components = |
| cache_info->getFirstBootClasspath()->getComponent(); |
| if (!CheckComponents(expected_bcp_components, bcp_components, &error_msg)) { |
| LOG(INFO) << "BootClasspath components mismatch: " << error_msg; |
| metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged); |
| // Boot classpath components can be dependencies of system_server components, so system_server |
| // components need to be recompiled if boot classpath components are changed. |
| *cleanup_required = true; |
| return false; |
| } |
| |
| if (!SystemServerArtifactsExist(/*on_system=*/false, &error_msg)) { |
| LOG(INFO) << "Incomplete system_server artifacts. " << error_msg; |
| // No clean-up is required here: we have boot extension artifacts. The method |
| // `SystemServerArtifactsExistOnData()` checks in compilation order so it is possible some of |
| // the artifacts are here. We likely ran out of space compiling the system_server artifacts. |
| // Any artifacts present are usable. |
| metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts); |
| *cleanup_required = false; |
| return false; |
| } |
| |
| *cleanup_required = false; |
| return true; |
| } |
| |
| WARN_UNUSED ExitCode OnDeviceRefresh::CheckArtifactsAreUpToDate( |
| OdrMetrics& metrics, |
| /*out*/ std::vector<InstructionSet>* compile_boot_extensions, |
| /*out*/ bool* compile_system_server) const { |
| metrics.SetStage(OdrMetrics::Stage::kCheck); |
| |
| // Clean-up helper used to simplify clean-ups and handling failures there. |
| auto cleanup_and_compile_all = [&, this]() { |
| *compile_boot_extensions = config_.GetBootExtensionIsas(); |
| *compile_system_server = true; |
| return RemoveArtifactsDirectory() ? ExitCode::kCompilationRequired : ExitCode::kCleanupFailed; |
| }; |
| |
| std::optional<std::vector<apex::ApexInfo>> apex_info_list = GetApexInfoList(); |
| if (!apex_info_list.has_value()) { |
| // This should never happen, further up-to-date checks are not possible if it does. |
| LOG(ERROR) << "Could not get APEX info."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kUnknown); |
| return cleanup_and_compile_all(); |
| } |
| |
| std::optional<apex::ApexInfo> art_apex_info = GetArtApexInfo(apex_info_list.value()); |
| if (!art_apex_info.has_value()) { |
| // This should never happen, further up-to-date checks are not possible if it does. |
| LOG(ERROR) << "Could not get ART APEX info."; |
| metrics.SetTrigger(OdrMetrics::Trigger::kUnknown); |
| return cleanup_and_compile_all(); |
| } |
| |
| // Record ART APEX version for metrics reporting. |
| metrics.SetArtApexVersion(art_apex_info->getVersionCode()); |
| |
| // Log the version so there's a starting point for any issues reported (b/197489543). |
| LOG(INFO) << "ART APEX version " << art_apex_info->getVersionCode(); |
| |
| // Record ART APEX last update milliseconds (used in compilation log). |
| metrics.SetArtApexLastUpdateMillis(art_apex_info->getLastUpdateMillis()); |
| |
| std::optional<art_apex::CacheInfo> cache_info = ReadCacheInfo(); |
| if (!cache_info.has_value() && OS::FileExists(cache_info_filename_.c_str())) { |
| // This should not happen unless odrefresh is updated to a new version that is not |
| // compatible with an old cache-info file. Further up-to-date checks are not possible if it |
| // does. |
| PLOG(ERROR) << "Failed to parse cache-info file: " << QuotePath(cache_info_filename_); |
| metrics.SetTrigger(OdrMetrics::Trigger::kUnknown); |
| return cleanup_and_compile_all(); |
| } |
| |
| InstructionSet system_server_isa = config_.GetSystemServerIsa(); |
| bool cleanup_required; |
| |
| for (const InstructionSet isa : config_.GetBootExtensionIsas()) { |
| cleanup_required = false; |
| if (!CheckBootExtensionArtifactsAreUpToDate( |
| metrics, isa, art_apex_info.value(), cache_info, &cleanup_required)) { |
| compile_boot_extensions->push_back(isa); |
| // system_server artifacts are invalid without valid boot extension artifacts. |
| if (isa == system_server_isa) { |
| *compile_system_server = true; |
| if (!RemoveSystemServerArtifactsFromData()) { |
| return ExitCode::kCleanupFailed; |
| } |
| } |
| } |
| if (cleanup_required) { |
| if (!RemoveBootExtensionArtifactsFromData(isa)) { |
| return ExitCode::kCleanupFailed; |
| } |
| } |
| } |
| |
| cleanup_required = false; |
| if (!*compile_system_server && |
| !CheckSystemServerArtifactsAreUpToDate( |
| metrics, apex_info_list.value(), cache_info, &cleanup_required)) { |
| *compile_system_server = true; |
| } |
| if (cleanup_required) { |
| if (!RemoveSystemServerArtifactsFromData()) { |
| return ExitCode::kCleanupFailed; |
| } |
| } |
| |
| return (!compile_boot_extensions->empty() || *compile_system_server) ? |
| ExitCode::kCompilationRequired : |
| ExitCode::kOkay; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::VerifyBootExtensionArtifactsAreUpToDate(const InstructionSet isa, |
| bool on_system) const { |
| const std::string dex_file = boot_extension_compilable_jars_.front(); |
| const std::string image_location = GetBootImageExtensionImage(on_system); |
| |
| std::vector<std::string> args; |
| args.emplace_back(config_.GetDexOptAnalyzer()); |
| args.emplace_back("--validate-bcp"); |
| args.emplace_back(Concatenate({"--image=", GetBootImage(), ":", image_location})); |
| args.emplace_back(Concatenate({"--isa=", GetInstructionSetString(isa)})); |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back(Concatenate({"-Xbootclasspath:", config_.GetDex2oatBootClasspath()})); |
| |
| LOG(INFO) << "Checking " << dex_file << ": " << android::base::Join(args, ' '); |
| |
| std::string error_msg; |
| bool timed_out = false; |
| const time_t timeout = GetSubprocessTimeout(); |
| const int dexoptanalyzer_result = |
| exec_utils_->ExecAndReturnCode(args, timeout, &timed_out, &error_msg); |
| if (dexoptanalyzer_result == -1) { |
| LOG(ERROR) << "Unexpected exit from dexoptanalyzer: " << error_msg; |
| if (timed_out) { |
| // TODO(oth): record metric for timeout. |
| } |
| return false; |
| } |
| auto rc = static_cast<dexoptanalyzer::ReturnCode>(dexoptanalyzer_result); |
| if (rc == dexoptanalyzer::ReturnCode::kNoDexOptNeeded) { |
| return true; |
| } |
| return false; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::VerifyBootExtensionArtifactsAreUpToDate( |
| InstructionSet isa) const { |
| bool system_ok = VerifyBootExtensionArtifactsAreUpToDate(isa, /*on_system=*/true); |
| LOG(INFO) << "Boot extension artifacts on /system are " << (system_ok ? "ok" : "stale"); |
| bool data_ok = VerifyBootExtensionArtifactsAreUpToDate(isa, /*on_system=*/false); |
| LOG(INFO) << "Boot extension artifacts on /data are " << (data_ok ? "ok" : "stale"); |
| return system_ok || data_ok; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::VerifySystemServerArtifactsAreUpToDate(bool on_system) const { |
| std::vector<std::string> classloader_context; |
| for (const std::string& jar_path : systemserver_compilable_jars_) { |
| std::vector<std::string> args; |
| args.emplace_back(config_.GetDexOptAnalyzer()); |
| args.emplace_back("--dex-file=" + jar_path); |
| |
| const std::string image_location = GetSystemServerImagePath(on_system, jar_path); |
| |
| // odrefresh produces app-image files, but these are not guaranteed for those pre-installed |
| // on /system. |
| if (!on_system && !OS::FileExists(image_location.c_str(), true)) { |
| LOG(INFO) << "Missing image file: " << QuotePath(image_location); |
| return false; |
| } |
| |
| // Generate set of artifacts that are output by compilation. |
| OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location); |
| if (!on_system) { |
| CHECK_EQ(artifacts.OatPath(), |
| GetApexDataOdexFilename(jar_path, config_.GetSystemServerIsa())); |
| CHECK_EQ(artifacts.ImagePath(), |
| GetApexDataDalvikCacheFilename(jar_path, config_.GetSystemServerIsa(), "art")); |
| CHECK_EQ(artifacts.OatPath(), |
| GetApexDataDalvikCacheFilename(jar_path, config_.GetSystemServerIsa(), "odex")); |
| CHECK_EQ(artifacts.VdexPath(), |
| GetApexDataDalvikCacheFilename(jar_path, config_.GetSystemServerIsa(), "vdex")); |
| } |
| |
| // Associate inputs and outputs with dexoptanalyzer arguments. |
| std::pair<const std::string, const char*> location_args[] = { |
| std::make_pair(artifacts.OatPath(), "--oat-fd="), |
| std::make_pair(artifacts.VdexPath(), "--vdex-fd="), |
| std::make_pair(jar_path, "--zip-fd=")}; |
| |
| // Open file descriptors for dexoptanalyzer file inputs and add to the command-line. |
| std::vector<std::unique_ptr<File>> files; |
| for (const auto& location_arg : location_args) { |
| auto& [location, arg] = location_arg; |
| std::unique_ptr<File> file(OS::OpenFileForReading(location.c_str())); |
| if (file == nullptr) { |
| PLOG(ERROR) << "Failed to open \"" << location << "\""; |
| return false; |
| } |
| args.emplace_back(android::base::StringPrintf("%s%d", arg, file->Fd())); |
| files.emplace_back(file.release()); |
| } |
| |
| const std::string basename(android::base::Basename(jar_path)); |
| const std::string root = GetAndroidRoot(); |
| const std::string profile_file = Concatenate({root, "/framework/", basename, ".prof"}); |
| if (OS::FileExists(profile_file.c_str())) { |
| args.emplace_back("--compiler-filter=speed-profile"); |
| } else { |
| args.emplace_back("--compiler-filter=speed"); |
| } |
| |
| args.emplace_back( |
| Concatenate({"--image=", GetBootImage(), ":", GetBootImageExtensionImage(on_system)})); |
| args.emplace_back( |
| Concatenate({"--isa=", GetInstructionSetString(config_.GetSystemServerIsa())})); |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back(Concatenate({"-Xbootclasspath:", config_.GetBootClasspath()})); |
| args.emplace_back(Concatenate( |
| {"--class-loader-context=PCL[", android::base::Join(classloader_context, ':'), "]"})); |
| |
| classloader_context.emplace_back(jar_path); |
| |
| LOG(INFO) << "Checking " << jar_path << ": " << android::base::Join(args, ' '); |
| std::string error_msg; |
| bool timed_out = false; |
| const time_t timeout = GetSubprocessTimeout(); |
| const int dexoptanalyzer_result = |
| exec_utils_->ExecAndReturnCode(args, timeout, &timed_out, &error_msg); |
| if (dexoptanalyzer_result == -1) { |
| LOG(ERROR) << "Unexpected exit from dexoptanalyzer: " << error_msg; |
| if (timed_out) { |
| // TODO(oth): record metric for timeout. |
| } |
| return false; |
| } |
| LOG(INFO) << "dexoptanalyzer returned " << dexoptanalyzer_result; |
| |
| bool unexpected_result = true; |
| switch (static_cast<dexoptanalyzer::ReturnCode>(dexoptanalyzer_result)) { |
| case art::dexoptanalyzer::ReturnCode::kNoDexOptNeeded: |
| unexpected_result = false; |
| break; |
| |
| // Recompile needed |
| case art::dexoptanalyzer::ReturnCode::kDex2OatFromScratch: |
| case art::dexoptanalyzer::ReturnCode::kDex2OatForBootImageOat: |
| case art::dexoptanalyzer::ReturnCode::kDex2OatForFilterOat: |
| case art::dexoptanalyzer::ReturnCode::kDex2OatForBootImageOdex: |
| case art::dexoptanalyzer::ReturnCode::kDex2OatForFilterOdex: |
| return false; |
| |
| // Unexpected issues (note no default-case here to catch missing enum values, but the |
| // return code from dexoptanalyzer may also be outside expected values, such as a |
| // process crash. |
| case art::dexoptanalyzer::ReturnCode::kFlattenClassLoaderContextSuccess: |
| case art::dexoptanalyzer::ReturnCode::kErrorInvalidArguments: |
| case art::dexoptanalyzer::ReturnCode::kErrorCannotCreateRuntime: |
| case art::dexoptanalyzer::ReturnCode::kErrorUnknownDexOptNeeded: |
| break; |
| } |
| |
| if (unexpected_result) { |
| LOG(ERROR) << "Unexpected result from dexoptanalyzer: " << dexoptanalyzer_result; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::VerifySystemServerArtifactsAreUpToDate() const { |
| bool system_ok = VerifySystemServerArtifactsAreUpToDate(/*on_system=*/true); |
| LOG(INFO) << "system_server artifacts on /system are " << (system_ok ? "ok" : "stale"); |
| bool data_ok = VerifySystemServerArtifactsAreUpToDate(/*on_system=*/false); |
| LOG(INFO) << "system_server artifacts on /data are " << (data_ok ? "ok" : "stale"); |
| return system_ok || data_ok; |
| } |
| |
| WARN_UNUSED ExitCode OnDeviceRefresh::VerifyArtifactsAreUpToDate() const { |
| ExitCode exit_code = ExitCode::kOkay; |
| for (const InstructionSet isa : config_.GetBootExtensionIsas()) { |
| if (!VerifyBootExtensionArtifactsAreUpToDate(isa)) { |
| // system_server artifacts are invalid without valid boot extension artifacts. |
| if (!RemoveSystemServerArtifactsFromData() || !RemoveBootExtensionArtifactsFromData(isa)) { |
| return ExitCode::kCleanupFailed; |
| } |
| exit_code = ExitCode::kCompilationRequired; |
| } |
| } |
| if (!VerifySystemServerArtifactsAreUpToDate()) { |
| if (!RemoveSystemServerArtifactsFromData()) { |
| return ExitCode::kCleanupFailed; |
| } |
| exit_code = ExitCode::kCompilationRequired; |
| } |
| return exit_code; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::CompileBootExtensionArtifacts(const InstructionSet isa, |
| const std::string& staging_dir, |
| OdrMetrics& metrics, |
| uint32_t* dex2oat_invocation_count, |
| std::string* error_msg) const { |
| ScopedOdrCompilationTimer compilation_timer(metrics); |
| std::vector<std::string> args; |
| args.push_back(config_.GetDex2Oat()); |
| |
| AddDex2OatCommonOptions(&args); |
| AddDex2OatConcurrencyArguments(&args); |
| AddDex2OatDebugInfo(&args); |
| AddDex2OatInstructionSet(&args, isa); |
| |
| std::vector<std::unique_ptr<File>> readonly_files_raii; |
| const std::string boot_profile_file(GetAndroidRoot() + "/etc/boot-image.prof"); |
| AddDex2OatProfileAndCompilerFilter(&args, &readonly_files_raii, boot_profile_file); |
| |
| // Compile as a single image for fewer files and slightly less memory overhead. |
| args.emplace_back("--single-image"); |
| |
| // Set boot-image and expectation of compiling boot classpath extensions. |
| args.emplace_back("--boot-image=" + GetBootImage()); |
| |
| const std::string dirty_image_objects_file(GetAndroidRoot() + "/etc/dirty-image-objects"); |
| if (OS::FileExists(dirty_image_objects_file.c_str())) { |
| std::unique_ptr<File> file(OS::OpenFileForReading(dirty_image_objects_file.c_str())); |
| args.emplace_back(android::base::StringPrintf("--dirty-image-objects-fd=%d", file->Fd())); |
| readonly_files_raii.push_back(std::move(file)); |
| } else { |
| LOG(WARNING) << "Missing dirty objects file : " << QuotePath(dirty_image_objects_file); |
| } |
| |
| // Add boot extensions to compile. |
| for (const std::string& component : boot_extension_compilable_jars_) { |
| args.emplace_back("--dex-file=" + component); |
| std::unique_ptr<File> file(OS::OpenFileForReading(component.c_str())); |
| args.emplace_back(android::base::StringPrintf("--dex-fd=%d", file->Fd())); |
| readonly_files_raii.push_back(std::move(file)); |
| } |
| |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back(Concatenate({"-Xbootclasspath:", config_.GetDex2oatBootClasspath()})); |
| auto bcp_jars = android::base::Split(config_.GetDex2oatBootClasspath(), ":"); |
| if (!AddBootClasspathFds(args, readonly_files_raii, bcp_jars)) { |
| return false; |
| } |
| |
| const std::string image_location = GetBootImageExtensionImagePath(/*on_system=*/false, isa); |
| const OdrArtifacts artifacts = OdrArtifacts::ForBootImageExtension(image_location); |
| CHECK_EQ(GetApexDataOatFilename(boot_extension_compilable_jars_.front().c_str(), isa), |
| artifacts.OatPath()); |
| |
| args.emplace_back("--oat-location=" + artifacts.OatPath()); |
| const std::pair<const std::string, const char*> location_kind_pairs[] = { |
| std::make_pair(artifacts.ImagePath(), "image"), |
| std::make_pair(artifacts.OatPath(), "oat"), |
| std::make_pair(artifacts.VdexPath(), "output-vdex")}; |
| |
| std::vector<std::unique_ptr<File>> staging_files; |
| for (const auto& location_kind_pair : location_kind_pairs) { |
| auto& [location, kind] = location_kind_pair; |
| const std::string staging_location = GetStagingLocation(staging_dir, location); |
| std::unique_ptr<File> staging_file(OS::CreateEmptyFile(staging_location.c_str())); |
| if (staging_file == nullptr) { |
| PLOG(ERROR) << "Failed to create " << kind << " file: " << staging_location; |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| EraseFiles(staging_files); |
| return false; |
| } |
| |
| if (fchmod(staging_file->Fd(), S_IRUSR | S_IWUSR) != 0) { |
| PLOG(ERROR) << "Could not set file mode on " << QuotePath(staging_location); |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| EraseFiles(staging_files); |
| return false; |
| } |
| |
| args.emplace_back(android::base::StringPrintf("--%s-fd=%d", kind, staging_file->Fd())); |
| staging_files.emplace_back(std::move(staging_file)); |
| } |
| |
| const std::string install_location = android::base::Dirname(image_location); |
| if (!EnsureDirectoryExists(install_location)) { |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| return false; |
| } |
| |
| if (config_.UseCompilationOs()) { |
| std::vector<std::string> prefix_args = { |
| "/apex/com.android.compos/bin/pvm_exec", |
| "--cid=" + config_.GetCompilationOsAddress(), |
| "--in-fd=" + JoinFilesAsFDs(readonly_files_raii, ','), |
| "--out-fd=" + JoinFilesAsFDs(staging_files, ','), |
| "--", |
| }; |
| args.insert(args.begin(), prefix_args.begin(), prefix_args.end()); |
| } |
| |
| const time_t timeout = GetSubprocessTimeout(); |
| const std::string cmd_line = android::base::Join(args, ' '); |
| LOG(INFO) << "Compiling boot extensions (" << isa << "): " << cmd_line << " [timeout " << timeout |
| << "s]"; |
| if (config_.GetDryRun()) { |
| LOG(INFO) << "Compilation skipped (dry-run)."; |
| return true; |
| } |
| |
| bool timed_out = false; |
| int dex2oat_exit_code = exec_utils_->ExecAndReturnCode(args, timeout, &timed_out, error_msg); |
| if (dex2oat_exit_code != 0) { |
| if (timed_out) { |
| metrics.SetStatus(OdrMetrics::Status::kTimeLimitExceeded); |
| } else { |
| metrics.SetStatus(OdrMetrics::Status::kDex2OatError); |
| } |
| EraseFiles(staging_files); |
| return false; |
| } |
| |
| if (!MoveOrEraseFiles(staging_files, install_location)) { |
| metrics.SetStatus(OdrMetrics::Status::kInstallFailed); |
| return false; |
| } |
| |
| *dex2oat_invocation_count = *dex2oat_invocation_count + 1; |
| ReportNextBootAnimationProgress(*dex2oat_invocation_count); |
| |
| return true; |
| } |
| |
| WARN_UNUSED bool OnDeviceRefresh::CompileSystemServerArtifacts(const std::string& staging_dir, |
| OdrMetrics& metrics, |
| uint32_t* dex2oat_invocation_count, |
| std::string* error_msg) const { |
| ScopedOdrCompilationTimer compilation_timer(metrics); |
| std::vector<std::string> classloader_context; |
| |
| const std::string dex2oat = config_.GetDex2Oat(); |
| const InstructionSet isa = config_.GetSystemServerIsa(); |
| for (const std::string& jar : systemserver_compilable_jars_) { |
| std::vector<std::unique_ptr<File>> readonly_files_raii; |
| std::vector<std::string> args; |
| args.emplace_back(dex2oat); |
| args.emplace_back("--dex-file=" + jar); |
| |
| std::unique_ptr<File> dex_file(OS::OpenFileForReading(jar.c_str())); |
| args.emplace_back(android::base::StringPrintf("--dex-fd=%d", dex_file->Fd())); |
| readonly_files_raii.push_back(std::move(dex_file)); |
| |
| AddDex2OatCommonOptions(&args); |
| AddDex2OatConcurrencyArguments(&args); |
| AddDex2OatDebugInfo(&args); |
| AddDex2OatInstructionSet(&args, isa); |
| const std::string jar_name(android::base::Basename(jar)); |
| const std::string profile = Concatenate({GetAndroidRoot(), "/framework/", jar_name, ".prof"}); |
| std::string compiler_filter = |
| android::base::GetProperty("dalvik.vm.systemservercompilerfilter", "speed"); |
| if (compiler_filter == "speed-profile") { |
| AddDex2OatProfileAndCompilerFilter(&args, &readonly_files_raii, profile); |
| } else { |
| args.emplace_back("--compiler-filter=" + compiler_filter); |
| } |
| |
| const std::string image_location = GetSystemServerImagePath(/*on_system=*/false, jar); |
| const std::string install_location = android::base::Dirname(image_location); |
| if (classloader_context.empty()) { |
| // All images are in the same directory, we only need to check on the first iteration. |
| if (!EnsureDirectoryExists(install_location)) { |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| return false; |
| } |
| } |
| |
| OdrArtifacts artifacts = OdrArtifacts::ForSystemServer(image_location); |
| CHECK_EQ(artifacts.OatPath(), GetApexDataOdexFilename(jar.c_str(), isa)); |
| |
| const std::pair<const std::string, const char*> location_kind_pairs[] = { |
| std::make_pair(artifacts.ImagePath(), "app-image"), |
| std::make_pair(artifacts.OatPath(), "oat"), |
| std::make_pair(artifacts.VdexPath(), "output-vdex")}; |
| |
| std::vector<std::unique_ptr<File>> staging_files; |
| for (const auto& location_kind_pair : location_kind_pairs) { |
| auto& [location, kind] = location_kind_pair; |
| const std::string staging_location = GetStagingLocation(staging_dir, location); |
| std::unique_ptr<File> staging_file(OS::CreateEmptyFile(staging_location.c_str())); |
| if (staging_file == nullptr) { |
| PLOG(ERROR) << "Failed to create " << kind << " file: " << staging_location; |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| EraseFiles(staging_files); |
| return false; |
| } |
| args.emplace_back(android::base::StringPrintf("--%s-fd=%d", kind, staging_file->Fd())); |
| staging_files.emplace_back(std::move(staging_file)); |
| } |
| args.emplace_back("--oat-location=" + artifacts.OatPath()); |
| |
| if (!config_.GetUpdatableBcpPackagesFile().empty()) { |
| const std::string& bcp_packages = config_.GetUpdatableBcpPackagesFile(); |
| if (!OS::FileExists(bcp_packages.c_str())) { |
| *error_msg = "Cannot compile system_server JARs: missing " + QuotePath(bcp_packages); |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| EraseFiles(staging_files); |
| return false; |
| } |
| std::unique_ptr<File> file(OS::OpenFileForReading(bcp_packages.c_str())); |
| args.emplace_back(android::base::StringPrintf("--updatable-bcp-packages-fd=%d", file->Fd())); |
| readonly_files_raii.push_back(std::move(file)); |
| } |
| |
| args.emplace_back("--runtime-arg"); |
| args.emplace_back(Concatenate({"-Xbootclasspath:", config_.GetBootClasspath()})); |
| auto bcp_jars = android::base::Split(config_.GetBootClasspath(), ":"); |
| if (!AddBootClasspathFds(args, readonly_files_raii, bcp_jars)) { |
| return false; |
| } |
| AddCompiledBootClasspathFdsIfAny(args, readonly_files_raii, bcp_jars, isa); |
| |
| const std::string context_path = android::base::Join(classloader_context, ':'); |
| args.emplace_back(Concatenate({"--class-loader-context=PCL[", context_path, "]"})); |
| if (!classloader_context.empty()) { |
| std::vector<int> fds; |
| for (const std::string& path : classloader_context) { |
| std::unique_ptr<File> file(OS::OpenFileForReading(path.c_str())); |
| if (!file->IsValid()) { |
| PLOG(ERROR) << "Failed to open classloader context " << path; |
| metrics.SetStatus(OdrMetrics::Status::kIoError); |
| return false; |
| } |
| fds.emplace_back(file->Fd()); |
| readonly_files_raii.emplace_back(std::move(file)); |
| } |
| const std::string context_fds = android::base::Join(fds, ':'); |
| args.emplace_back(Concatenate({"--class-loader-context-fds=", context_fds})); |
| } |
| const std::string extension_image = GetBootImageExtensionImage(/*on_system=*/false); |
| args.emplace_back(Concatenate({"--boot-image=", GetBootImage(), ":", extension_image})); |
| |
| if (config_.UseCompilationOs()) { |
| std::vector<std::string> prefix_args = { |
| "/apex/com.android.compos/bin/pvm_exec", |
| "--cid=" + config_.GetCompilationOsAddress(), |
| "--in-fd=" + JoinFilesAsFDs(readonly_files_raii, ','), |
| "--out-fd=" + JoinFilesAsFDs(staging_files, ','), |
| "--", |
| }; |
| args.insert(args.begin(), prefix_args.begin(), prefix_args.end()); |
| } |
| |
| const time_t timeout = GetSubprocessTimeout(); |
| const std::string cmd_line = android::base::Join(args, ' '); |
| LOG(INFO) << "Compiling " << jar << ": " << cmd_line << " [timeout " << timeout << "s]"; |
| if (config_.GetDryRun()) { |
| LOG(INFO) << "Compilation skipped (dry-run)."; |
| return true; |
| } |
| |
| bool timed_out = false; |
| int dex2oat_exit_code = exec_utils_->ExecAndReturnCode(args, timeout, &timed_out, error_msg); |
| if (dex2oat_exit_code != 0) { |
| if (timed_out) { |
| metrics.SetStatus(OdrMetrics::Status::kTimeLimitExceeded); |
| } else { |
| metrics.SetStatus(OdrMetrics::Status::kDex2OatError); |
| } |
| EraseFiles(staging_files); |
| return false; |
| } |
| |
| if (!MoveOrEraseFiles(staging_files, install_location)) { |
| metrics.SetStatus(OdrMetrics::Status::kInstallFailed); |
| return false; |
| } |
| |
| *dex2oat_invocation_count = *dex2oat_invocation_count + 1; |
| ReportNextBootAnimationProgress(*dex2oat_invocation_count); |
| classloader_context.emplace_back(jar); |
| } |
| |
| return true; |
| } |
| |
| WARN_UNUSED ExitCode |
| OnDeviceRefresh::Compile(OdrMetrics& metrics, |
| const std::vector<InstructionSet>& compile_boot_extensions, |
| bool compile_system_server) const { |
| const char* staging_dir = nullptr; |
| metrics.SetStage(OdrMetrics::Stage::kPreparation); |
| |
| if (!config_.GetStagingDir().empty()) { |
| staging_dir = config_.GetStagingDir().c_str(); |
| } else { |
| // Create staging area and assign label for generating compilation artifacts. |
| if (PaletteCreateOdrefreshStagingDirectory(&staging_dir) != PALETTE_STATUS_OK) { |
| metrics.SetStatus(OdrMetrics::Status::kStagingFailed); |
| return ExitCode::kCleanupFailed; |
| } |
| } |
| |
| // Emit cache info before compiling. This can be used to throttle compilation attempts later. |
| WriteCacheInfo(); |
| |
| std::string error_msg; |
| |
| uint32_t dex2oat_invocation_count = 0; |
| ReportNextBootAnimationProgress(dex2oat_invocation_count); |
| |
| const auto& bcp_instruction_sets = config_.GetBootExtensionIsas(); |
| DCHECK(!bcp_instruction_sets.empty() && bcp_instruction_sets.size() <= 2); |
| for (const InstructionSet isa : compile_boot_extensions) { |
| auto stage = (isa == bcp_instruction_sets.front()) ? OdrMetrics::Stage::kPrimaryBootClasspath : |
| OdrMetrics::Stage::kSecondaryBootClasspath; |
| metrics.SetStage(stage); |
| if (!CheckCompilationSpace()) { |
| metrics.SetStatus(OdrMetrics::Status::kNoSpace); |
| // Return kOkay so odsign will keep and sign whatever we have been able to compile. |
| return ExitCode::kOkay; |
| } |
| |
| if (!CompileBootExtensionArtifacts( |
| isa, staging_dir, metrics, &dex2oat_invocation_count, &error_msg)) { |
| LOG(ERROR) << "Compilation of BCP failed: " << error_msg; |
| if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) { |
| return ExitCode::kCleanupFailed; |
| } |
| return ExitCode::kCompilationFailed; |
| } |
| } |
| |
| if (compile_system_server) { |
| metrics.SetStage(OdrMetrics::Stage::kSystemServerClasspath); |
| |
| if (!CheckCompilationSpace()) { |
| metrics.SetStatus(OdrMetrics::Status::kNoSpace); |
| // Return kOkay so odsign will keep and sign whatever we have been able to compile. |
| return ExitCode::kOkay; |
| } |
| |
| if (!CompileSystemServerArtifacts( |
| staging_dir, metrics, &dex2oat_invocation_count, &error_msg)) { |
| LOG(ERROR) << "Compilation of system_server failed: " << error_msg; |
| if (!config_.GetDryRun() && !RemoveDirectory(staging_dir)) { |
| return ExitCode::kCleanupFailed; |
| } |
| return ExitCode::kCompilationFailed; |
| } |
| } |
| |
| metrics.SetStage(OdrMetrics::Stage::kComplete); |
| return ExitCode::kCompilationSuccess; |
| } |
| |
| } // namespace odrefresh |
| } // namespace art |