blob: cd6f0874c0de6f9b8d902c874cf3eb46b795f21c [file] [log] [blame]
// Copyright 2011 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Implementation of bootstat_log(), part of the Chromium OS 'bootstat'
// facility.
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <linux/rtc.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <optional>
#include <string>
#include <vector>
#include <base/files/file_util.h>
#include <base/files/scoped_file.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <rootdev/rootdev.h>
#include "bootstat/bootstat.h"
namespace bootstat {
namespace {
// Default root-relative path to directory where output statistics will be
// stored.
static const char kDefaultOutputDirectoryName[] = "run/bootstat";
static constexpr char kProcUptime[] = "proc/uptime";
static constexpr int64_t kNsecsPerSec = 1e9;
// Parse a line of text containing one or more space-separated columns of
// decimal numbers. For example (without the quotes):
// "12.76543 0.89"
// "10.3333"
static std::optional<std::vector<base::TimeDelta>> ParseDecimalColumns(
const base::StringPiece& line) {
auto numbers = base::SplitStringPiece(line, " ", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
if (numbers.empty()) {
LOG(ERROR) << "Malformed line: " << line;
return std::nullopt;
}
std::vector<base::TimeDelta> results;
for (auto& number : numbers) {
auto pieces = base::SplitStringPiece(number, ".", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
if (pieces.size() != 2) {
LOG(ERROR) << "Malformed number: " << line;
return std::nullopt;
}
uint64_t secs;
if (!base::StringToUint64(pieces[0], &secs)) {
LOG(ERROR) << "Malformed seconds: " << line;
return std::nullopt;
}
// We're looking for nanoseconds.
if (pieces[1].size() > 9) {
LOG(ERROR) << "Malformed decimal places: " << line;
return std::nullopt;
}
// Pad to 9, with trailing zeroes.
unsigned int nsecs;
if (!base::StringToUint(pieces[1], &nsecs)) {
LOG(ERROR) << "Malformed decimal: " << line;
return std::nullopt;
}
for (int i = 0; i < 9 - pieces[1].size(); i++)
nsecs *= 10;
results.push_back(base::Seconds(secs) + base::Nanoseconds(nsecs));
}
return results;
}
} // namespace
BootStatSystem::BootStatSystem() : BootStatSystem(base::FilePath("/")) {}
BootStatSystem::BootStatSystem(const base::FilePath& root_path)
: root_path_(root_path) {}
// TODO(drinkcat): Cache function output (we only need to evaluate it once)
base::FilePath BootStatSystem::GetDiskStatisticsFilePath() const {
char boot_path[PATH_MAX];
int ret = rootdev(boot_path, sizeof(boot_path),
true, // Do full resolution.
false); // Do not remove partition number.
if (ret != 0) {
LOG(ERROR) << "Cannot get rootdev.";
return base::FilePath();
}
// The general idea is to use the the root device's sysfs entry to
// get the path to the root disk's sysfs entry.
// Example:
// - rootdev() returns "/dev/sda3"
// - Use /sys/class/block/sda3/../ to get to root disk (sda) sysfs entry.
// This is because /sys/class/block/sda3 is a symlink that maps to:
// /sys/devices/pci.../.../ata./host./target.../.../block/sda/sda3
base::FilePath root_device_name = base::FilePath(boot_path).BaseName();
base::FilePath stat_path = base::FilePath("/sys/class/block")
.Append(root_device_name)
.Append("../stat");
// Normalize the path as some functions refuse to follow symlink/`..`.
base::FilePath norm;
if (!base::NormalizeFilePath(stat_path, &norm)) {
LOG(ERROR) << "Cannot normalize disk statistics file path.";
return base::FilePath();
}
return norm;
}
std::optional<struct timespec> BootStatSystem::GetUpTime() const {
struct timespec uptime;
int ret = clock_gettime(CLOCK_BOOTTIME, &uptime);
if (ret != 0) {
PLOG(ERROR) << "Cannot get uptime (CLOCK_BOOTTIME).";
return std::nullopt;
}
return uptime;
}
std::optional<base::TimeDelta> BootStatSystem::GetIdleTime() const {
base::FilePath path = root_path_.Append(kProcUptime);
std::string data;
if (!base::ReadFileToString(path, &data)) {
PLOG(ERROR) << "Cannot read uptime from: " << path;
return std::nullopt;
}
auto numbers = ParseDecimalColumns(data);
if (!numbers) {
LOG(ERROR) << "Couldn't parse uptime: " << data;
return std::nullopt;
}
if (numbers->size() != 2) {
LOG(ERROR) << "Unexpected uptime contents: " << data;
return std::nullopt;
}
// Second column is idle time.
return (*numbers)[1];
}
base::ScopedFD BootStatSystem::OpenRtc() const {
int rtc_fd = HANDLE_EINTR(open("/dev/rtc", O_RDONLY | O_CLOEXEC));
if (rtc_fd < 0)
PLOG(ERROR) << "Cannot open RTC";
return base::ScopedFD(rtc_fd);
}
std::optional<struct rtc_time> BootStatSystem::GetRtcTime(
base::ScopedFD* rtc_fd) const {
struct rtc_time rtc_time;
if (ioctl(rtc_fd->get(), RTC_RD_TIME, &rtc_time) < 0) {
PLOG(ERROR) << "RTC ioctl error";
return std::nullopt;
}
return rtc_time;
}
BootStat::BootStat() : BootStat(base::FilePath("/")) {}
BootStat::BootStat(const base::FilePath& root_path)
: BootStat(root_path.Append(base::FilePath(kDefaultOutputDirectoryName)),
std::make_unique<BootStatSystem>()) {}
BootStat::BootStat(const base::FilePath& output_directory_path,
std::unique_ptr<BootStatSystem> boot_stat_system)
: output_directory_path_(output_directory_path),
boot_stat_system_(std::move(boot_stat_system)) {}
BootStat::~BootStat() = default;
base::FilePath BootStat::GetEventPath(const std::string& prefix,
const std::string& event_name) const {
//
// For those not up on the more esoteric features of printf
// formats: the "%.*s" format is used to truncate the event name
// to the proper number of characters..
//
std::string output_file =
base::StringPrintf("%s-%.*s", prefix.c_str(), BOOTSTAT_MAX_EVENT_LEN - 1,
event_name.c_str());
return output_directory_path_.Append(output_file);
}
std::optional<struct BootStat::RtcTick> BootStat::GetRtcTick() const {
base::ScopedFD rtc_fd = boot_stat_system_->OpenRtc();
if (!rtc_fd.is_valid())
return std::nullopt;
// Record start time so that we can timeout if needed.
std::optional<struct timespec> tps_start = boot_stat_system_->GetUpTime();
if (!tps_start)
return std::nullopt;
std::optional<struct rtc_time> rtc_time[2];
for (int i = 0;; i++) {
int old = (i + 1) % 2;
int cur = i % 2;
std::optional<struct timespec> tps_cur = boot_stat_system_->GetUpTime();
if (!tps_cur)
return std::nullopt;
rtc_time[cur] = boot_stat_system_->GetRtcTime(&rtc_fd);
if (!rtc_time[cur])
return std::nullopt;
if (i > 0 && rtc_time[cur]->tm_sec != rtc_time[old]->tm_sec) {
// RTC ticked, record "after" time.
std::optional<struct timespec> tps_after = boot_stat_system_->GetUpTime();
if (!tps_after)
return std::nullopt;
return {{*rtc_time[cur], *tps_cur, *tps_after}};
}
// Timeout after 1.5 seconds.
if (difftime(tps_cur->tv_sec, tps_start->tv_sec) +
(tps_cur->tv_nsec - tps_start->tv_nsec) * 1e-9 >
1.5) {
LOG(ERROR) << "Timeout waiting for RTC tick.";
return std::nullopt;
}
// Don't hog the CPU too much, we don't care about sub-ms resolution
// anyway.
usleep(1000);
}
}
base::ScopedFD BootStat::OpenEventFile(const std::string& output_name_prefix,
const std::string& event_name) const {
const mode_t kFileCreationMode =
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
base::FilePath output_path = GetEventPath(output_name_prefix, event_name);
int output_fd =
HANDLE_EINTR(open(output_path.value().c_str(),
O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW | O_CLOEXEC,
kFileCreationMode));
if (output_fd < 0) {
PLOG(ERROR) << "Cannot open event file " << output_path.value() << ".";
return base::ScopedFD();
}
base::stat_wrapper_t stat;
if (base::File::Fstat(output_fd, &stat) < 0) {
PLOG(ERROR) << "Failed to stat file " << output_path.value();
return base::ScopedFD();
}
// Double check the read permissions, because umask may override us during
// creation, and we need those. (We allow write permissions to be masked.)
mode_t new_mode = stat.st_mode | S_IRGRP | S_IROTH;
if (stat.st_mode == new_mode)
return base::ScopedFD(output_fd);
// We need to force the permissions again. There's a small race here, as the
// file may exist with a umask()'ed (incorrect) mode briefly, so consumers
// should still be prepared to handle EPERM errors.
if (HANDLE_EINTR(fchmod(output_fd, new_mode)) == -1) {
PLOG(ERROR) << "Failed to set permissions for event file "
<< output_path.value();
return base::ScopedFD();
}
return base::ScopedFD(output_fd);
}
bool BootStat::LogDiskEvent(const std::string& event_name) const {
base::FilePath disk_statistics_file_path =
boot_stat_system_->GetDiskStatisticsFilePath();
if (disk_statistics_file_path.empty())
return false;
std::string data;
if (!base::ReadFileToString(disk_statistics_file_path, &data)) {
LOG(ERROR) << "Cannot read disk statistics "
<< disk_statistics_file_path.value() << ".";
return false;
}
base::ScopedFD output_fd = OpenEventFile("disk", event_name);
if (!output_fd.is_valid())
return false;
bool ret = base::WriteFileDescriptor(output_fd.get(), data);
LOG_IF(ERROR, !ret) << "Cannot write disk event.";
return ret;
}
bool BootStat::LogUptimeEvent(const std::string& event_name) const {
std::optional<struct timespec> uptime = boot_stat_system_->GetUpTime();
if (!uptime)
return false;
std::optional<base::TimeDelta> idle = boot_stat_system_->GetIdleTime();
if (!idle)
return false;
std::string data = base::StringPrintf(
"%" PRId64 ".%09ld %" PRId64 ".%09" PRId64 "\n",
static_cast<int64_t>(uptime->tv_sec), uptime->tv_nsec, idle->InSeconds(),
idle->InNanoseconds() % kNsecsPerSec);
base::ScopedFD output_fd = OpenEventFile("uptime", event_name);
if (!output_fd.is_valid())
return false;
bool ret = base::WriteFileDescriptor(output_fd.get(), data);
LOG_IF(ERROR, !ret) << "Cannot write uptime event.";
return ret;
}
std::optional<std::vector<BootStat::BootstatTiming>> BootStat::ParseUptimeEvent(
const std::string& contents) const {
auto lines = base::SplitStringPiece(contents, "\n", base::TRIM_WHITESPACE,
base::SPLIT_WANT_NONEMPTY);
std::vector<BootStat::BootstatTiming> events;
for (auto& line : lines) {
auto result = ParseDecimalColumns(line);
if (!result)
return std::nullopt;
if (result->size() != 2) {
LOG(ERROR) << "Unexpected uptime line: " << line;
return std::nullopt;
}
BootStat::BootstatTiming event = {
.uptime = (*result)[0],
.idle_time = (*result)[1],
};
events.push_back(std::move(event));
}
return events;
}
// API functions.
bool BootStat::LogEvent(const std::string& event_name) const {
bool ret = true;
ret &= LogDiskEvent(event_name);
ret &= LogUptimeEvent(event_name);
return ret;
}
bool BootStat::LogRtcSync(const char* event_name) {
std::optional<struct RtcTick> tick = GetRtcTick();
if (!tick)
return false;
base::ScopedFD output_fd = OpenEventFile("sync-rtc", event_name);
if (!output_fd.is_valid())
return false;
std::string data = base::StringPrintf(
"%" PRId64 ".%09ld %" PRId64 ".%09ld %04d-%02d-%02d %02d:%02d:%02d\n",
static_cast<int64_t>(tick->boottime_before.tv_sec),
tick->boottime_before.tv_nsec,
static_cast<int64_t>(tick->boottime_after.tv_sec),
tick->boottime_after.tv_nsec, tick->rtc_time.tm_year + 1900,
tick->rtc_time.tm_mon + 1, tick->rtc_time.tm_mday, tick->rtc_time.tm_hour,
tick->rtc_time.tm_min, tick->rtc_time.tm_sec);
bool ret = base::WriteFileDescriptor(output_fd.get(), data);
LOG_IF(ERROR, !ret) << "Cannot write rtc sync.";
return ret;
}
std::optional<std::vector<BootStat::BootstatTiming>> BootStat::GetEventTimings(
const std::string& event_name) const {
base::FilePath event_path = GetEventPath("uptime", event_name);
std::string data;
if (!base::ReadFileToString(event_path, &data)) {
PLOG(ERROR) << "Could not read event file: " << event_path;
return std::nullopt;
}
auto result = ParseUptimeEvent(data);
if (!result)
LOG(ERROR) << "Failed to parse bootstat file for event: " << event_name;
return result;
}
}; // namespace bootstat