odrefresh: add metrics support
Adds metrics to stages of odrefresh.
Bug: 169925964
Test: atest art_odrefresh_tests
Test: atest --host art_odrefresh_tests
(cherry picked from commit 3d877f082636f26ad57c92e3aae1525faacff51b)
Merged-In: I768ce5f122b0c1b839f4cdf55aa6dafb68708eb2
Change-Id: I8355fd38c28e41b04f0ea52384061b686cb1e362
diff --git a/odrefresh/Android.bp b/odrefresh/Android.bp
index 6f84e8f..8a9acd3 100644
--- a/odrefresh/Android.bp
+++ b/odrefresh/Android.bp
@@ -30,12 +30,15 @@
srcs: [
"odrefresh.cc",
"odr_fs_utils.cc",
+ "odr_metrics.cc",
+ "odr_metrics_record.cc",
],
local_include_dirs: ["include"],
header_libs: ["dexoptanalyzer_headers"],
generated_sources: [
"apex-info-list",
"art-apex-cache-info",
+ "art-odrefresh-operator-srcs",
],
shared_libs: [
"libartpalette",
@@ -81,6 +84,16 @@
visibility: ["//visibility:public"],
}
+gensrcs {
+ name: "art-odrefresh-operator-srcs",
+ cmd: "$(location generate_operator_out) art/odrefresh $(in) > $(out)",
+ tools: ["generate_operator_out"],
+ srcs: [
+ "odr_metrics.h",
+ ],
+ output_extension: "operator_out.cc",
+}
+
art_cc_binary {
name: "odrefresh",
defaults: ["odrefresh-defaults"],
@@ -126,16 +139,19 @@
defaults: [
"art_gtest_defaults",
],
+ generated_sources: ["art-odrefresh-operator-srcs"],
header_libs: ["odrefresh_headers"],
srcs: [
"odr_artifacts_test.cc",
"odr_fs_utils.cc",
"odr_fs_utils_test.cc",
+ "odr_metrics.cc",
+ "odr_metrics_test.cc",
+ "odr_metrics_record.cc",
+ "odr_metrics_record_test.cc",
"odrefresh_test.cc",
],
- shared_libs: [
- "libbase",
- ],
+ shared_libs: ["libbase"],
}
xsd_config {
diff --git a/odrefresh/odr_metrics.cc b/odrefresh/odr_metrics.cc
new file mode 100644
index 0000000..4bddb17
--- /dev/null
+++ b/odrefresh/odr_metrics.cc
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2021 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 "odr_metrics.h"
+
+#include <unistd.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <fstream>
+#include <iosfwd>
+#include <optional>
+#include <ostream>
+#include <string>
+
+#include <android-base/logging.h>
+#include <base/os.h>
+#include <base/string_view_cpp20.h>
+#include <odr_fs_utils.h>
+#include <odr_metrics_record.h>
+
+namespace art {
+namespace odrefresh {
+
+OdrMetrics::OdrMetrics(const std::string& cache_directory, const std::string& metrics_file)
+ : cache_directory_(cache_directory), metrics_file_(metrics_file), status_(Status::kOK) {
+ DCHECK(StartsWith(metrics_file_, "/"));
+
+ // Remove existing metrics file if it exists.
+ if (OS::FileExists(metrics_file.c_str())) {
+ if (unlink(metrics_file.c_str()) != 0) {
+ PLOG(ERROR) << "Failed to remove metrics file '" << metrics_file << "'";
+ }
+ }
+
+ // Create apexdata dalvik-cache directory if it does not exist. It is required before
+ // calling GetFreeSpaceMiB().
+ if (!EnsureDirectoryExists(cache_directory)) {
+ // This should never fail except for no space on device or configuration issues (e.g. SELinux).
+ LOG(WARNING) << "Cache directory '" << cache_directory << "' could not be created.";
+ }
+ cache_space_free_start_mib_ = GetFreeSpaceMiB(cache_directory);
+}
+
+OdrMetrics::~OdrMetrics() {
+ cache_space_free_end_mib_ = GetFreeSpaceMiB(cache_directory_);
+
+ // Log metrics only if odrefresh detected a reason to compile.
+ if (trigger_.has_value()) {
+ WriteToFile(metrics_file_, this);
+ }
+}
+
+void OdrMetrics::SetCompilationTime(int32_t seconds) {
+ switch (stage_) {
+ case Stage::kPrimaryBootClasspath:
+ primary_bcp_compilation_seconds_ = seconds;
+ break;
+ case Stage::kSecondaryBootClasspath:
+ secondary_bcp_compilation_seconds_ = seconds;
+ break;
+ case Stage::kSystemServerClasspath:
+ system_server_compilation_seconds_ = seconds;
+ break;
+ case Stage::kCheck:
+ case Stage::kComplete:
+ case Stage::kPreparation:
+ case Stage::kUnknown:
+ break;
+ }
+}
+
+void OdrMetrics::SetStage(Stage stage) {
+ if (status_ == Status::kOK) {
+ stage_ = stage;
+ }
+}
+
+int32_t OdrMetrics::GetFreeSpaceMiB(const std::string& path) {
+ static constexpr uint32_t kBytesPerMiB = 1024 * 1024;
+ static constexpr uint64_t kNominalMaximumCacheBytes = 1024 * kBytesPerMiB;
+
+ // Assume nominal cache space is 1GiB (much larger than expected, ~100MB).
+ uint64_t used_space_bytes;
+ if (!GetUsedSpace(path, &used_space_bytes)) {
+ used_space_bytes = 0;
+ }
+ uint64_t nominal_free_space_bytes = kNominalMaximumCacheBytes - used_space_bytes;
+
+ // Get free space on partition containing `path`.
+ uint64_t free_space_bytes;
+ if (!GetFreeSpace(path, &free_space_bytes)) {
+ free_space_bytes = kNominalMaximumCacheBytes;
+ }
+
+ // Pick the smallest free space, ie space on partition or nominal space in cache.
+ // There are two things of interest for metrics:
+ // (i) identifying failed compilations due to low space.
+ // (ii) understanding what the storage requirements are for the spectrum of boot classpaths and
+ // system_server classpaths.
+ uint64_t free_space_mib = std::min(free_space_bytes, nominal_free_space_bytes) / kBytesPerMiB;
+ return static_cast<int32_t>(free_space_mib);
+}
+
+bool OdrMetrics::ToRecord(/*out*/OdrMetricsRecord* record) const {
+ if (!trigger_.has_value()) {
+ return false;
+ }
+ record->art_apex_version = art_apex_version_;
+ record->trigger = static_cast<uint32_t>(trigger_.value());
+ record->stage_reached = static_cast<uint32_t>(stage_);
+ record->status = static_cast<uint32_t>(status_);
+ record->primary_bcp_compilation_seconds = primary_bcp_compilation_seconds_;
+ record->secondary_bcp_compilation_seconds = secondary_bcp_compilation_seconds_;
+ record->system_server_compilation_seconds = system_server_compilation_seconds_;
+ record->cache_space_free_start_mib = cache_space_free_start_mib_;
+ record->cache_space_free_end_mib = cache_space_free_end_mib_;
+ return true;
+}
+
+void OdrMetrics::WriteToFile(const std::string& path, const OdrMetrics* metrics) {
+ OdrMetricsRecord record;
+ if (!metrics->ToRecord(&record)) {
+ LOG(ERROR) << "Attempting to report metrics without a compilation trigger.";
+ return;
+ }
+
+ // Preserve order from frameworks/proto_logging/stats/atoms.proto in metrics file written.
+ std::ofstream ofs(path);
+ ofs << record;
+}
+
+} // namespace odrefresh
+} // namespace art
diff --git a/odrefresh/odr_metrics.h b/odrefresh/odr_metrics.h
new file mode 100644
index 0000000..8b8d5ff
--- /dev/null
+++ b/odrefresh/odr_metrics.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#ifndef ART_ODREFRESH_ODR_METRICS_H_
+#define ART_ODREFRESH_ODR_METRICS_H_
+
+#include <chrono>
+#include <cstdint>
+#include <iosfwd>
+#include <optional>
+#include <string>
+
+#include "base/macros.h"
+#include "odr_metrics_record.h"
+
+namespace art {
+namespace odrefresh {
+
+class OdrMetrics final {
+ public:
+ // Enumeration used to track the latest stage reached running odrefresh.
+ //
+ // These values mirror those in OdrefreshReported::Stage in frameworks/proto_logging/atoms.proto.
+ // NB There are gaps between the values in case an additional stages are introduced.
+ enum class Stage : uint8_t {
+ kUnknown = 0,
+ kCheck = 10,
+ kPreparation = 20,
+ kPrimaryBootClasspath = 30,
+ kSecondaryBootClasspath = 40,
+ kSystemServerClasspath = 50,
+ kComplete = 60,
+ };
+
+ // Enumeration describing the overall status, processing stops on the first error discovered.
+ //
+ // These values mirror those in OdrefreshReported::Status in frameworks/proto_logging/atoms.proto.
+ enum class Status : uint8_t {
+ kUnknown = 0,
+ kOK = 1,
+ kNoSpace = 2,
+ kIoError = 3,
+ kDex2OatError = 4,
+ kTimeLimitExceeded = 5,
+ kStagingFailed = 6,
+ kInstallFailed = 7,
+ };
+
+ // Enumeration describing the cause of compilation (if any) in odrefresh.
+ //
+ // These values mirror those in OdrefreshReported::Trigger in
+ // frameworks/proto_logging/atoms.proto.
+ enum class Trigger : uint8_t {
+ kUnknown = 0,
+ kApexVersionMismatch = 1,
+ kDexFilesChanged = 2,
+ kMissingArtifacts = 3,
+ };
+
+ explicit OdrMetrics(const std::string& cache_directory,
+ const std::string& metrics_file = kOdrefreshMetricsFile);
+ ~OdrMetrics();
+
+ // Sets the ART APEX that metrics are being collected on behalf of.
+ void SetArtApexVersion(int64_t version) {
+ art_apex_version_ = version;
+ }
+
+ // Sets the trigger for metrics collection. The trigger is the reason why odrefresh considers
+ // compilation necessary. Only call this method if compilation is necessary as the presence
+ // of a trigger means we will try to record and upload metrics.
+ void SetTrigger(const Trigger trigger) {
+ trigger_ = trigger;
+ }
+
+ // Sets the execution status of the current odrefresh processing stage.
+ void SetStatus(const Status status) {
+ status_ = status;
+ }
+
+ // Sets the current odrefresh processing stage.
+ void SetStage(Stage stage);
+
+ // Record metrics into an OdrMetricsRecord.
+ // returns true on success, false if instance is not valid (because the trigger value is not set).
+ bool ToRecord(/*out*/OdrMetricsRecord* record) const;
+
+ private:
+ OdrMetrics(const OdrMetrics&) = delete;
+ OdrMetrics operator=(const OdrMetrics&) = delete;
+
+ static int32_t GetFreeSpaceMiB(const std::string& path);
+ static void WriteToFile(const std::string& path, const OdrMetrics* metrics);
+
+ void SetCompilationTime(int32_t seconds);
+
+ const std::string cache_directory_;
+ const std::string metrics_file_;
+
+ int64_t art_apex_version_ = 0;
+ std::optional<Trigger> trigger_ = {}; // metrics are only logged if compilation is triggered.
+ Stage stage_ = Stage::kUnknown;
+ Status status_ = Status::kUnknown;
+
+ int32_t primary_bcp_compilation_seconds_ = 0;
+ int32_t secondary_bcp_compilation_seconds_ = 0;
+ int32_t system_server_compilation_seconds_ = 0;
+ int32_t cache_space_free_start_mib_ = 0;
+ int32_t cache_space_free_end_mib_ = 0;
+
+ friend class ScopedOdrCompilationTimer;
+};
+
+// Timer used to measure compilation time (in seconds). Automatically associates the time recorded
+// with the current stage of the metrics used.
+class ScopedOdrCompilationTimer final {
+ public:
+ explicit ScopedOdrCompilationTimer(OdrMetrics& metrics) :
+ metrics_(metrics), start_(std::chrono::steady_clock::now()) {}
+
+ ~ScopedOdrCompilationTimer() {
+ auto elapsed_time = std::chrono::steady_clock::now() - start_;
+ auto elapsed_seconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed_time);
+ metrics_.SetCompilationTime(static_cast<int32_t>(elapsed_seconds.count()));
+ }
+
+ private:
+ OdrMetrics& metrics_;
+ std::chrono::time_point<std::chrono::steady_clock> start_;
+
+ DISALLOW_ALLOCATION();
+};
+
+// Generated ostream operators.
+std::ostream& operator<<(std::ostream& os, OdrMetrics::Status status);
+std::ostream& operator<<(std::ostream& os, OdrMetrics::Stage stage);
+std::ostream& operator<<(std::ostream& os, OdrMetrics::Trigger trigger);
+
+} // namespace odrefresh
+} // namespace art
+
+#endif // ART_ODREFRESH_ODR_METRICS_H_
diff --git a/odrefresh/odr_metrics_record.cc b/odrefresh/odr_metrics_record.cc
new file mode 100644
index 0000000..fc135d3
--- /dev/null
+++ b/odrefresh/odr_metrics_record.cc
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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 "odr_metrics_record.h"
+
+#include <iosfwd>
+#include <istream>
+#include <ostream>
+#include <streambuf>
+#include <string>
+
+namespace art {
+namespace odrefresh {
+
+std::istream& operator>>(std::istream& is, OdrMetricsRecord& record) {
+ // Block I/O related exceptions
+ auto saved_exceptions = is.exceptions();
+ is.exceptions(std::ios_base::iostate {});
+
+ // The order here matches the field order of MetricsRecord.
+ is >> record.art_apex_version >> std::ws;
+ is >> record.trigger >> std::ws;
+ is >> record.stage_reached >> std::ws;
+ is >> record.status >> std::ws;
+ is >> record.primary_bcp_compilation_seconds >> std::ws;
+ is >> record.secondary_bcp_compilation_seconds >> std::ws;
+ is >> record.system_server_compilation_seconds >> std::ws;
+ is >> record.cache_space_free_start_mib >> std::ws;
+ is >> record.cache_space_free_end_mib >> std::ws;
+
+ // Restore I/O related exceptions
+ is.exceptions(saved_exceptions);
+ return is;
+}
+
+std::ostream& operator<<(std::ostream& os, const OdrMetricsRecord& record) {
+ static const char kSpace = ' ';
+
+ // Block I/O related exceptions
+ auto saved_exceptions = os.exceptions();
+ os.exceptions(std::ios_base::iostate {});
+
+ // The order here matches the field order of MetricsRecord.
+ os << record.art_apex_version << kSpace;
+ os << record.trigger << kSpace;
+ os << record.stage_reached << kSpace;
+ os << record.status << kSpace;
+ os << record.primary_bcp_compilation_seconds << kSpace;
+ os << record.secondary_bcp_compilation_seconds << kSpace;
+ os << record.system_server_compilation_seconds << kSpace;
+ os << record.cache_space_free_start_mib << kSpace;
+ os << record.cache_space_free_end_mib << std::endl;
+
+ // Restore I/O related exceptions
+ os.exceptions(saved_exceptions);
+ return os;
+}
+
+} // namespace odrefresh
+} // namespace art
diff --git a/odrefresh/odr_metrics_record.h b/odrefresh/odr_metrics_record.h
new file mode 100644
index 0000000..9dd51a6
--- /dev/null
+++ b/odrefresh/odr_metrics_record.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+#ifndef ART_ODREFRESH_ODR_METRICS_RECORD_H_
+#define ART_ODREFRESH_ODR_METRICS_RECORD_H_
+
+#include <cstdint>
+#include <iosfwd> // For forward-declaration of std::string.
+
+namespace art {
+namespace odrefresh {
+
+// Default location for storing metrics from odrefresh.
+constexpr const char* kOdrefreshMetricsFile = "/data/misc/odrefresh/odrefresh-metrics.txt";
+
+// MetricsRecord is a simpler container for Odrefresh metric values reported to statsd. The order
+// and types of fields here mirror definition of `OdrefreshReported` in
+// frameworks/proto_logging/stats/atoms.proto.
+struct OdrMetricsRecord {
+ int64_t art_apex_version;
+ int32_t trigger;
+ int32_t stage_reached;
+ int32_t status;
+ int32_t primary_bcp_compilation_seconds;
+ int32_t secondary_bcp_compilation_seconds;
+ int32_t system_server_compilation_seconds;
+ int32_t cache_space_free_start_mib;
+ int32_t cache_space_free_end_mib;
+};
+
+// Read a `MetricsRecord` from an `istream`.
+//
+// This method blocks istream related exceptions, the caller should check `is.fail()` is false after
+// calling.
+//
+// Returns `is`.
+std::istream& operator>>(std::istream& is, OdrMetricsRecord& record);
+
+// Write a `MetricsRecord` to an `ostream`.
+//
+// This method blocks ostream related exceptions, the caller should check `os.fail()` is false after
+// calling.
+//
+// Returns `os`
+std::ostream& operator<<(std::ostream& os, const OdrMetricsRecord& record);
+
+} // namespace odrefresh
+} // namespace art
+
+#endif // ART_ODREFRESH_ODR_METRICS_RECORD_H_
diff --git a/odrefresh/odr_metrics_record_test.cc b/odrefresh/odr_metrics_record_test.cc
new file mode 100644
index 0000000..dd739d6
--- /dev/null
+++ b/odrefresh/odr_metrics_record_test.cc
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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 "odr_metrics_record.h"
+
+#include <string.h>
+
+#include <fstream>
+
+#include "base/common_art_test.h"
+
+namespace art {
+namespace odrefresh {
+
+class OdrMetricsRecordTest : public CommonArtTest {};
+
+TEST_F(OdrMetricsRecordTest, HappyPath) {
+ const OdrMetricsRecord expected {
+ .art_apex_version = 0x01233456'789abcde,
+ .trigger = 0x01020304,
+ .stage_reached = 0x11121314,
+ .status = 0x21222324,
+ .primary_bcp_compilation_seconds = 0x31323334,
+ .secondary_bcp_compilation_seconds = 0x41424344,
+ .system_server_compilation_seconds = 0x51525354,
+ .cache_space_free_start_mib = 0x61626364,
+ .cache_space_free_end_mib = 0x71727374
+ };
+
+ ScratchDir dir(/*keep_files=*/false);
+ std::string file_path = dir.GetPath() + "/metrics-record.txt";
+
+ {
+ std::ofstream ofs(file_path);
+ ofs << expected;
+ ASSERT_FALSE(ofs.fail());
+ ofs.close();
+ }
+
+ OdrMetricsRecord actual {};
+ {
+ std::ifstream ifs(file_path);
+ ifs >> actual;
+ ASSERT_TRUE(ifs.eof());
+ }
+
+ ASSERT_EQ(expected.art_apex_version, actual.art_apex_version);
+ ASSERT_EQ(expected.trigger, actual.trigger);
+ ASSERT_EQ(expected.stage_reached, actual.stage_reached);
+ ASSERT_EQ(expected.status, actual.status);
+ ASSERT_EQ(expected.primary_bcp_compilation_seconds, actual.primary_bcp_compilation_seconds);
+ ASSERT_EQ(expected.secondary_bcp_compilation_seconds, actual.secondary_bcp_compilation_seconds);
+ ASSERT_EQ(expected.system_server_compilation_seconds, actual.system_server_compilation_seconds);
+ ASSERT_EQ(expected.cache_space_free_start_mib, actual.cache_space_free_start_mib);
+ ASSERT_EQ(expected.cache_space_free_end_mib, actual.cache_space_free_end_mib);
+ ASSERT_EQ(0, memcmp(&expected, &actual, sizeof(expected)));
+}
+
+TEST_F(OdrMetricsRecordTest, EmptyInput) {
+ ScratchDir dir(/*keep_files=*/false);
+ std::string file_path = dir.GetPath() + "/metrics-record.txt";
+
+ std::ifstream ifs(file_path);
+ OdrMetricsRecord record;
+ ifs >> record;
+
+ ASSERT_TRUE(ifs.fail());
+ ASSERT_TRUE(!ifs);
+}
+
+TEST_F(OdrMetricsRecordTest, ClosedInput) {
+ ScratchDir dir(/*keep_files=*/false);
+ std::string file_path = dir.GetPath() + "/metrics-record.txt";
+
+ std::ifstream ifs(file_path);
+ ifs.close();
+
+ OdrMetricsRecord record;
+ ifs >> record;
+
+ ASSERT_TRUE(ifs.fail());
+ ASSERT_TRUE(!ifs);
+}
+
+TEST_F(OdrMetricsRecordTest, ClosedOutput) {
+ ScratchDir dir(/*keep_files=*/false);
+ std::string file_path = dir.GetPath() + "/metrics-record.txt";
+
+ std::ofstream ofs(file_path);
+ ofs.close();
+
+ OdrMetricsRecord record {};
+ ofs << record;
+
+ ASSERT_TRUE(ofs.fail());
+ ASSERT_TRUE(!ofs.good());
+}
+
+} // namespace odrefresh
+} // namespace art
diff --git a/odrefresh/odr_metrics_test.cc b/odrefresh/odr_metrics_test.cc
new file mode 100644
index 0000000..4519f00
--- /dev/null
+++ b/odrefresh/odr_metrics_test.cc
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2021 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 "odr_metrics.h"
+#include "base/casts.h"
+#include "odr_metrics_record.h"
+
+#include <unistd.h>
+
+#include <fstream>
+#include <memory>
+#include <string>
+
+#include "base/common_art_test.h"
+
+namespace art {
+namespace odrefresh {
+
+class OdrMetricsTest : public CommonArtTest {
+ public:
+ void SetUp() override {
+ CommonArtTest::SetUp();
+
+ scratch_dir_ = std::make_unique<ScratchDir>();
+ metrics_file_path_ = scratch_dir_->GetPath() + "/metrics.txt";
+ cache_directory_ = scratch_dir_->GetPath() + "/dir";
+ mkdir(cache_directory_.c_str(), S_IRWXU);
+ }
+
+ void TearDown() override {
+ scratch_dir_.reset();
+ }
+
+ bool MetricsFileExists() const {
+ const char* path = metrics_file_path_.c_str();
+ return OS::FileExists(path);
+ }
+
+ bool RemoveMetricsFile() const {
+ const char* path = metrics_file_path_.c_str();
+ if (OS::FileExists(path)) {
+ return unlink(path) == 0;
+ }
+ return true;
+ }
+
+ const std::string GetCacheDirectory() const { return cache_directory_; }
+ const std::string GetMetricsFilePath() const { return metrics_file_path_; }
+
+ protected:
+ std::unique_ptr<ScratchDir> scratch_dir_;
+ std::string metrics_file_path_;
+ std::string cache_directory_;
+};
+
+TEST_F(OdrMetricsTest, ToRecordFailsIfNotTriggered) {
+ {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ OdrMetricsRecord record {};
+ EXPECT_FALSE(metrics.ToRecord(&record));
+ }
+
+ {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(99);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+ OdrMetricsRecord record {};
+ EXPECT_FALSE(metrics.ToRecord(&record));
+ }
+}
+
+TEST_F(OdrMetricsTest, ToRecordSucceedsIfTriggered) {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(99);
+ metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+
+ OdrMetricsRecord record{};
+ EXPECT_TRUE(metrics.ToRecord(&record));
+
+ EXPECT_EQ(99, record.art_apex_version);
+ EXPECT_EQ(OdrMetrics::Trigger::kApexVersionMismatch,
+ enum_cast<OdrMetrics::Trigger>(record.trigger));
+ EXPECT_EQ(OdrMetrics::Stage::kCheck, enum_cast<OdrMetrics::Stage>(record.stage_reached));
+ EXPECT_EQ(OdrMetrics::Status::kNoSpace, enum_cast<OdrMetrics::Status>(record.status));
+}
+
+TEST_F(OdrMetricsTest, MetricsFileIsNotCreatedIfNotTriggered) {
+ EXPECT_TRUE(RemoveMetricsFile());
+
+ // Metrics file is (potentially) written in OdrMetrics destructor.
+ {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(99);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+ }
+ EXPECT_FALSE(MetricsFileExists());
+}
+
+TEST_F(OdrMetricsTest, NoMetricsFileIsCreatedIfTriggered) {
+ EXPECT_TRUE(RemoveMetricsFile());
+
+ // Metrics file is (potentially) written in OdrMetrics destructor.
+ {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(101);
+ metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+ }
+ EXPECT_TRUE(MetricsFileExists());
+}
+
+TEST_F(OdrMetricsTest, StageDoesNotAdvancedAfterFailure) {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(1999);
+ metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kNoSpace);
+ metrics.SetStage(OdrMetrics::Stage::kComplete);
+
+ OdrMetricsRecord record{};
+ EXPECT_TRUE(metrics.ToRecord(&record));
+
+ EXPECT_EQ(OdrMetrics::Stage::kCheck, enum_cast<OdrMetrics::Stage>(record.stage_reached));
+}
+
+TEST_F(OdrMetricsTest, TimeValuesAreRecorded) {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(1999);
+ metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kOK);
+
+ // Primary boot classpath compilation time.
+ OdrMetricsRecord record{};
+ {
+ metrics.SetStage(OdrMetrics::Stage::kPrimaryBootClasspath);
+ ScopedOdrCompilationTimer timer(metrics);
+ sleep(2u);
+ }
+ EXPECT_TRUE(metrics.ToRecord(&record));
+ EXPECT_EQ(OdrMetrics::Stage::kPrimaryBootClasspath,
+ enum_cast<OdrMetrics::Stage>(record.stage_reached));
+ EXPECT_NE(0, record.primary_bcp_compilation_seconds);
+ EXPECT_GT(10, record.primary_bcp_compilation_seconds);
+ EXPECT_EQ(0, record.secondary_bcp_compilation_seconds);
+ EXPECT_EQ(0, record.system_server_compilation_seconds);
+
+ // Secondary boot classpath compilation time.
+ {
+ metrics.SetStage(OdrMetrics::Stage::kSecondaryBootClasspath);
+ ScopedOdrCompilationTimer timer(metrics);
+ sleep(2u);
+ }
+ EXPECT_TRUE(metrics.ToRecord(&record));
+ EXPECT_EQ(OdrMetrics::Stage::kSecondaryBootClasspath,
+ enum_cast<OdrMetrics::Stage>(record.stage_reached));
+ EXPECT_NE(0, record.primary_bcp_compilation_seconds);
+ EXPECT_NE(0, record.secondary_bcp_compilation_seconds);
+ EXPECT_GT(10, record.secondary_bcp_compilation_seconds);
+ EXPECT_EQ(0, record.system_server_compilation_seconds);
+
+ // system_server classpath compilation time.
+ {
+ metrics.SetStage(OdrMetrics::Stage::kSystemServerClasspath);
+ ScopedOdrCompilationTimer timer(metrics);
+ sleep(2u);
+ }
+ EXPECT_TRUE(metrics.ToRecord(&record));
+ EXPECT_EQ(OdrMetrics::Stage::kSystemServerClasspath,
+ enum_cast<OdrMetrics::Stage>(record.stage_reached));
+ EXPECT_NE(0, record.primary_bcp_compilation_seconds);
+ EXPECT_NE(0, record.secondary_bcp_compilation_seconds);
+ EXPECT_NE(0, record.system_server_compilation_seconds);
+ EXPECT_GT(10, record.system_server_compilation_seconds);
+}
+
+TEST_F(OdrMetricsTest, CacheSpaceValuesAreUpdated) {
+ OdrMetricsRecord snap {};
+ snap.cache_space_free_start_mib = -1;
+ snap.cache_space_free_end_mib = -1;
+ {
+ OdrMetrics metrics(GetCacheDirectory(), GetMetricsFilePath());
+ metrics.SetArtApexVersion(1999);
+ metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+ metrics.SetStatus(OdrMetrics::Status::kOK);
+ EXPECT_TRUE(metrics.ToRecord(&snap));
+ EXPECT_NE(0, snap.cache_space_free_start_mib);
+ EXPECT_EQ(0, snap.cache_space_free_end_mib);
+ }
+
+ OdrMetricsRecord on_disk;
+ std::ifstream ifs(GetMetricsFilePath());
+ EXPECT_TRUE(ifs);
+ ifs >> on_disk;
+ EXPECT_TRUE(ifs);
+ EXPECT_EQ(snap.cache_space_free_start_mib, on_disk.cache_space_free_start_mib);
+ EXPECT_NE(0, on_disk.cache_space_free_end_mib);
+}
+
+} // namespace odrefresh
+} // namespace art
diff --git a/odrefresh/odrefresh.cc b/odrefresh/odrefresh.cc
index 24ef7d1..5f8b072 100644
--- a/odrefresh/odrefresh.cc
+++ b/odrefresh/odrefresh.cc
@@ -64,12 +64,14 @@
#include "dex/art_dex_file_loader.h"
#include "dexoptanalyzer.h"
#include "exec_utils.h"
+#include "log/log.h"
#include "palette/palette.h"
#include "palette/palette_types.h"
#include "odr_artifacts.h"
#include "odr_config.h"
#include "odr_fs_utils.h"
+#include "odr_metrics.h"
namespace art {
namespace odrefresh {
@@ -479,7 +481,9 @@
return true;
}
- WARN_UNUSED ExitCode CheckArtifactsAreUpToDate() {
+ WARN_UNUSED ExitCode CheckArtifactsAreUpToDate(OdrMetrics& metrics) {
+ metrics.SetStage(OdrMetrics::Stage::kCheck);
+
// Clean-up helper used to simplify clean-ups and handling failures there.
auto cleanup_return = [this](ExitCode exit_code) {
return CleanApexdataDirectory() ? exit_code : ExitCode::kCleanupFailed;
@@ -487,8 +491,9 @@
const auto apex_info = GetArtApexInfo();
if (!apex_info.has_value()) {
- // This should never happen, but do not proceed if it does.
+ // 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::kApexVersionMismatch);
return cleanup_return(ExitCode::kCompilationRequired);
}
@@ -502,30 +507,40 @@
// If the cache info file does not exist, assume compilation is required because the
// file is missing and because the current ART APEX is not factory installed.
PLOG(ERROR) << "No prior cache-info file: " << QuotePath(cache_info_filename_);
+ metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
return cleanup_return(ExitCode::kCompilationRequired);
}
// Get and parse the ART APEX cache info file.
std::optional<art_apex::CacheInfo> cache_info = ReadCacheInfo();
if (!cache_info.has_value()) {
+ // This should never happen, further up-to-date checks are not possible if it does.
PLOG(ERROR) << "Failed to read cache-info file: " << QuotePath(cache_info_filename_);
+ metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
return cleanup_return(ExitCode::kCompilationRequired);
}
// Generate current module info for the current ART APEX.
const auto current_info = GenerateArtModuleInfo();
if (!current_info.has_value()) {
+ // This should never happen, further up-to-date checks are not possible if it does.
LOG(ERROR) << "Failed to generate cache provenance.";
+ metrics.SetTrigger(OdrMetrics::Trigger::kUnknown);
return cleanup_return(ExitCode::kCompilationRequired);
}
+ // Record ART Apex version for metrics reporting.
+ metrics.SetArtApexVersion(current_info->getVersionCode());
+
// Check whether the current cache ART module info differs from the current ART module info.
// Always check APEX version.
const auto cached_info = cache_info->getFirstArtModuleInfo();
+
if (cached_info->getVersionCode() != current_info->getVersionCode()) {
LOG(INFO) << "ART APEX version code mismatch ("
<< cached_info->getVersionCode()
<< " != " << current_info->getVersionCode() << ").";
+ metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
return cleanup_return(ExitCode::kCompilationRequired);
}
@@ -533,6 +548,7 @@
LOG(INFO) << "ART APEX version code mismatch ("
<< cached_info->getVersionName()
<< " != " << current_info->getVersionName() << ").";
+ metrics.SetTrigger(OdrMetrics::Trigger::kApexVersionMismatch);
return cleanup_return(ExitCode::kCompilationRequired);
}
@@ -550,6 +566,7 @@
(!cache_info->hasDex2oatBootClasspath() ||
!cache_info->getFirstDex2oatBootClasspath()->hasComponent())) {
LOG(INFO) << "Missing Dex2oatBootClasspath components.";
+ metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
return cleanup_return(ExitCode::kCompilationRequired);
}
@@ -558,6 +575,7 @@
cache_info->getFirstDex2oatBootClasspath()->getComponent();
if (!CheckComponents(expected_bcp_components, bcp_components, &error_msg)) {
LOG(INFO) << "Dex2OatClasspath components mismatch: " << error_msg;
+ metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
return cleanup_return(ExitCode::kCompilationRequired);
}
@@ -580,6 +598,7 @@
(!cache_info->hasSystemServerClasspath() ||
!cache_info->getFirstSystemServerClasspath()->hasComponent())) {
LOG(INFO) << "Missing SystemServerClasspath components.";
+ metrics.SetTrigger(OdrMetrics::Trigger::kDexFilesChanged);
return cleanup_system_server_return(ExitCode::kCompilationRequired);
}
@@ -587,6 +606,7 @@
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);
return cleanup_system_server_return(ExitCode::kCompilationRequired);
}
@@ -598,6 +618,7 @@
for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
if (!BootExtensionArtifactsExistOnData(isa, &error_msg)) {
LOG(INFO) << "Incomplete boot extension artifacts. " << error_msg;
+ metrics.SetTrigger(OdrMetrics::Trigger::kMissingArtifacts);
return cleanup_boot_extensions_return(ExitCode::kCompilationRequired, isa);
}
}
@@ -608,6 +629,7 @@
// `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);
return ExitCode::kCompilationRequired;
}
@@ -980,8 +1002,10 @@
WARN_UNUSED bool 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());
@@ -1032,12 +1056,14 @@
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;
}
@@ -1048,6 +1074,7 @@
const std::string install_location = android::base::Dirname(image_location);
if (!EnsureDirectoryExists(install_location)) {
+ metrics.SetStatus(OdrMetrics::Status::kIoError);
return false;
}
@@ -1061,15 +1088,19 @@
}
bool timed_out = false;
- if (ExecAndReturnCode(args, timeout, &timed_out, error_msg) != 0) {
+ int dex2oat_exit_code = ExecAndReturnCode(args, timeout, &timed_out, error_msg);
+ if (dex2oat_exit_code != 0) {
if (timed_out) {
- // TODO(oth): record timeout event for compiling boot extension
+ 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;
}
@@ -1080,8 +1111,10 @@
}
WARN_UNUSED bool 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();
@@ -1104,6 +1137,7 @@
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;
}
}
@@ -1124,6 +1158,7 @@
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;
}
@@ -1152,15 +1187,19 @@
}
bool timed_out = false;
- if (!Exec(args, error_msg)) {
+ int dex2oat_exit_code = ExecAndReturnCode(args, timeout, &timed_out, error_msg);
+ if (dex2oat_exit_code != 0) {
if (timed_out) {
- // TODO(oth): record timeout event for compiling boot extension
+ 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;
}
@@ -1181,28 +1220,39 @@
android::base::SetProperty("service.bootanim.progress", std::to_string(value));
}
- WARN_UNUSED ExitCode Compile(bool force_compile) const {
+
+ WARN_UNUSED ExitCode Compile(OdrMetrics& metrics, bool force_compile) const {
ReportSpace(); // TODO(oth): Factor available space into compilation logic.
+ const char* staging_dir = nullptr;
+ metrics.SetStage(OdrMetrics::Stage::kPreparation);
// Clean-up existing files.
if (force_compile && !CleanApexdataDirectory()) {
+ metrics.SetStatus(OdrMetrics::Status::kIoError);
+ return ExitCode::kCleanupFailed;
+ }
+
+ // 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();
- // Create staging area and assign label for generating compilation artifacts.
- const char* staging_dir;
- if (PaletteCreateOdrefreshStagingDirectory(&staging_dir) != PALETTE_STATUS_OK) {
- return ExitCode::kCompilationFailed;
- }
-
std::string error_msg;
uint32_t dex2oat_invocation_count = 0;
ReportNextBootAnimationProgress(dex2oat_invocation_count);
- for (const InstructionSet isa : config_.GetBootExtensionIsas()) {
+
+ const auto& bcp_instruction_sets = config_.GetBootExtensionIsas();
+ DCHECK(!bcp_instruction_sets.empty() && bcp_instruction_sets.size() <= 2);
+ for (const InstructionSet isa : bcp_instruction_sets) {
+ auto stage = (isa == bcp_instruction_sets.front()) ?
+ OdrMetrics::Stage::kPrimaryBootClasspath :
+ OdrMetrics::Stage::kSecondaryBootClasspath;
+ metrics.SetStage(stage);
if (force_compile || !BootExtensionArtifactsExistOnData(isa, &error_msg)) {
// Remove artifacts we are about to generate. Ordinarily these are removed in the checking
// step, but this is not always run (e.g. during manual testing).
@@ -1210,7 +1260,7 @@
return ExitCode::kCleanupFailed;
}
if (!CompileBootExtensionArtifacts(
- isa, staging_dir, &dex2oat_invocation_count, &error_msg)) {
+ isa, staging_dir, metrics, &dex2oat_invocation_count, &error_msg)) {
LOG(ERROR) << "Compilation of BCP failed: " << error_msg;
if (!config_.GetDryRun() && !CleanDirectory(staging_dir)) {
return ExitCode::kCleanupFailed;
@@ -1221,7 +1271,9 @@
}
if (force_compile || !SystemServerArtifactsExistOnData(&error_msg)) {
- if (!CompileSystemServerArtifacts(staging_dir, &dex2oat_invocation_count, &error_msg)) {
+ metrics.SetStage(OdrMetrics::Stage::kSystemServerClasspath);
+ if (!CompileSystemServerArtifacts(
+ staging_dir, metrics, &dex2oat_invocation_count, &error_msg)) {
LOG(ERROR) << "Compilation of system_server failed: " << error_msg;
if (!config_.GetDryRun() && !CleanDirectory(staging_dir)) {
return ExitCode::kCleanupFailed;
@@ -1230,6 +1282,7 @@
}
}
+ metrics.SetStage(OdrMetrics::Stage::kComplete);
return ExitCode::kCompilationSuccess;
}
@@ -1341,26 +1394,28 @@
static int main(int argc, const char** argv) {
OdrConfig config(argv[0]);
-
int n = InitializeConfig(argc, argv, &config);
argv += n;
argc -= n;
-
if (argc != 1) {
UsageError("Expected 1 argument, but have %d.", argc);
}
+ OdrMetrics metrics(kOdrefreshArtifactDirectory);
OnDeviceRefresh odr(config);
for (int i = 0; i < argc; ++i) {
std::string_view action(argv[i]);
if (action == "--check") {
// Fast determination of whether artifacts are up to date.
- return odr.CheckArtifactsAreUpToDate();
+ return odr.CheckArtifactsAreUpToDate(metrics);
} else if (action == "--compile") {
- const ExitCode e = odr.CheckArtifactsAreUpToDate();
- return (e == ExitCode::kCompilationRequired) ? odr.Compile(/*force_compile=*/false) : e;
+ const ExitCode exit_code = odr.CheckArtifactsAreUpToDate(metrics);
+ if (exit_code == ExitCode::kCompilationRequired) {
+ return odr.Compile(metrics, /*force_compile=*/false);
+ }
+ return exit_code;
} else if (action == "--force-compile") {
- return odr.Compile(/*force_compile=*/true);
+ return odr.Compile(metrics, /*force_compile=*/true);
} else if (action == "--verify") {
// Slow determination of whether artifacts are up to date. These are too slow for checking
// during boot (b/181689036).