blob: 46260230850e27d8d3f3970a6a0c28ee05925efd [file] [log] [blame]
// Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "metrics/vmlog_writer.h"
#include <fcntl.h>
#include <inttypes.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <base/bind.h>
#include <base/bind_helpers.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_split.h>
#include <base/strings/stringprintf.h>
#include <brillo/daemons/daemon.h>
namespace chromeos_metrics {
namespace {
constexpr char kVmlogHeader[] =
"time pgmajfault pgmajfault_f pgmajfault_a pswpin pswpout\n";
// We limit the size of vmlog log files to keep frequent logging from wasting
// disk space.
constexpr int kMaxVmlogFileSize = 1024 * 1024;
} // namespace
bool VmStatsParseStats(const std::string& stats, struct VmstatRecord* record) {
// a mapping of string name to field in VmstatRecord and whether we found it
struct Mapping {
const std::string name;
uint64_t* value_p;
bool found;
bool optional;
} map[] = {
{.name = "pgmajfault",
.value_p = &record->page_faults_,
.found = false,
.optional = false},
// pgmajfault_f and pgmajfault_a may not be present in all kernels.
// Don't fuss if they are not.
{.name = "pgmajfault_f",
.value_p = &record->file_page_faults_,
.found = false,
.optional = true},
{.name = "pgmajfault_a",
.value_p = &record->anon_page_faults_,
.found = false,
.optional = true},
{.name = "pswpin",
.value_p = &record->swap_in_,
.found = false,
.optional = false},
{.name = "pswpout",
.value_p = &record->swap_out_,
.found = false,
.optional = false},
};
// Each line in the file has the form
// <ID> <VALUE>
// for instance:
// nr_free_pages 213427
std::vector<std::string> lines = base::SplitString(
stats, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (const auto& line : lines) {
std::vector<std::string> tokens = base::SplitString(
line, " ", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (tokens.size() != 2u) {
LOG(WARNING) << "Unexpected vmstat format in line: " << line;
continue;
}
for (auto& mapping : map) {
if (!tokens[0].compare(mapping.name)) {
if (!base::StringToUint64(tokens[1], mapping.value_p))
return false;
mapping.found = true;
}
}
}
// Make sure we got all the stats, except the optional ones.
for (const auto& mapping : map) {
if (!mapping.found) {
if (mapping.optional) {
*mapping.value_p = 0;
} else {
LOG(WARNING) << "vmstat missing " << mapping.name;
return false;
}
}
}
return true;
}
VmlogFile::VmlogFile(const base::FilePath& live_path,
const base::FilePath& rotated_path,
const uint64_t max_size,
const std::string& header)
: live_path_(live_path),
rotated_path_(rotated_path),
max_size_(max_size),
header_(header) {
fd_ = open(live_path_.value().c_str(), O_CREAT | O_RDWR | O_EXCL, 0644);
if (fd_ != -1) {
Write(header_);
} else {
PLOG(ERROR) << "Failed to open file: " << live_path_.value();
}
}
VmlogFile::~VmlogFile() = default;
bool VmlogFile::Write(const std::string& data) {
if (fd_ == -1)
return false;
if (cur_size_ + data.size() > max_size_) {
if (!base::CopyFile(live_path_, rotated_path_)) {
PLOG(ERROR) << "Could not copy vmlog to: " << rotated_path_.value();
}
base::FilePath rotated_path_dir = rotated_path_.DirName();
base::FilePath rotated_symlink = rotated_path_dir.Append("vmlog.1.LATEST");
if (!base::PathExists(rotated_symlink)) {
if (!base::CreateSymbolicLink(rotated_path_, rotated_symlink)) {
PLOG(ERROR) << "Unable to create symbolic link from "
<< rotated_symlink.value() << " to "
<< rotated_path_.value();
}
}
if (HANDLE_EINTR(ftruncate(fd_, 0)) != 0) {
PLOG(ERROR) << "Could not ftruncate() file";
return false;
}
if (HANDLE_EINTR(lseek(fd_, 0, SEEK_SET)) != 0) {
PLOG(ERROR) << "Could not lseek() file";
return false;
}
cur_size_ = 0;
if (!Write(header_)) {
return false;
}
}
if (!base::WriteFileDescriptor(fd_, data.c_str(), data.size())) {
return false;
}
cur_size_ += data.size();
return true;
}
VmlogWriter::VmlogWriter(const base::FilePath& vmlog_dir,
const base::TimeDelta& log_interval) {
if (!base::DirectoryExists(vmlog_dir)) {
if (!base::CreateDirectory(vmlog_dir)) {
PLOG(ERROR) << "Couldn't create " << vmlog_dir.value();
return;
}
}
if (!base::SetPosixFilePermissions(vmlog_dir, 0755)) {
PLOG(ERROR) << "Couldn't set permissions for " << vmlog_dir.value();
}
Init(vmlog_dir, log_interval);
}
void VmlogWriter::Init(const base::FilePath& vmlog_dir,
const base::TimeDelta& log_interval) {
base::Time now = base::Time::Now();
// If the current time is within a day of the epoch, we probably don't have a
// good time set for naming files. Wait 5 minutes.
//
// See crbug.com/724175 for details.
if (now - base::Time::UnixEpoch() < base::TimeDelta::FromDays(1)) {
LOG(WARNING) << "Time seems incorrect, too close to epoch: " << now;
valid_time_delay_timer_.Start(FROM_HERE,
base::TimeDelta::FromMinutes(5),
base::Bind(&VmlogWriter::Init,
base::Unretained(this),
vmlog_dir,
log_interval));
return;
}
base::FilePath vmlog_current_path =
vmlog_dir.Append("vmlog." + brillo::GetTimeAsLogString(now));
base::FilePath vmlog_rotated_path =
vmlog_dir.Append("vmlog.1." + brillo::GetTimeAsLogString(now));
brillo::UpdateLogSymlinks(vmlog_dir.Append("vmlog.LATEST"),
vmlog_dir.Append("vmlog.PREVIOUS"),
vmlog_current_path);
base::DeleteFile(vmlog_dir.Append("vmlog.1.PREVIOUS"), false);
if (base::PathExists(vmlog_dir.Append("vmlog.1.LATEST"))) {
base::Move(vmlog_dir.Append("vmlog.1.LATEST"),
vmlog_dir.Append("vmlog.1.PREVIOUS"));
}
vmlog_.reset(new VmlogFile(
vmlog_current_path, vmlog_rotated_path, kMaxVmlogFileSize, kVmlogHeader));
vmstat_fd_ = open("/proc/vmstat", O_RDONLY);
if (vmstat_fd_ < 0) {
PLOG(ERROR) << "Couldn't open /proc/vmstat";
return;
}
if (!log_interval.is_zero()) {
timer_.Start(FROM_HERE, log_interval, this, &VmlogWriter::WriteCallback);
}
}
VmlogWriter::~VmlogWriter() = default;
void VmlogWriter::WriteCallback() {
if (lseek(vmstat_fd_, 0, SEEK_SET) < 0) {
PLOG(ERROR) << "Unable to lseek() /proc/vmstat";
timer_.Stop();
return;
}
// Assume that vmstat output will be no larger than 8K-1. Poking around some
// chromebooks, the size seems to generally be around 2K.
char buf[8192];
int len = HANDLE_EINTR(read(vmstat_fd_, buf, arraysize(buf) - 1));
if (len < 0) {
PLOG(ERROR) << "Unable to read() /proc/vmstat";
timer_.Stop();
return;
}
// Null terminate the data we've read
buf[len] = 0;
VmstatRecord r;
if (!VmStatsParseStats(buf, &r)) {
LOG(ERROR) << "Unable to parse vmstat data";
timer_.Stop();
return;
}
uint64_t delta_page_faults = r.page_faults_ - previous_record_.page_faults_;
uint64_t delta_file_page_faults =
r.file_page_faults_ - previous_record_.file_page_faults_;
uint64_t delta_anon_page_faults =
r.anon_page_faults_ - previous_record_.anon_page_faults_;
uint64_t delta_swap_in = r.swap_in_ - previous_record_.swap_in_;
uint64_t delta_swap_out = r.swap_out_ - previous_record_.swap_out_;
timeval tv;
gettimeofday(&tv, nullptr);
struct tm tm_time;
localtime_r(&tv.tv_sec, &tm_time);
std::string out_line = base::StringPrintf(
"[%02d%02d/%02d%02d%02d]"
" %" PRIu64 " %" PRIu64" %" PRIu64" %" PRIu64 " %" PRIu64 "\n",
tm_time.tm_mon + 1,
tm_time.tm_mday,
tm_time.tm_hour,
tm_time.tm_min,
tm_time.tm_sec,
delta_page_faults,
delta_file_page_faults,
delta_anon_page_faults,
delta_swap_in,
delta_swap_out);
if (!vmlog_->Write(out_line)) {
LOG(ERROR) << "Writing to vmlog failed.";
timer_.Stop();
return;
}
previous_record_ = r;
}
} // namespace chromeos_metrics