blob: 10b12d7f641893cd9709ca82637a5980b3301420 [file] [log] [blame]
// Copyright 2021 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.
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use anyhow::{bail, Context, Result};
use once_cell::sync::Lazy;
use sys_util::error;
use crate::common;
const GAME_MODE_OFFSET_KB: u64 = 300 * 1024;
/// calculate_reserved_free_kb() calculates the reserved free memory in KiB from
/// /proc/zoneinfo. Reserved pages are free pages reserved for emergent kernel
/// allocation and are not available to the user space. It's the sum of high
/// watermarks and max protection pages of memory zones. It implements the same
/// reserved pages calculation in linux kernel calculate_totalreserve_pages().
///
/// /proc/zoneinfo example:
/// ...
/// Node 0, zone DMA32
/// pages free 422432
/// min 16270
/// low 20337
/// high 24404
/// ...
/// protection: (0, 0, 1953, 1953)
///
/// The high field is the high watermark for this zone. The protection field is
/// the protected pages for lower zones. See the lowmem_reserve_ratio section
/// in https://www.kernel.org/doc/Documentation/sysctl/vm.txt.
///
/// It's pub for unit test.
pub fn calculate_reserved_free_kb<R: BufRead>(reader: R) -> Result<u64> {
let page_size_kb = 4;
let mut num_reserved_pages: u64 = 0;
for line in reader.lines() {
let line = line?;
let mut tokens = line.split_whitespace();
let key = if let Some(k) = tokens.next() {
k
} else {
continue;
};
if key == "high" {
num_reserved_pages += if let Some(v) = tokens.next() {
v.parse::<u64>()
.with_context(|| format!("Couldn't parse the high field: {}", line))?
} else {
0
};
} else if key == "protection:" {
num_reserved_pages += tokens.try_fold(0u64, |maximal, token| -> Result<u64> {
let pattern = &['(', ')', ','][..];
let num = token
.trim_matches(pattern)
.parse::<u64>()
.with_context(|| format!("Couldn't parse protection field: {}", line))?;
Ok(std::cmp::max(maximal, num))
})?;
}
}
Ok(num_reserved_pages * page_size_kb)
}
fn get_reserved_memory_kb() -> Result<u64> {
// Reserve free pages is high watermark + lowmem_reserve. extra_free_kbytes
// raises the high watermark. Nullify the effect of extra_free_kbytes by
// excluding it from the reserved pages. The default extra_free_kbytes
// value is 0 if the file couldn't be accessed.
let reader = File::open(Path::new("/proc/zoneinfo"))
.map(BufReader::new)
.context("Couldn't read /proc/zoneinfo")?;
Ok(calculate_reserved_free_kb(reader)?
- common::read_file_to_u64("/proc/sys/vm/extra_free_kbytes").unwrap_or(0))
}
/// Returns the percentage of the recent 10 seconds that some process is blocked
/// by memory.
/// Example input:
/// some avg10=0.00 avg60=0.00 avg300=0.00 total=0
/// full avg10=0.00 avg60=0.00 avg300=0.00 total=0
///
/// It's pub for unit test.
pub fn parse_psi_memory<R: BufRead>(reader: R) -> Result<f64> {
for line in reader.lines() {
let line = line?;
let mut tokens = line.split_whitespace();
if tokens.next() != Some("some") {
continue;
}
if let Some(pair) = tokens.next() {
let mut elements = pair.split('=');
if elements.next() != Some("avg10") {
continue;
}
if let Some(value) = elements.next() {
return value.parse().context("Couldn't parse the avg10 value");
}
}
bail!("Couldn't parse /proc/pressure/memory, line: {}", line);
}
bail!("Couldn't parse /proc/pressure/memory");
}
#[allow(dead_code)]
fn get_psi_memory_pressure_10_seconds() -> Result<f64> {
let reader = File::open(Path::new("/proc/pressure/memory"))
.map(BufReader::new)
.context("Couldn't read /proc/pressure/memory")?;
parse_psi_memory(reader)
}
/// Struct to hold parsed /proc/meminfo data, only contains used fields.
///
/// It's pub for unit test.
#[derive(Default)]
pub struct MemInfo {
total: u64,
pub free: u64,
pub active_anon: u64,
pub inactive_anon: u64,
pub active_file: u64,
pub inactive_file: u64,
pub dirty: u64,
pub swap_free: u64,
}
/// Parsing /proc/meminfo.
///
/// It's pub for unit test.
pub fn parse_meminfo<R: BufRead>(reader: R) -> Result<MemInfo> {
let mut result = MemInfo::default();
for line in reader.lines() {
let line = line?;
let mut tokens = line.split_whitespace();
let key = if let Some(k) = tokens.next() {
k
} else {
continue;
};
let value = if let Some(v) = tokens.next() {
v.parse()?
} else {
continue;
};
if key == "MemTotal:" {
result.total = value;
} else if key == "MemFree:" {
result.free = value;
} else if key == "Active(anon):" {
result.active_anon = value;
} else if key == "Inactive(anon):" {
result.inactive_anon = value;
} else if key == "Active(file):" {
result.active_file = value;
} else if key == "Inactive(file):" {
result.inactive_file = value;
} else if key == "Dirty:" {
result.dirty = value;
} else if key == "SwapFree:" {
result.swap_free = value;
}
}
Ok(result)
}
/// Return MemInfo object containing /proc/meminfo data.
fn get_meminfo() -> Result<MemInfo> {
let reader = File::open(Path::new("/proc/meminfo"))
.map(BufReader::new)
.context("Couldn't read /proc/meminfo")?;
parse_meminfo(reader)
}
/// calculate_available_memory_kb implements similar available memory
/// calculation as kernel function get_available_mem_adj(). The available memory
/// consists of 3 parts: the free memory, the file cache, and the swappable
/// memory. The available free memory is free memory minus reserved free memory.
/// The available file cache is the total file cache minus reserved file cache
/// (min_filelist). Because swapping is prohibited if there is no anonymous
/// memory or no swap free, the swappable memory is the minimal of anonymous
/// memory and swap free. As swapping memory is more costly than dropping file
/// cache, only a fraction (1 / ram_swap_weight) of the swappable memory
/// contributes to the available memory.
///
/// It's pub for unit test.
pub fn calculate_available_memory_kb(
info: &MemInfo,
reserved_free: u64,
min_filelist: u64,
ram_swap_weight: u64,
) -> u64 {
let free = info.free;
let anon = info.active_anon.saturating_add(info.inactive_anon);
let file = info.active_file.saturating_add(info.inactive_file);
let dirty = info.dirty;
let free_component = free.saturating_sub(reserved_free);
let cache_component = file.saturating_sub(dirty).saturating_sub(min_filelist);
let swappable = std::cmp::min(anon, info.swap_free);
let swap_component = if ram_swap_weight != 0 {
swappable / ram_swap_weight
} else {
0
};
free_component
.saturating_add(cache_component)
.saturating_add(swap_component)
}
struct MemoryParameters {
reserved_free: u64,
min_filelist: u64,
ram_swap_weight: u64,
}
fn get_memory_parameters() -> MemoryParameters {
static RESERVED_FREE: Lazy<u64> = Lazy::new(|| match get_reserved_memory_kb() {
Ok(reserved) => reserved,
Err(e) => {
error!("get_reserved_memory_kb failed: {}", e);
0
}
});
let min_filelist: u64 =
common::read_file_to_u64("/proc/sys/vm/min_filelist_kbytes").unwrap_or(0);
// TODO(vovoy): Use a regular config file instead of sysfs file.
static RAM_SWAP_WEIGHT: Lazy<u64> = Lazy::new(|| {
common::read_file_to_u64("/sys/kernel/mm/chromeos-low_mem/ram_vs_swap_weight").unwrap_or(0)
});
MemoryParameters {
reserved_free: *RESERVED_FREE,
min_filelist,
ram_swap_weight: *RAM_SWAP_WEIGHT,
}
}
fn get_available_memory_kb() -> Result<u64> {
let meminfo = get_meminfo()?;
let p = get_memory_parameters();
Ok(calculate_available_memory_kb(
&meminfo,
p.reserved_free,
p.min_filelist,
p.ram_swap_weight,
))
}
pub fn get_foreground_available_memory_kb() -> Result<u64> {
get_available_memory_kb()
}
pub fn get_background_available_memory_kb() -> Result<u64> {
let available = get_available_memory_kb()?;
if common::get_game_mode()? != common::GameMode::Off {
if available > GAME_MODE_OFFSET_KB {
Ok(available - GAME_MODE_OFFSET_KB)
} else {
Ok(0)
}
} else {
Ok(available)
}
}
// It's pub for unit test.
pub fn parse_margins<R: BufRead>(reader: R) -> Result<Vec<u64>> {
let first_line = reader
.lines()
.next()
.context("No content in margin buffer")??;
let margins = first_line
.split_whitespace()
.map(|x| x.parse().context("Couldn't parse an element in margins"))
.collect::<Result<Vec<u64>>>()?;
if margins.len() < 2 {
bail!("Less than 2 numbers in margin content.");
} else {
Ok(margins)
}
}
fn get_memory_margins_kb_impl() -> (u64, u64) {
// TODO(vovoy): Use a regular config file instead of sysfs file.
let margin_path = "/sys/kernel/mm/chromeos-low_mem/margin";
match File::open(Path::new(margin_path)).map(BufReader::new) {
Ok(reader) => match parse_margins(reader) {
Ok(margins) => return (margins[0] * 1024, margins[1] * 1024),
Err(e) => error!("Couldn't parse {}: {}", margin_path, e),
},
Err(e) => error!("Couldn't read {}: {}", margin_path, e),
}
// Critical margin is 5.2% of total memory, moderate margin is 40% of total
// memory. See also /usr/share/cros/init/swap.sh on DUT.
let total_memory_kb = match get_meminfo() {
Ok(meminfo) => meminfo.total,
Err(e) => {
error!("Assume 2 GiB total memory if get_meminfo failed: {}", e);
2 * 1024
}
};
(total_memory_kb * 13 / 250, total_memory_kb * 2 / 5)
}
pub fn get_memory_margins_kb() -> (u64, u64) {
static MARGINS: Lazy<(u64, u64)> = Lazy::new(get_memory_margins_kb_impl);
*MARGINS
}
struct ComponentMarginsKb {
chrome_critical: u64,
chrome_moderate: u64,
arcvm_foreground: u64,
arcvm_perceptible: u64,
arcvm_cached: u64,
}
fn get_component_margins_kb() -> ComponentMarginsKb {
let (critical, moderate) = get_memory_margins_kb();
ComponentMarginsKb {
chrome_critical: critical,
chrome_moderate: moderate,
arcvm_foreground: critical * 3 / 4, // 75 % of critical
arcvm_perceptible: critical * 3 / 2, // 150 % of critical
arcvm_cached: moderate,
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum PressureLevelChrome {
// There is enough memory to use.
None = 0,
// Chrome is advised to free buffers that are cheap to re-allocate and not
// immediately needed.
Moderate = 1,
// Chrome is advised to free all possible memory.
Critical = 2,
}
#[derive(Clone, Copy, PartialEq)]
pub enum PressureLevelArcvm {
// There is enough memory to use.
None = 0,
// ARCVM is advised to kill cached processes to free memory.
Cached = 1,
// ARCVM is advised to kill perceptible processes to free memory.
Perceptible = 2,
// ARCVM is advised to kill foreground processes to free memory.
Foreground = 3,
}
pub struct PressureStatus {
pub chrome_level: PressureLevelChrome,
pub chrome_reclaim_target_kb: u64,
pub arcvm_level: PressureLevelArcvm,
pub arcvm_reclaim_target_kb: u64,
}
pub fn get_memory_pressure_status() -> Result<PressureStatus> {
let available = get_background_available_memory_kb()?;
let margins = get_component_margins_kb();
let (chrome_level, chrome_reclaim_target_kb) = if available < margins.chrome_critical {
(
PressureLevelChrome::Critical,
margins.chrome_critical - available,
)
} else if available < margins.chrome_moderate {
(
PressureLevelChrome::Moderate,
margins.chrome_moderate - available,
)
} else {
(PressureLevelChrome::None, 0)
};
let (arcvm_level, arcvm_reclaim_target_kb) = if available < margins.arcvm_foreground {
(
PressureLevelArcvm::Foreground,
margins.arcvm_foreground - available,
)
} else if available < margins.arcvm_perceptible {
(
PressureLevelArcvm::Perceptible,
margins.arcvm_perceptible - available,
)
} else if available < margins.arcvm_cached {
(PressureLevelArcvm::Cached, margins.arcvm_cached - available)
} else {
(PressureLevelArcvm::None, 0)
};
Ok(PressureStatus {
chrome_level,
chrome_reclaim_target_kb,
arcvm_level,
arcvm_reclaim_target_kb,
})
}