diff --git a/Android.bp b/Android.bp index afab76c2..76e69859 100644 --- a/Android.bp +++ b/Android.bp @@ -256,6 +256,7 @@ cc_binary { shared_libs: [ "libbase", "liblog", + "libmetricslogger", ], static_libs: [ diff --git a/Android.mk b/Android.mk index 80d107dc..7be12306 100644 --- a/Android.mk +++ b/Android.mk @@ -71,10 +71,13 @@ LOCAL_REQUIRED_MODULES += \ endif endif +# On A/B devices recovery-persist reads the recovery related file from the persist storage and +# copies them into /data/misc/recovery. Then, for both A/B and non-A/B devices, recovery-persist +# parses the last_install file and reports the embedded update metrics. Also, the last_install file +# will be deteleted after the report. +LOCAL_REQUIRED_MODULES += recovery-persist ifeq ($(BOARD_CACHEIMAGE_PARTITION_SIZE),) -LOCAL_REQUIRED_MODULES += \ - recovery-persist \ - recovery-refresh +LOCAL_REQUIRED_MODULES += recovery-refresh endif include $(BUILD_PHONY_PACKAGE) diff --git a/logging.cpp b/logging.cpp index d5af72aa..50642a26 100644 --- a/logging.cpp +++ b/logging.cpp @@ -221,6 +221,7 @@ void copy_logs(bool modified_flash, bool has_cache) { chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM); chmod(LAST_LOG_FILE, 0640); chmod(LAST_INSTALL_FILE, 0644); + chown(LAST_INSTALL_FILE, AID_SYSTEM, AID_SYSTEM); sync(); } diff --git a/otautil/Android.bp b/otautil/Android.bp index 56c7c9e8..41018dd2 100644 --- a/otautil/Android.bp +++ b/otautil/Android.bp @@ -41,6 +41,7 @@ cc_library_static { srcs: [ "dirutil.cpp", "mounts.cpp", + "parse_install_logs.cpp", "sysutil.cpp", "thermalutil.cpp", ], diff --git a/otautil/include/otautil/parse_install_logs.h b/otautil/include/otautil/parse_install_logs.h new file mode 100644 index 00000000..135d29cc --- /dev/null +++ b/otautil/include/otautil/parse_install_logs.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 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. + */ + +#pragma once + +#include + +#include +#include +#include + +constexpr const char* LAST_INSTALL_FILE = "/data/misc/recovery/last_install"; +constexpr const char* LAST_INSTALL_FILE_IN_CACHE = "/cache/recovery/last_install"; + +// Parses the metrics of update applied under recovery mode in |lines|, and returns a map with +// "name: value". +std::map ParseRecoveryUpdateMetrics(const std::vector& lines); +// Parses the sideload history and update metrics in the last_install file. Returns a map with +// entries as "metrics_name: value". If no such file exists, returns an empty map. +std::map ParseLastInstall(const std::string& file_name); diff --git a/otautil/parse_install_logs.cpp b/otautil/parse_install_logs.cpp new file mode 100644 index 00000000..13a72992 --- /dev/null +++ b/otautil/parse_install_logs.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2018 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 "otautil/parse_install_logs.h" + +#include + +#include + +#include +#include +#include +#include +#include + +constexpr const char* OTA_SIDELOAD_METRICS = "ota_sideload"; + +// Here is an example of lines in last_install: +// ... +// time_total: 101 +// bytes_written_vendor: 51074 +// bytes_stashed_vendor: 200 +std::map ParseRecoveryUpdateMetrics(const std::vector& lines) { + constexpr unsigned int kMiB = 1024 * 1024; + std::optional bytes_written_in_mib; + std::optional bytes_stashed_in_mib; + std::map metrics; + for (const auto& line : lines) { + size_t num_index = line.find(':'); + if (num_index == std::string::npos) { + LOG(WARNING) << "Skip parsing " << line; + continue; + } + + std::string num_string = android::base::Trim(line.substr(num_index + 1)); + int64_t parsed_num; + if (!android::base::ParseInt(num_string, &parsed_num)) { + LOG(ERROR) << "Failed to parse numbers in " << line; + continue; + } + + if (android::base::StartsWith(line, "bytes_written")) { + bytes_written_in_mib = bytes_written_in_mib.value_or(0) + parsed_num / kMiB; + } else if (android::base::StartsWith(line, "bytes_stashed")) { + bytes_stashed_in_mib = bytes_stashed_in_mib.value_or(0) + parsed_num / kMiB; + } else if (android::base::StartsWith(line, "time")) { + metrics.emplace("ota_time_total", parsed_num); + } else if (android::base::StartsWith(line, "uncrypt_time")) { + metrics.emplace("ota_uncrypt_time", parsed_num); + } else if (android::base::StartsWith(line, "source_build")) { + metrics.emplace("ota_source_version", parsed_num); + } else if (android::base::StartsWith(line, "temperature_start")) { + metrics.emplace("ota_temperature_start", parsed_num); + } else if (android::base::StartsWith(line, "temperature_end")) { + metrics.emplace("ota_temperature_end", parsed_num); + } else if (android::base::StartsWith(line, "temperature_max")) { + metrics.emplace("ota_temperature_max", parsed_num); + } else if (android::base::StartsWith(line, "error")) { + metrics.emplace("ota_non_ab_error_code", parsed_num); + } else if (android::base::StartsWith(line, "cause")) { + metrics.emplace("ota_non_ab_cause_code", parsed_num); + } + } + + if (bytes_written_in_mib) { + metrics.emplace("ota_written_in_MiBs", bytes_written_in_mib.value()); + } + if (bytes_stashed_in_mib) { + metrics.emplace("ota_stashed_in_MiBs", bytes_stashed_in_mib.value()); + } + + return metrics; +} + +std::map ParseLastInstall(const std::string& file_name) { + if (access(file_name.c_str(), F_OK) != 0) { + return {}; + } + + std::string content; + if (!android::base::ReadFileToString(file_name, &content)) { + PLOG(ERROR) << "Failed to read " << file_name; + return {}; + } + + if (content.empty()) { + LOG(INFO) << "Empty last_install file"; + return {}; + } + + std::vector lines = android::base::Split(content, "\n"); + auto metrics = ParseRecoveryUpdateMetrics(lines); + + // LAST_INSTALL starts with "/sideload/package.zip" after a sideload. + if (android::base::Trim(lines[0]) == "/sideload/package.zip") { + int type = (android::base::GetProperty("ro.build.type", "") == "user") ? 1 : 0; + metrics.emplace(OTA_SIDELOAD_METRICS, type); + } + + return metrics; +} diff --git a/recovery-persist.cpp b/recovery-persist.cpp index d3ade626..ebb42d22 100644 --- a/recovery-persist.cpp +++ b/recovery-persist.cpp @@ -35,19 +35,22 @@ #include #include +#include #include #include #include +#include #include /* private pmsg functions */ #include "logging.h" +#include "otautil/parse_install_logs.h" -static const char *LAST_LOG_FILE = "/data/misc/recovery/last_log"; -static const char *LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0"; -static const char *LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg"; -static const char *LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0"; -static const char *ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops"; +constexpr const char* LAST_LOG_FILE = "/data/misc/recovery/last_log"; +constexpr const char* LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0"; +constexpr const char* LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg"; +constexpr const char* LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0"; +constexpr const char* ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops"; // close a file, log an error if the error indicator is set static void check_and_fclose(FILE *fp, const char *name) { @@ -109,6 +112,20 @@ ssize_t logsave( return android::base::WriteStringToFile(buffer, destination.c_str()); } +// Parses the LAST_INSTALL file and reports the update metrics saved under recovery mode. +static void report_metrics_from_last_install(const std::string& file_name) { + auto metrics = ParseLastInstall(file_name); + // TODO(xunchang) report the installation result. + for (const auto& [event, value] : metrics) { + if (value > std::numeric_limits::max()) { + LOG(WARNING) << event << " (" << value << ") exceeds integer max."; + } else { + LOG(INFO) << "Uploading " << value << " to " << event; + android::metricslogger::LogHistogram(event, value); + } + } +} + int main(int argc, char **argv) { /* Is /cache a mount?, we have been delivered where we are not wanted */ @@ -138,14 +155,18 @@ int main(int argc, char **argv) { } if (has_cache) { - /* - * TBD: Future location to move content from - * /cache/recovery to /data/misc/recovery/ - */ - /* if --force-persist flag, then transfer pmsg data anyways */ - if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) { - return 0; - } + // Collects and reports the non-a/b update metrics from last_install; and removes the file + // to avoid duplicate report. + report_metrics_from_last_install(LAST_INSTALL_FILE_IN_CACHE); + if (unlink(LAST_INSTALL_FILE_IN_CACHE) == -1) { + PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE_IN_CACHE; + } + + // TBD: Future location to move content from /cache/recovery to /data/misc/recovery/ + // if --force-persist flag, then transfer pmsg data anyways + if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) { + return 0; + } } /* Is there something in pmsg? */ @@ -157,6 +178,15 @@ int main(int argc, char **argv) { __android_log_pmsg_file_read( LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", logsave, NULL); + // For those device without /cache, the last_install file has been copied to + // /data/misc/recovery from pmsg. Looks for the sideload history only. + if (!has_cache) { + report_metrics_from_last_install(LAST_INSTALL_FILE); + if (unlink(LAST_INSTALL_FILE) == -1) { + PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE; + } + } + /* Is there a last console log too? */ if (rotated) { if (!access(LAST_CONSOLE_FILE, R_OK)) { diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp new file mode 100644 index 00000000..8061f3be --- /dev/null +++ b/tests/unit/parse_install_logs_test.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 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 +#include +#include + +#include +#include +#include +#include + +#include "otautil/parse_install_logs.h" + +TEST(ParseInstallLogsTest, EmptyFile) { + TemporaryFile last_install; + + auto metrics = ParseLastInstall(last_install.path); + ASSERT_TRUE(metrics.empty()); +} + +TEST(ParseInstallLogsTest, SideloadSmoke) { + TemporaryFile last_install; + ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path)); + auto metrics = ParseLastInstall(last_install.path); + ASSERT_EQ(metrics.end(), metrics.find("ota_sideload")); + + ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path)); + metrics = ParseLastInstall(last_install.path); + ASSERT_NE(metrics.end(), metrics.find("ota_sideload")); +} + +TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) { + std::vector lines = { + "/sideload/package.zip", + "0", + "time_total: 300", + "uncrypt_time: 40", + "source_build: 4973410", + "bytes_written_system: " + std::to_string(1200 * 1024 * 1024), + "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024), + "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024), + "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024), + "temperature_start: 37000", + "temperature_end: 38000", + "temperature_max: 39000", + "error: 22", + "cause: 55", + }; + + auto metrics = ParseRecoveryUpdateMetrics(lines); + + std::map expected_result = { + { "ota_time_total", 300 }, { "ota_uncrypt_time", 40 }, + { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 }, + { "ota_stashed_in_MiBs", 350 }, { "ota_temperature_start", 37000 }, + { "ota_temperature_end", 38000 }, { "ota_temperature_max", 39000 }, + { "ota_non_ab_error_code", 22 }, { "ota_non_ab_cause_code", 55 }, + }; + + ASSERT_EQ(expected_result, metrics); +}