blob: 1eb6b8aeebe299ad3aa06f6b017533bc3d17199a [file] [log] [blame] [edit]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "swap_management/metrics.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <absl/status/status.h>
#include <base/logging.h>
#include <base/metrics/histogram_functions.h>
#include <base/metrics/histogram_macros.h>
#include <base/strings/string_split.h>
#include "swap_management/utils.h"
#include "swap_management/zram_stats.h"
namespace swap_management {
namespace {
constexpr uint32_t kNumAbslStatus = 21;
// Default period for reporting psi and zram metrics. Must be either 10, 60 or
// 300 to match psi report.
constexpr uint32_t kDefaultPeriodSec = 10;
// Max number of pages should be the max system memory divided by the smallest
// possible page size, or: 32GB / 4096
constexpr uint64_t kMaxNumPages = (static_cast<uint64_t>(32) << 30) / (4096);
} // namespace
Metrics* Metrics::Get() {
return *GetSingleton<Metrics>();
}
Metrics::~Metrics() {
Stop();
}
void Metrics::OverrideForTesting(Metrics* metrics) {
[[maybe_unused]] static bool is_overridden = []() -> bool {
if (*GetSingleton<Metrics>()) {
delete *GetSingleton<Metrics>();
}
return true;
}();
*GetSingleton<Metrics>() = metrics;
}
void Metrics::ReportSwapStartStatus(absl::Status status) {
metrics_.SendEnumToUMA("ChromeOS.SwapManagement.SwapStart.Status",
static_cast<int>(status.code()), kNumAbslStatus);
}
void Metrics::ReportSwapStopStatus(absl::Status status) {
metrics_.SendEnumToUMA("ChromeOS.SwapManagement.SwapStop.Status",
static_cast<int>(status.code()), kNumAbslStatus);
}
void Metrics::PeriodicReportZramMetrics() {
absl::StatusOr<ZramMmStat> zram_mm_stat = GetZramMmStat();
LOG_IF(ERROR, !zram_mm_stat.ok())
<< "Failed to read zram mm stat: " << zram_mm_stat.status();
if (zram_mm_stat.ok()) {
const uint64_t kTotalPagesSwapped =
(*zram_mm_stat).orig_data_size / kPageSize;
metrics_.SendToUMA("ChromeOS.Zram.OrigDataSizeMB",
(*zram_mm_stat).orig_data_size / kMiB, 1, 64000, 100);
metrics_.SendToUMA("ChromeOS.Zram.ComprDataSizeMB",
(*zram_mm_stat).compr_data_size / kMiB, 1, 64000, 100);
metrics_.SendPercentageToUMA("ChromeOS.Zram.CompressedSizePct",
(*zram_mm_stat).orig_data_size
? ((*zram_mm_stat).compr_data_size *
100.0 / (*zram_mm_stat).orig_data_size)
: 0);
metrics_.SendToUMA("ChromeOS.Zram.MemUsedTotalMB",
(*zram_mm_stat).mem_used_total / kMiB, 1, 64000, 100);
metrics_.SendToUMA("ChromeOS.Zram.MemLimitMB",
(*zram_mm_stat).mem_limit / kMiB, 1, 64000, 100);
metrics_.SendToUMA("ChromeOS.Zram.MemUsedMaxMB",
(*zram_mm_stat).mem_used_max / kMiB, 1, 64000, 100);
metrics_.SendToUMA("ChromeOS.Zram.SamePages", (*zram_mm_stat).same_pages, 1,
kMaxNumPages, 50);
metrics_.SendPercentageToUMA(
"ChromeOS.Zram.SamePagesPct",
kTotalPagesSwapped
? (*zram_mm_stat).same_pages * 100.0 / kTotalPagesSwapped
: 0);
metrics_.SendToUMA("ChromeOS.Zram.PagesCompacted",
(*zram_mm_stat).pages_compacted, 1, kMaxNumPages, 50);
if ((*zram_mm_stat).huge_pages) {
metrics_.SendToUMA("ChromeOS.Zram.HugePages", *(*zram_mm_stat).huge_pages,
1, kMaxNumPages, 50);
metrics_.SendPercentageToUMA(
"ChromeOS.Zram.HugePagesPct",
kTotalPagesSwapped
? *(*zram_mm_stat).huge_pages * 100.0 / kTotalPagesSwapped
: 0);
if ((*zram_mm_stat).huge_pages_since) {
metrics_.SendToUMA("ChromeOS.Zram.HugePagesSince",
*(*zram_mm_stat).huge_pages_since, 1, kMaxNumPages,
50);
if (has_old_huge_pages_) {
int64_t stored =
*(*zram_mm_stat).huge_pages_since - old_huge_pages_since_;
// The delta in 'stored' minus the growth in state is the number of
// pages removed.
int64_t removed =
stored - (*(*zram_mm_stat).huge_pages - old_huge_pages_);
if (stored >= 0 && removed >= 0) {
metrics_.SendToUMA("ChromeOS.Zram.HugePagesStored", stored, 1,
kMaxNumPages, 50);
metrics_.SendToUMA("ChromeOS.Zram.HugePagesRemoved", removed, 1,
kMaxNumPages, 50);
}
}
// Save for next time.
has_old_huge_pages_ = true;
old_huge_pages_ = *(*zram_mm_stat).huge_pages;
old_huge_pages_since_ = *(*zram_mm_stat).huge_pages_since;
}
}
}
absl::StatusOr<ZramBdStat> zram_bd_stat = GetZramBdStat();
LOG_IF(ERROR, !zram_bd_stat.ok())
<< "Failed to read zram bd stat: " << zram_bd_stat.status();
if (zram_bd_stat.ok()) {
metrics_.SendToUMA("ChromeOS.Zram.BdCount", (*zram_bd_stat).bd_count, 1,
1000000, 50);
metrics_.SendToUMA("ChromeOS.Zram.BdReads", (*zram_bd_stat).bd_reads, 1,
1000000, 50);
metrics_.SendToUMA("ChromeOS.Zram.BdWrites", (*zram_bd_stat).bd_writes, 1,
1000000, 50);
}
absl::StatusOr<ZramIoStat> zram_io_stat = GetZramIoStat();
LOG_IF(ERROR, !zram_io_stat.ok())
<< "Failed to read zram io stat: " << zram_io_stat.status();
metrics_.SendToUMA("ChromeOS.Zram.FailedReads", (*zram_io_stat).failed_reads,
1, 1000, 50);
metrics_.SendToUMA("ChromeOS.Zram.FailedWrites",
(*zram_io_stat).failed_writes, 1, 1000, 50);
metrics_.SendToUMA("ChromeOS.Zram.InvalidIo", (*zram_io_stat).invalid_io, 1,
1000, 50);
metrics_.SendToUMA("ChromeOS.Zram.NotifyFree", (*zram_io_stat).notify_free, 1,
1000, 50);
// Values as logged in the histogram for (Memory|CPU|IO) pressure.
constexpr uint32_t kPressureMin = 1; // As 0 is for underflow.
constexpr uint32_t kPressureExclusiveMax = 10000;
constexpr uint32_t kPressureHistogramBuckets = 100;
absl::StatusOr<std::vector<uint32_t>> psi_memory_metrics =
PSIParser(base::FilePath("/proc/pressure/memory"), kDefaultPeriodSec);
LOG_IF(ERROR, !psi_memory_metrics.ok())
<< "Failed to read PSI memory metrics: " << psi_memory_metrics.status();
if (psi_memory_metrics.ok()) {
metrics_.SendToUMA("ChromeOS.CWP.PSIMemPressure.Some",
(*psi_memory_metrics)[0], kPressureMin,
kPressureExclusiveMax, kPressureHistogramBuckets);
metrics_.SendToUMA("ChromeOS.CWP.PSIMemPressure.Full",
(*psi_memory_metrics)[1], kPressureMin,
kPressureExclusiveMax, kPressureHistogramBuckets);
}
absl::StatusOr<std::vector<uint32_t>> psi_cpu_metrics =
PSIParser(base::FilePath("/proc/pressure/cpu"), kDefaultPeriodSec);
LOG_IF(ERROR, !psi_cpu_metrics.ok())
<< "Failed to read PSI cpu metrics: " << psi_cpu_metrics.status();
if (psi_cpu_metrics.ok()) {
metrics_.SendToUMA("ChromeOS.CWP.PSICpuPressure.Some",
(*psi_cpu_metrics)[0], kPressureMin,
kPressureExclusiveMax, kPressureHistogramBuckets);
metrics_.SendToUMA("ChromeOS.CWP.PSICpuPressure.Full",
(*psi_cpu_metrics)[1], kPressureMin,
kPressureExclusiveMax, kPressureHistogramBuckets);
}
absl::StatusOr<std::vector<uint32_t>> psi_io_metrics =
PSIParser(base::FilePath("/proc/pressure/io"), kDefaultPeriodSec);
LOG_IF(ERROR, !psi_io_metrics.ok())
<< "Failed to read PSI io metrics: " << psi_io_metrics.status();
if (psi_io_metrics.ok()) {
metrics_.SendToUMA("ChromeOS.CWP.PSIIoPressure.Some", (*psi_io_metrics)[0],
kPressureMin, kPressureExclusiveMax,
kPressureHistogramBuckets);
metrics_.SendToUMA("ChromeOS.CWP.PSIIoPressure.Full", (*psi_io_metrics)[1],
kPressureMin, kPressureExclusiveMax,
kPressureHistogramBuckets);
}
}
void Metrics::Start() {
// Start periodic writeback.
metrics_timer_.Start(FROM_HERE, base::Seconds(kDefaultPeriodSec),
base::BindRepeating(&Metrics::PeriodicReportZramMetrics,
weak_factory_.GetWeakPtr()));
}
void Metrics::Stop() {
metrics_timer_.Stop();
}
absl::StatusOr<std::vector<uint32_t>> Metrics::PSIParser(base::FilePath path,
uint32_t period) {
if (period != 10 && period != 60 && period != 300) {
return absl::InvalidArgumentError("Invalid PSI period " +
std::to_string(period));
}
std::string content;
absl::Status status = Utils::Get()->ReadFileToString(path, &content);
if (!status.ok()) {
return status;
}
std::vector<std::string> tokens = base::SplitString(
content, " \n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// Example output for /proc/pressure/memory:
// some avg10=0.10 avg60=3.85 avg300=2.01 total=7693280
// full avg10=0.10 avg60=3.85 avg300=2.01 total=7689487
// After split with space and newline, there will be two tokens start with
// prefix "avg|period|=", the first is some psi, and the next is full psi, in
// |period|.
std::vector<uint32_t> res;
std::string metric_prefix = "avg" + std::to_string(period) + "=";
for (const std::string& t : tokens) {
if (t.find(metric_prefix) != std::string::npos) {
std::string metric_in_text =
t.substr(t.find(metric_prefix) + metric_prefix.length());
absl::StatusOr<double> metric = Utils::Get()->SimpleAtod(metric_in_text);
if (!metric.ok()) {
return metric.status();
}
// Want to multiply by 100, but to avoid integer truncation,
// do best-effort rounding.
const uint32_t preround = static_cast<uint32_t>(*metric * 1000);
res.push_back((preround + 5) / 10);
}
}
// Sanity check if res contains two entries.
if (res.size() != 2) {
return absl::InternalError("Failed to parse PSI metrics.");
}
return res;
}
void Metrics::EnableZramWritebackMetrics() {
last_zram_bd_stat_ = std::make_unique<ZramBdStat>();
// Report writeback metrics every 24hr.
writeback_metrics_timer_.Start(
FROM_HERE, base::Days(1),
base::BindRepeating(&Metrics::PeriodicReportZramWritebackMetrics,
weak_factory_.GetWeakPtr()));
}
void Metrics::PeriodicReportZramWritebackMetrics() {
absl::StatusOr<ZramBdStat> zram_bd_stat = GetZramBdStat();
LOG_IF(ERROR, !zram_bd_stat.ok())
<< "Failed to read zram bd stat: " << zram_bd_stat.status();
uint64_t bd_write_delta =
(*zram_bd_stat).bd_writes - last_zram_bd_stat_->bd_writes;
metrics_.SendToUMA("ChromeOS.Zram.WritebackPagesPerDay", bd_write_delta, 0,
(4ul << 30) / kPageSize, 100);
last_zram_bd_stat_ = std::make_unique<ZramBdStat>(std::move(*zram_bd_stat));
}
} // namespace swap_management