blob: f65d0241cf5387e4f87096eddad77dd4feaee3bb [file] [log] [blame] [edit]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "featured/c_feature_library.h"
#include "swap_management/metrics.h"
#include "swap_management/swap_tool.h"
#include "swap_management/utils.h"
#include "swap_management/zram_idle.h"
#include "swap_management/zram_recompression.h"
#include "swap_management/zram_writeback.h"
#include <algorithm>
#include <optional>
#include <vector>
#include <absl/status/status.h>
#include <base/files/dir_reader_posix.h>
#include <base/files/file_util.h>
#include <base/logging.h>
#include <base/posix/safe_strerror.h>
#include <base/process/process_metrics.h>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <base/threading/platform_thread.h>
#include <base/time/time.h>
#include <base/timer/timer.h>
namespace swap_management {
namespace {
constexpr char kSwapSizeFile[] = "/var/lib/swap/swap_size";
// The default size of zram is twice the device's memory size.
constexpr float kDefaultZramSizeToMemTotalMultiplier = 2.0;
constexpr VariationsFeature kSwapZramCompAlgorithmFeature{
"CrOSLateBootSwapZramCompAlgorithm", FEATURE_ENABLED_BY_DEFAULT};
constexpr VariationsFeature kSwapZramDisksizeFeature{
"CrOSLateBootSwapZramDisksize", FEATURE_DISABLED_BY_DEFAULT};
constexpr VariationsFeature kSwapZramWritebackFeature{
"CrOSLateBootSwapZramWriteback", FEATURE_ENABLED_BY_DEFAULT};
constexpr VariationsFeature kSwapZramRecompressionFeature{
"CrOSLateBootSwapZramRecompression", FEATURE_ENABLED_BY_DEFAULT};
// Reclaimable memory types.
//
// The values represent bits that must be alligned with their corresponding
// RECLAIM_MEMORY_* value from the D-Bus interface.
constexpr std::array kReclaimTypes{
std::pair{0x01, "anon"}, std::pair{0x02, "shmem"}, std::pair{0x04, "file"}};
} // namespace
SwapTool::SwapTool(feature::PlatformFeatures* platform_features)
: platform_features_(platform_features) {}
// Check if swap is already turned on.
absl::StatusOr<bool> SwapTool::IsZramSwapOn() {
std::string swaps;
absl::Status status =
Utils::Get()->ReadFileToString(base::FilePath("/proc/swaps"), &swaps);
if (!status.ok())
return status;
std::vector<std::string> swaps_lines = base::SplitString(
swaps, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// Skip the first line which is header. Swap is turned on if swaps_lines
// contains entry with zram0 keyword.
for (size_t i = 1; i < swaps_lines.size(); i++) {
if (swaps_lines[i].find("zram0") != std::string::npos)
return true;
}
return false;
}
// Return user runtime config zram size in byte for swap.
// kSwapSizeFile contains the zram size in MiB.
// Return 0 if swap is disabled, and NotFoundError if kSwapSizeFile is empty.
// Otherwise propagate error back, and the following code should calculate zram
// size based on MemTotal/features instead.
absl::StatusOr<uint64_t> SwapTool::GetUserConfigZramSizeBytes() {
// For security, only read first few bytes of kSwapSizeFile.
std::string buf;
absl::Status status = Utils::Get()->ReadFileToStringWithMaxSize(
base::FilePath(kSwapSizeFile), &buf, 5);
if (!status.ok())
return status;
// Trim the potential leading/trailing ASCII whitespaces.
// Note that TrimWhitespaceASCII can safely use the same variable for inputs
// and outputs.
base::TrimWhitespaceASCII(buf, base::TRIM_ALL, &buf);
if (buf.empty())
return absl::InvalidArgumentError(std::string(kSwapSizeFile) +
" is empty.");
absl::StatusOr<uint64_t> requested_size_mib =
Utils::Get()->SimpleAtoi<uint64_t>(buf);
if (!requested_size_mib.ok())
return requested_size_mib.status();
if (*requested_size_mib == 0)
LOG(WARNING) << "swap is disabled since " << std::string(kSwapSizeFile)
<< " contains 0.";
return (*requested_size_mib) * 1024 * 1024;
}
// Set comp_algorithm, override if kSwapZramCompAlgorithmFeature is enabled.
void SwapTool::SetCompAlgorithm() {
// Start from M126, we enable recompression with lz4 as default compression
// algorithm. Since lz4 can only improve performance with recompression, we
// will use the system default comp algorithm if recompression cannot be
// enabled.
std::string comp_algorithm = zram_recompression_configured_ ? "lz4" : "";
auto params = GetFeatureParams(kSwapZramCompAlgorithmFeature);
if (params.has_value() && (*params).find("comp_algorithm") != (*params).end())
comp_algorithm = (*params)["comp_algorithm"];
if (!comp_algorithm.empty()) {
LOG(INFO) << "Setting zram comp_algorithm to " << comp_algorithm;
absl::Status status = Utils::Get()->WriteFile(
base::FilePath(kZramSysfsDir).Append("comp_algorithm"), comp_algorithm);
LOG_IF(WARNING, !status.ok())
<< "Failed to set zram comp_algorithm: " << status;
}
}
// Get zram size in byte.
// There are two factor to decide the size: user runtime config and
// feature.
// 1. User runtime config:
// Read size in MiB in kSwapSizeFile (programmed by SwapSetSize).
// 0 means disable zram.
// 2. Feature (kSwapZramDisksizeFeature):
// If the feature is available, load multiplier from features.
// Then size = mem_total * multiplier (2 by default).
// We first check if user runtime config is available, if not then
// feature, if not then finally using default zram size.
absl::StatusOr<uint64_t> SwapTool::GetZramSizeBytes() {
// 1. User runtime config
absl::StatusOr<uint64_t> size_byte = GetUserConfigZramSizeBytes();
// Return since user has runtime config for zram size, or swap is disabled.
if (size_byte.ok())
return size_byte;
// Let's provide log for errors other than NotFoundError which is valid, and
// continue.
LOG_IF(WARNING, !absl::IsNotFound(size_byte.status()))
<< "Failed to get user config zram size: " << size_byte.status();
// 2. Feature
// First, read /proc/meminfo for MemTotal in kiB.
absl::StatusOr<base::SystemMemoryInfoKB> meminfo =
Utils::Get()->GetSystemMemoryInfo();
if (!meminfo.ok())
return meminfo.status();
// Then check if feature kSwapZramDisksizeFeature is available.
float multiplier = kDefaultZramSizeToMemTotalMultiplier;
std::optional<std::string> feature_multiplier =
GetFeatureParamValue(kSwapZramDisksizeFeature, "multiplier");
if (feature_multiplier.has_value()) {
if (!absl::SimpleAtof(*feature_multiplier, &multiplier)) {
LOG(WARNING) << absl::OutOfRangeError(
"Failed to convert " + *feature_multiplier +
" to float. Using default zram size multiplier.");
multiplier = kDefaultZramSizeToMemTotalMultiplier;
}
}
// Should roundup with page size.
return Utils::Get()->RoundupMultiple(
static_cast<uint64_t>((*meminfo).total) * 1024 * multiplier, kPageSize);
}
// Enable zram recompression if kSwapZramRecompressionFeature is enabled.
absl::Status SwapTool::EnableZramRecompression() {
if (!ZramRecompression::Get()->KernelSupportsZramRecompression())
return absl::OkStatus();
// Check if feature is enabled, and get the params.
auto params = GetFeatureParams(kSwapZramRecompressionFeature);
if (!params.has_value())
return absl::OkStatus();
// Read config from feature and override the default.
for (const auto& [key, value] : *params) {
absl::Status status =
ZramRecompression::Get()->SetZramRecompressionConfigIfOverriden(key,
value);
LOG_IF(WARNING, !status.ok()) << "Failed to set zram recompression config ["
<< key << ": " << value << "]: " << status;
}
absl::Status status = ZramRecompression::Get()->EnableRecompression();
if (status.ok())
zram_recompression_configured_ = true;
return status;
}
// Return params map for the feature.
std::optional<std::map<std::string, std::string>> SwapTool::GetFeatureParams(
const VariationsFeature& vf) {
if (!platform_features_) {
LOG(ERROR) << "PlatformFeature is not available.";
return std::nullopt;
}
feature::PlatformFeaturesInterface::ParamsResult result =
platform_features_->GetParamsAndEnabledBlocking({&vf});
if (result.find(vf.name) != result.end() && result[vf.name].enabled)
return result[vf.name].params;
return std::nullopt;
}
// Return value for params in feature if feature is enabled.
std::optional<std::string> SwapTool::GetFeatureParamValue(
const VariationsFeature& vf, const std::string& key) {
auto params = GetFeatureParams(vf);
if (!params.has_value())
return std::nullopt;
if ((*params).find(key) != (*params).end())
return (*params)[key];
LOG(ERROR) << key << " is not configured in PlatformFeature " << vf.name;
return std::nullopt;
}
// Run swapon to enable zram swapping.
// swapon may fail because of races with other programs that inspect all
// block devices, so try several times.
absl::Status SwapTool::EnableZramSwapping() {
constexpr uint8_t kMaxEnableTries = 10;
constexpr base::TimeDelta kRetryDelayUs = base::Milliseconds(100);
absl::Status status = absl::OkStatus();
for (size_t i = 0; i < kMaxEnableTries; i++) {
status = Utils::Get()->RunProcessHelper({"/sbin/swapon", kZramDeviceFile});
if (status.ok())
return status;
LOG(WARNING) << "swapon " << kZramDeviceFile << " failed, try " << i
<< " times, last error:" << status;
base::PlatformThread::Sleep(kRetryDelayUs);
}
return absl::AbortedError("swapon " + std::string(kZramDeviceFile) +
" failed after " + std::to_string(kMaxEnableTries) +
" tries" + " last error: " + status.ToString());
}
absl::Status SwapTool::SwapStart() {
absl::Status status = absl::OkStatus();
// Return true if swap is already on.
absl::StatusOr<bool> on = IsZramSwapOn();
if (!on.ok())
return on.status();
if (*on) {
LOG(WARNING) << "Swap is already on.";
return absl::OkStatus();
}
// Get zram size.
absl::StatusOr<uint64_t> size_byte = GetZramSizeBytes();
if (!size_byte.ok() || *size_byte == 0)
return status;
// Load zram module. Ignore failure (it could be compiled in the kernel).
if (!Utils::Get()->RunProcessHelper({"/usr/bin/modprobe", "zram"}).ok())
LOG(WARNING) << "modprobe zram failed (compiled?)";
// Enable zram recompression if feature is available.
status = EnableZramRecompression();
LOG_IF(WARNING, !status.ok())
<< "Failed to enable zram recompression: " << status;
// Set zram compress algorithm.
SetCompAlgorithm();
// Set zram size.
LOG(INFO) << "Setting zram disksize to " << *size_byte << " bytes";
status =
Utils::Get()->WriteFile(base::FilePath(kZramSysfsDir).Append("disksize"),
std::to_string(*size_byte));
if (!status.ok())
return status;
// Set swap area.
status = Utils::Get()->RunProcessHelper({"/sbin/mkswap", kZramDeviceFile});
if (!status.ok())
return status;
// Enable zram swap.
status = EnableZramSwapping();
if (!status.ok())
return status;
// Enable zram writeback if feature is available.
status = EnableZramWriteback();
LOG_IF(ERROR, !status.ok()) << "Failed to enable zram writeback: " << status;
// Start zram recompression.
if (zram_recompression_configured_)
ZramRecompression::Get()->Start();
// Start periodically reporting zram and psi metrics.
Metrics::Get()->Start();
return absl::OkStatus();
}
absl::Status SwapTool::SwapStop() {
// Return false if swap is already off.
absl::StatusOr<bool> on = IsZramSwapOn();
if (!on.ok())
return on.status();
if (!*on) {
LOG(WARNING) << "Swap is already off.";
return absl::OkStatus();
}
// Stop zram writeback.
ZramWriteback::Get()->Stop();
// It is possible that the Filename of swap file zram0 in /proc/swaps shows
// wrong path "/zram0", since devtmpfs in minijail mount namespace is lazily
// unmounted while swap_management terminates.
// At this point we already know swap is on, with the only swap device
// /dev/zram0 we have, anyway we turn off /dev/zram0, regardless what
// /proc/swaps shows.
absl::Status status =
Utils::Get()->RunProcessHelper({"/sbin/swapoff", "-v", kZramDeviceFile});
if (!status.ok())
return status;
// When we start up, we try to configure zram0, but it doesn't like to
// be reconfigured on the fly. Reset it so we can changes its params.
// If there was a backing device being used, it will be automatically
// removed because after it's created it was removed with deferred remove.
return Utils::Get()->WriteFile(base::FilePath(kZramSysfsDir).Append("reset"),
"1");
}
// Set zram disksize in MiB.
// If `size` equals 0, set zram size file to the default value.
// If `size` is negative, set zram size file to 0. Swap is disabled if zram size
// file contains 0.
absl::Status SwapTool::SwapSetSize(int32_t size) {
// Remove kSwapSizeFile so SwapStart will use default size for zram.
if (size == 0) {
return Utils::Get()->DeleteFile(base::FilePath(kSwapSizeFile));
} else if (size < 0) {
size = 0;
} else if (size < 128 || size > 65000) {
return absl::InvalidArgumentError("Size is not between 128 and 65000 MiB.");
}
return Utils::Get()->WriteFile(base::FilePath(kSwapSizeFile),
std::to_string(size));
}
absl::Status SwapTool::SwapSetSwappiness(uint32_t swappiness) {
// Only allow swappiness between 0 and 100.
if (swappiness > 100)
return absl::OutOfRangeError("Invalid swappiness " +
std::to_string(swappiness));
return Utils::Get()->WriteFile(base::FilePath("/proc/sys/vm/swappiness"),
std::to_string(swappiness));
}
std::string SwapTool::SwapStatus() {
std::stringstream output;
std::string tmp;
// Show general swap info first.
if (Utils::Get()->ReadFileToString(base::FilePath("/proc/swaps"), &tmp).ok())
output << tmp;
// Show tunables.
if (Utils::Get()
->ReadFileToString(base::FilePath("/proc/sys/vm/min_filelist_kbytes"),
&tmp)
.ok())
output << "min_filelist_kbytes (KiB): " + tmp;
if (Utils::Get()
->ReadFileToString(base::FilePath("/proc/sys/vm/extra_free_kbytes"),
&tmp)
.ok())
output << "extra_free_kbytes (KiB): " + tmp;
// Show top entries in kZramSysfsDir for zram setting.
base::DirReaderPosix dir_reader(kZramSysfsDir);
if (dir_reader.IsValid()) {
output << "\ntop-level entries in " + std::string(kZramSysfsDir) + ":\n";
base::FilePath zram_sysfs(kZramSysfsDir);
while (dir_reader.Next()) {
std::string name = dir_reader.name();
if (Utils::Get()->ReadFileToString(zram_sysfs.Append(name), &tmp).ok() &&
!tmp.empty()) {
std::vector<std::string> lines = base::SplitString(
tmp, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
for (auto& line : lines)
output << name + ": " + line + "\n";
}
}
}
return output.str();
}
absl::Status SwapTool::SwapZramEnableWriteback(uint32_t size_mb) {
return ZramWriteback::Get()->EnableWriteback(size_mb);
}
absl::Status SwapTool::SwapZramSetWritebackLimit(uint32_t num_pages) {
return ZramWriteback::Get()->SetWritebackLimit(num_pages);
}
absl::Status SwapTool::SwapZramMarkIdle(uint32_t age_seconds) {
return MarkIdle(age_seconds);
}
absl::Status SwapTool::InitiateSwapZramWriteback(ZramWritebackMode mode) {
return ZramWriteback::Get()->InitiateWriteback(mode);
}
absl::Status SwapTool::MGLRUSetEnable(uint8_t value) {
return Utils::Get()->WriteFile(
base::FilePath("/sys/kernel/mm/lru_gen/enabled"), std::to_string(value));
}
absl::Status SwapTool::EnableZramWriteback() {
// Check if feature is enabled, and get the params.
auto params = GetFeatureParams(kSwapZramWritebackFeature);
if (!params.has_value())
return absl::OkStatus();
// Read config from feature and override the default.
for (const auto& [key, value] : *params) {
absl::Status status =
ZramWriteback::Get()->SetZramWritebackConfigIfOverriden(key, value);
LOG_IF(WARNING, !status.ok()) << "Failed to set zram writeback config ["
<< key << ": " << value << "]: " << status;
}
return ZramWriteback::Get()->Start();
}
absl::Status SwapTool::ReclaimAllProcesses(uint8_t memory_types) {
std::string pid = std::to_string(getpid());
base::DirReaderPosix dir_reader("/proc");
if (!dir_reader.IsValid()) {
PLOG(ERROR) << "Can't access /proc";
return absl::AbortedError("Can't access /proc");
}
base::FilePath proc_dir("/proc");
while (dir_reader.Next()) {
std::string name = dir_reader.name();
// Don't reclaim our own memory and skip non-PID directories.
if (name == pid || !std::all_of(name.begin(), name.end(), ::isdigit))
continue;
base::FilePath pid_dir = proc_dir.Append(name);
base::FilePath reclaim_file = pid_dir.Append("reclaim");
for (const auto& reclaim_type : kReclaimTypes) {
if (!(memory_types & reclaim_type.first))
continue;
absl::Status status =
Utils::Get()->WriteFile(reclaim_file, reclaim_type.second);
LOG_IF(WARNING, !status.ok())
<< "Failed to reclaim memory (" << reclaim_type.second
<< ") for process with PID " << pid;
}
}
return absl::OkStatus();
}
} // namespace swap_management