blob: e7987bdfa86525a5c0d4bb74c0b68bb8d31bd0bb [file] [log] [blame]
extern crate anyhow;
extern crate dbus;
extern crate lazy_static;
#[cfg(test)]
mod test;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::time::Duration;
use anyhow::{bail, Context, Result};
use dbus::blocking::LocalConnection;
use dbus::tree::{Factory, MTFn, MethodErr, MethodInfo, MethodResult};
use lazy_static::lazy_static;
// Extract the parsing function for unittest.
fn parse_file_to_u64<R: BufRead>(reader: R) -> Result<u64> {
let first_line = reader.lines().next().context("No content in buffer")??;
first_line
.parse()
.with_context(|| format!("Couldn't parse \"{}\" as u64", first_line))
}
/// Get the first line in a file and parse as u64.
fn read_file_to_u64<P: AsRef<Path>>(filename: P) -> Result<u64> {
let reader = File::open(filename).map(BufReader::new)?;
parse_file_to_u64(reader)
}
/// 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.
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)?
- 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
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.
#[derive(Default)]
struct MemInfo {
total: u64,
free: u64,
active_anon: u64,
inactive_anon: u64,
active_file: u64,
inactive_file: u64,
dirty: u64,
swap_free: u64,
}
/// Parsing /proc/meminfo.
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.
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)
}
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,
))
}
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) => println!("Couldn't parse {}: {}", margin_path, e),
},
Err(e) => println!("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) => {
println!("Assume 2 GiB total memory if get_meminfo failed: {}", e);
2 * 1024
}
};
(total_memory_kb * 13 / 250, total_memory_kb * 2 / 5)
}
fn get_memory_margins_kb() -> (u64, u64) {
lazy_static! {
static ref MARGINS: (u64, u64) = get_memory_margins_kb_impl();
}
*MARGINS
}
struct MemoryParameters {
reserved_free: u64,
min_filelist: u64,
ram_swap_weight: u64,
}
fn get_memory_parameters() -> MemoryParameters {
lazy_static! {
static ref RESERVED_FREE: u64 = match get_reserved_memory_kb() {
Ok(reserved) => reserved,
Err(e) => {
println!("get_reserved_memory_kb failed: {}", e);
0
}
};
static ref MIN_FILELIST: u64 = 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 ref RAM_SWAP_WEIGHT: u64 = read_file_to_u64("/sys/kernel/mm/chromeos-low_mem/ram_vs_swap_weight").unwrap_or(0);
}
MemoryParameters {
reserved_free: *RESERVED_FREE,
min_filelist: *MIN_FILELIST,
ram_swap_weight: *RAM_SWAP_WEIGHT,
}
}
fn dbus_method_get_available_memory_kb(m: &MethodInfo<MTFn<()>, ()>) -> MethodResult {
match get_available_memory_kb() {
// One message will be returned - the method return (and should always be there).
Ok(available) => Ok(vec![m.msg.method_return().append1(available)]),
Err(_) => Err(MethodErr::failed("Couldn't get available memory")),
}
}
fn dbus_method_get_memory_margins_kb(m: &MethodInfo<MTFn<()>, ()>) -> MethodResult {
let margins = get_memory_margins_kb();
Ok(vec![m.msg.method_return().append2(margins.0, margins.1)])
}
fn start_service() -> Result<()> {
let service_name = "org.chromium.MemoryPressure";
let path_name = "/org/chromium/MemoryPressure";
let interface_name = service_name;
// Let's start by starting up a connection to the system bus and request a name.
let c = LocalConnection::new_system()?;
c.request_name(service_name, false, true, false)?;
let f = Factory::new_fn::<()>();
// We create a tree with one object path inside and make that path introspectable.
let tree = f.tree(()).add(
f.object_path(path_name, ()).introspectable().add(
f.interface(interface_name, ())
.add_m(
f.method(
"GetAvailableMemoryKB",
(),
dbus_method_get_available_memory_kb,
)
// Our method has one output argument.
.outarg::<u64, _>("available"),
)
.add_m(
f.method("GetMemoryMarginsKB", (), dbus_method_get_memory_margins_kb)
.outarg::<u64, _>("reply"),
),
),
);
tree.start_receive(&c);
// Serve clients forever.
loop {
c.process(Duration::from_millis(u64::MAX))?;
}
}
fn main() -> Result<()> {
start_service()
}