blob: 2c5be9c3b7e8dbbfe96a3f05c9948a7e648dae34 [file] [log] [blame] [edit]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
mod meminfo;
mod page_size;
mod psi_memory_handler;
mod psi_monitor;
mod psi_policy;
mod vmstat;
use std::collections::BTreeMap;
use std::fmt;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::path::Path;
use std::sync::Mutex;
use std::time::Duration;
use std::time::Instant;
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use log::error;
use log::info;
use once_cell::sync::Lazy;
use system_api::vm_memory_management::ResizePriority;
pub use self::meminfo::MemInfo;
pub use self::psi_memory_handler::PsiMemoryHandler;
pub use self::psi_memory_handler::Result as PsiPolicyResult;
pub use self::psi_memory_handler::MAX_PSI_ERROR_TYPE;
pub use self::psi_memory_handler::UMA_NAME_PSI_POLICY_ERROR;
use crate::common;
use crate::common::read_from_file;
use crate::feature;
use crate::metrics;
use crate::swappiness_config::SwappinessConfig;
use crate::sync::NoPoison;
use crate::vm_memory_management_client::VmMemoryManagementClient;
// 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. BPS are basis points.
const DEFAULT_MODERATE_MARGIN_BPS: u64 = 4000;
const DEFAULT_CRITICAL_MARGIN_BPS: u64 = 520;
const DEFAULT_CRITICAL_PROTECTED_MARGIN_BPS: u64 = DEFAULT_CRITICAL_MARGIN_BPS;
// A quarter of swap free is counted as available memory.
const DEFAULT_RAM_SWAP_WEIGHT: u64 = 4;
// Memory config paths.
const RESOURCED_CONFIG_DIR: &str = "run/resourced";
// /run/resourced/margins_kb file is removed. Use D-Bus method SetMemoryMargins to set the
// margins instead.
// The old margins_kb is confusing that it doesn't update when the D-Bus method is called to update
// the margin.
const RAM_SWAP_WEIGHT_FILENAME: &str = "ram_swap_weight";
// The available memory for background components is discounted by 300 MiB.
const GAME_MODE_OFFSET_KB: u64 = 300 * 1024;
const VMMMS_RECLAIM_ENTIRE_TARGET_FEATURE_NAME: &str =
"CrOSLateBootResourcedVmmmsReclaimEntireTarget";
const DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME: &str =
"CrOSLateBootDiscardStaleAtModeratePressure";
// Devices which have more RAM than this will not discard stale tabs at moderate pressure.
const DISCARD_STALE_AT_MODERATE_HIGH_RAM_CUTOFF_KB: u64 = 8 * 1024 * 1024;
#[cfg(not(test))]
const DISCARD_STALE_AT_MODERATE_PRESSURE_MIN_VISIBLE_SECONDS_THRESHOLD_PARAM: &str =
"MinLastVisibleDurationSeconds";
#[cfg(not(test))]
const DISCARD_STALE_AT_MODERATE_PRESSURE_MAX_VISIBLE_SECONDS_THRESHOLD_PARAM: &str =
"MaxLastVisibleDurationSeconds";
// Proportion of the reclaim target to attempt to reclaim from VMMS.
// Represented as a percentage. For example a value of 50 means that
// 50% of the reclaim target will attempt to be reclaimed from VMMS.
const DISCARD_STALE_AT_MODERATE_PRESSURE_VMMS_RECLAIM_PROPORTION_PARAM: &str =
"VmmmsReclaimProportion";
// By default attempt to reclaim 100% of the reclaim target from VMMS.
const DISCARD_STALE_AT_MODERATE_PRESSURE_VMMS_RECLAIM_PROPORTION_DEFAULT: u64 = 0;
// The minimum allowed interval between stale tab discard attempts.
const DISCARD_STALE_AT_MODERATE_PRESSURE_MIN_DISCARD_INTERVAL: &str = "MinDiscardIntervalSeconds";
const MIN_DISCARD_INTERVAL_DEFAULT: Duration = Duration::from_secs(10);
const PSI_ADJUST_AVAILABLE_FEATURE_NAME: &str = "CrOSLateBootPsiAdjustAvailable";
const PSI_ADJUST_AVAILABLE_TOP_THRESHOLD_PARAM: &str = "PsiTopThreshold";
const PSI_ADJUST_AVAILABLE_TOP_THRESHOLD_DEFAULT: f32 = 60.0;
const PSI_ADJUST_AVAILABLE_USE_FULL_PARAM: &str = "PsiUseFull";
const PSI_ADJUST_AVAILABLE_COOLDOWN_MILLIS_PARAM: &str = "CooldownMillis";
const PSI_ADJUST_AVAILABLE_COOLDOWN_DEFAULT: Duration = Duration::from_secs(10);
const MGLRU_SPLIT_GENERATIONS: &str = "CrOSLateBootSplitGenerations";
const MGLRU_SPLIT_GENERATIONS_SWAPPINESS: &str = "Swappiness";
const MGLRU_CONSERVATIVE_MM: &str = "CrOSLateBootMglruConservativeMm";
fn update_mglru_sysfs(mask: u64, enable: bool) -> Result<()> {
const MGLRU_ENABLE_PATH: &str = "/sys/kernel/mm/lru_gen/enabled";
let enabled_setting: u64 = u64::from_str_radix(
std::str::from_utf8(
&std::fs::read(MGLRU_ENABLE_PATH).context("failed to read MGLRU enable state")?,
)
.context("malformed kernel output")?
.trim_start_matches("0x")
.trim(),
16,
)
.context("non-hex kernel output")?;
let enabled_setting = if enable {
enabled_setting | mask
} else {
enabled_setting & !mask
};
let mut bytes = Vec::new();
write!(&mut bytes, "0x{enabled_setting:x}").expect("Failed to format string");
std::fs::write(MGLRU_ENABLE_PATH, bytes).context("Failed to update MGLRU enable state")?;
Ok(())
}
fn update_mglru_split_settings(
enable_split_gens: bool,
swappiness_config: &SwappinessConfig,
) -> Result<()> {
let swappiness_val = if enable_split_gens {
feature::get_feature_param_as::<u32>(
MGLRU_SPLIT_GENERATIONS,
MGLRU_SPLIT_GENERATIONS_SWAPPINESS,
)
.ok()
.flatten()
} else {
None
};
// Set the linked gen bit to the inverse of the split gen experiment state.
const MGLRU_LINKED_GEN_BIT: u64 = 0x10;
update_mglru_sysfs(MGLRU_LINKED_GEN_BIT, !enable_split_gens)?;
swappiness_config.update_default_swappiness(swappiness_val);
Ok(())
}
pub fn register_features(swappiness: SwappinessConfig) {
feature::register_feature(VMMMS_RECLAIM_ENTIRE_TARGET_FEATURE_NAME, true, None);
feature::register_feature(DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME, true, None);
feature::register_feature(PSI_ADJUST_AVAILABLE_FEATURE_NAME, true, None);
let kernel_version = sys_info::os_release().unwrap_or("".to_string());
if kernel_version.starts_with("5.15") || kernel_version.starts_with("6.6") {
feature::register_feature(
MGLRU_SPLIT_GENERATIONS,
false,
Some(Box::new(move |enabled| {
if let Err(e) = update_mglru_split_settings(enabled, &swappiness) {
error!("Failed to update MGLRU split settings {e:?}");
}
})),
);
feature::register_feature(
MGLRU_CONSERVATIVE_MM,
false,
Some(Box::new(move |enabled| {
// Set the aggressive mm bit to the inverse of the split gen experiment state.
const MGLRU_AGGRESSIVE_MM_BIT: u64 = 0x30;
if let Err(e) = update_mglru_sysfs(MGLRU_AGGRESSIVE_MM_BIT, !enabled) {
error!("Failed to update MGLRU conservative mm {e:?}");
}
})),
)
}
}
pub const PSI_MEMORY_POLICY_FEATURE_NAME: &str = "CrOSLateBootPSIMemoryPolicy";
/// 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_from_file(&"/proc/sys/vm/extra_free_kbytes").unwrap_or(0))
}
/// Adjusts the memory component according to PSI memory some. When PSI memory some is higher,
/// returns lower memory component in KiB.
fn psi_adjust_memory_kb(memory_component_kb: u64) -> u64 {
let psi = match procfs::MemoryPressure::new() {
Ok(psi) => psi,
Err(e) => {
error!("procfs::MemoryPressure::new() failed: {e}");
return memory_component_kb;
}
};
let psi_avg10 = match feature::get_feature_param_as::<bool>(
PSI_ADJUST_AVAILABLE_FEATURE_NAME,
PSI_ADJUST_AVAILABLE_USE_FULL_PARAM,
) {
Ok(Some(true)) => psi.full.avg10,
_ => psi.some.avg10,
};
// If PSI memory some is higher than the top threshold, discount the memory component to 0. If
// PSI memory some is lower than the bottom threshold, do not discount.
const PSI_BOTTOM_THRESHOLD: f32 = 5.0;
let psi_top_threshold = match feature::get_feature_param_as::<f32>(
PSI_ADJUST_AVAILABLE_FEATURE_NAME,
PSI_ADJUST_AVAILABLE_TOP_THRESHOLD_PARAM,
) {
Ok(Some(threshold)) => threshold,
_ => PSI_ADJUST_AVAILABLE_TOP_THRESHOLD_DEFAULT,
};
let psi_multiplier = if psi_avg10 >= psi_top_threshold {
0.0
} else if psi_avg10 < PSI_BOTTOM_THRESHOLD {
1.0
} else {
// When PSI is between the bottom threshold and top threshold, discount a portion of the
// reclaimable memory. This portion should be the percentage that current PSI is between
// the top and bottom thresholds.
(psi_top_threshold - psi_avg10 as f32) / (psi_top_threshold - PSI_BOTTOM_THRESHOLD)
};
let result_f64 = (memory_component_kb as f32) * psi_multiplier;
result_f64.round() as u64
}
/// 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,
do_thrashing_discount: bool,
) -> 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
};
let reclaimable_component = cache_component.saturating_add(swap_component);
let reclaimable_adjusted = if do_thrashing_discount {
// When PSI memory some is high, the cost of reclaim memory is high, discount the
// reclaimable component of available memory. Do not adjust the free component according
// to PSI to avoid making free too high.
psi_adjust_memory_kb(reclaimable_component)
} else {
reclaimable_component
};
free_component.saturating_add(reclaimable_adjusted)
}
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 = read_from_file(&"/proc/sys/vm/min_filelist_kbytes").unwrap_or(0);
static RAM_SWAP_WEIGHT: Lazy<u64> = Lazy::new(|| {
read_from_file(
&Path::new("/")
.join(RESOURCED_CONFIG_DIR)
.join(RAM_SWAP_WEIGHT_FILENAME),
)
.unwrap_or(DEFAULT_RAM_SWAP_WEIGHT)
});
MemoryParameters {
reserved_free: *RESERVED_FREE,
min_filelist,
ram_swap_weight: *RAM_SWAP_WEIGHT,
}
}
fn get_available_memory_kb(meminfo: &MemInfo, do_thrashing_discount: bool) -> u64 {
let p = get_memory_parameters();
calculate_available_memory_kb(
meminfo,
p.reserved_free,
p.min_filelist,
p.ram_swap_weight,
do_thrashing_discount,
)
}
pub fn get_foreground_available_memory_kb(meminfo: &MemInfo) -> u64 {
get_available_memory_kb(meminfo, false)
}
// |game_mode| is passed rather than implicitly queried. This saves us a query
// (hence a lock) in the case where the caller needs the game mode state for a
// separate purpose (see |get_memory_pressure_status|).
pub fn get_background_available_memory_kb(
meminfo: &MemInfo,
game_mode: common::GameMode,
do_thrashing_discount: bool,
) -> u64 {
let available = get_available_memory_kb(meminfo, do_thrashing_discount);
if game_mode != common::GameMode::Off {
available.saturating_sub(GAME_MODE_OFFSET_KB)
} else {
available
}
}
pub struct MemoryMarginsBps {
pub moderate: u64,
pub critical: u64,
pub critical_protected: u64,
}
#[derive(Clone, Copy)]
pub struct MemoryMarginsKb {
pub moderate: u64,
pub critical: u64,
critical_protected: u64,
}
static MEMORY_MARGINS: Lazy<Mutex<MemoryMarginsKb>> =
Lazy::new(|| Mutex::new(get_default_memory_margins_kb_impl()));
// Given the total system memory in KB and the basis points for the margin, calculate the absolute
// values in KBs.
fn total_mem_bps_to_kb(total_mem_kb: u64, margin_bps: u64) -> u64 {
// A basis point is 1/100th of a percent, so we need to convert to whole digit percent and then
// convert into a fraction of 1, so we divide by 100 twice, ie. 4000bps -> 40% -> .4.
let total_mem_kb = total_mem_kb as f64;
let bps = margin_bps as f64;
(total_mem_kb * (bps / 100.0) / 100.0) as u64
}
fn get_memory_margins_kb_from_bps(margins_bps: MemoryMarginsBps) -> MemoryMarginsKb {
let total_memory_kb = match MemInfo::load() {
Ok(meminfo) => meminfo.total,
Err(e) => {
error!("Assume 2 GiB total memory if get_meminfo failed: {e}");
2 * 1024
}
};
MemoryMarginsKb {
moderate: total_mem_bps_to_kb(total_memory_kb, margins_bps.moderate),
critical: total_mem_bps_to_kb(total_memory_kb, margins_bps.critical),
critical_protected: total_mem_bps_to_kb(total_memory_kb, margins_bps.critical_protected),
}
}
fn get_default_memory_margins_kb_impl() -> MemoryMarginsKb {
let margins_bps = MemoryMarginsBps {
moderate: DEFAULT_MODERATE_MARGIN_BPS,
critical: DEFAULT_CRITICAL_MARGIN_BPS,
critical_protected: DEFAULT_CRITICAL_PROTECTED_MARGIN_BPS,
};
get_memory_margins_kb_from_bps(margins_bps)
}
pub fn get_memory_margins_kb() -> MemoryMarginsKb {
let data = MEMORY_MARGINS.do_lock();
*data
}
pub fn set_memory_margins_bps(margins_bps: MemoryMarginsBps) {
let mut data = MEMORY_MARGINS.do_lock();
*data = get_memory_margins_kb_from_bps(margins_bps);
}
pub struct ArcMarginsKb {
pub foreground: u64,
pub perceptible: u64,
pub cached: u64,
}
pub struct ComponentMarginsKb {
pub chrome_moderate: u64,
pub chrome_critical: u64,
pub chrome_critical_protected: u64,
pub arcvm: ArcMarginsKb,
pub arc_container: ArcMarginsKb,
}
impl ComponentMarginsKb {
fn compute_chrome_pressure(&self, available: u64) -> (PressureLevelChrome, u64) {
if available < self.chrome_critical_protected {
(
PressureLevelChrome::Critical,
// Computing the amount to free with the critical margin may discard protected tabs
// between the critical protected and critical margins, but we need to get the
// device back into a better state.
self.chrome_critical - available,
)
} else if available < self.chrome_critical {
(
PressureLevelChrome::DiscardUnprotected,
self.chrome_critical - available,
)
} else if available < self.chrome_moderate {
(
PressureLevelChrome::Moderate,
self.chrome_moderate - available,
)
} else {
(PressureLevelChrome::None, 0)
}
}
}
pub fn get_component_margins_kb() -> ComponentMarginsKb {
let margins_kb = get_memory_margins_kb();
ComponentMarginsKb {
chrome_moderate: margins_kb.moderate,
chrome_critical: margins_kb.critical,
chrome_critical_protected: margins_kb.critical_protected,
arcvm: ArcMarginsKb {
// 75 % of critical.
foreground: margins_kb.critical * 3 / 4,
// 100 % of critical.
perceptible: margins_kb.critical,
cached: 2 * margins_kb.critical,
},
arc_container: ArcMarginsKb {
// Don't kill ARC container foreground process. It might be supported in the future.
foreground: 0,
perceptible: margins_kb.critical,
// Minimal of moderate and 200 % of critical.
cached: std::cmp::min(margins_kb.moderate, 2 * margins_kb.critical),
},
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PressureLevelChrome {
// There is enough memory to use.
None,
// Chrome is advised to free buffers that are cheap to re-allocate and not
// immediately needed.
Moderate,
// Chrome is advised to discard unprotected tabs to free memory.
DiscardUnprotected,
// Chrome is advised to free all possible memory.
Critical,
}
// The pressure level parameter for the D-Bus Chrome memory pressure signal.
enum PressureLevelChromeDbus {
// 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,
}
// The discard type parameter for the D-Bus Chrome memory pressure signal.
enum DiscardTypeDbus {
// Only unprotected pages can be discarded.
Unprotected = 0,
// Both unprotected and protected pages can be discarded.
Protected = 1,
}
impl PressureLevelChrome {
// Returns the pressure level and the discard type for the Chrome memory pressure signal.
pub fn to_dbus_params(&self) -> (u8, u8) {
match self {
PressureLevelChrome::None => (
PressureLevelChromeDbus::None as u8,
DiscardTypeDbus::Unprotected as u8,
),
PressureLevelChrome::Moderate => (
PressureLevelChromeDbus::Moderate as u8,
DiscardTypeDbus::Unprotected as u8,
),
PressureLevelChrome::DiscardUnprotected => (
PressureLevelChromeDbus::Critical as u8,
DiscardTypeDbus::Unprotected as u8,
),
PressureLevelChrome::Critical => (
PressureLevelChromeDbus::Critical as u8,
DiscardTypeDbus::Protected as u8,
),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd)]
pub enum PressureLevelArcContainer {
// There is enough memory to use.
None = 0,
// ARC container is advised to kill cached processes to free memory.
Cached = 1,
// ARC container is advised to kill perceptible processes to free memory.
Perceptible = 2,
// ARC container is advised to kill foreground processes to free memory.
Foreground = 3,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct PressureStatus {
pub chrome_level: PressureLevelChrome,
pub chrome_reclaim_target_kb: u64,
pub arc_container_level: PressureLevelArcContainer,
pub arc_container_reclaim_target_kb: u64,
}
macro_rules! get_arc_level {
($fn:ident, $ret_type:ty) => {
fn $fn(margins: &ArcMarginsKb, available: u64, background_memory: u64) -> ($ret_type, u64) {
// We should kill background tabs before foreground/perceptible ARC apps, so
// background memory is counted as available for those ARC pressure levels.
if available + background_memory < margins.foreground {
(<$ret_type>::Foreground, margins.foreground - available)
} else if available + background_memory < margins.perceptible {
(<$ret_type>::Perceptible, margins.perceptible - available)
} else if available < margins.cached {
(<$ret_type>::Cached, margins.cached - available)
} else {
(<$ret_type>::None, 0)
}
}
};
}
get_arc_level!(get_arc_container_level, PressureLevelArcContainer);
// Should only be called when the Chrome pressure level is moderate.
// If necessary, attempts to reclaim memory from VMMS in concierge at
// the appropriate priority for moderate memory pressure.
async fn try_vmms_reclaim_memory_moderate(
vmms_client: &VmMemoryManagementClient,
reclaim_target: u64,
) -> u64 {
// If the discard stale at moderate feature is enabled,
// attempt to reclaim from vmms at stale cached tab priority.
// Note that this reclaim is attempted regardless of if there
// is actually any stale cached tab memory from Chrome.
// There may still be stale memory to clean up in other
// contexts (i.e. ARCVM).
// In some cases it might be better to only reclaim a
// portion of the target from VMMS to better balance
// memory pressure.
let vmms_reclaim_proportion = match feature::get_feature_param_as::<u64>(
DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME,
DISCARD_STALE_AT_MODERATE_PRESSURE_VMMS_RECLAIM_PROPORTION_PARAM,
) {
// Quick sanity check on the feature param value.
Ok(Some(v)) => {
if v <= 100 {
v
} else {
DISCARD_STALE_AT_MODERATE_PRESSURE_VMMS_RECLAIM_PROPORTION_DEFAULT
}
}
_ => DISCARD_STALE_AT_MODERATE_PRESSURE_VMMS_RECLAIM_PROPORTION_DEFAULT,
};
let adjusted_reclaim_target = (reclaim_target * vmms_reclaim_proportion) / 100;
if adjusted_reclaim_target > 0 {
vmms_client
.try_reclaim_memory(
adjusted_reclaim_target,
ResizePriority::RESIZE_PRIORITY_STALE_CACHED_TAB,
)
.await
} else {
0
}
}
// Should only be called when the Chrome pressure level is critical.
// Attempts to reclaim memory from VMMS in concierge at the appropriate priorities for
// critical memory pressure.
// Returns the total amount of memory reclaimed by VMMS.
async fn try_vmms_reclaim_memory_critical(
vmms_client: &VmMemoryManagementClient,
chrome_level: PressureLevelChrome,
mut reclaim_target: u64,
chrome_background_memory: u64,
game_mode: common::GameMode,
) -> u64 {
let reclaim_entire_target =
feature::is_feature_enabled(VMMMS_RECLAIM_ENTIRE_TARGET_FEATURE_NAME).unwrap_or(false);
// Tell VM Memory Management Service to reclaim memory from guests to
// try to save the background tabs.
let cached_target = if reclaim_entire_target {
reclaim_target
} else {
std::cmp::min(chrome_background_memory, reclaim_target)
};
let cached_actual = if cached_target > 0 {
vmms_client
.try_reclaim_memory(cached_target, ResizePriority::RESIZE_PRIORITY_CACHED_TAB)
.await
} else {
0
};
// If the needed memory is less than what was reclaimed from guests plus what Chrome
// will reclaim from background tabs, then no need for higher priority requests.
reclaim_target = reclaim_target.saturating_sub(cached_actual + chrome_background_memory);
if reclaim_target == 0 {
return cached_actual;
}
// Don't try to reclaim memory at higher than cached priority when in ARC
// game mode, to make sure we don't kill the game or something it depends upon.
if game_mode == common::GameMode::Arc {
return cached_actual;
}
// Don't try to reclaim memory at higher than cached priority when the discard type is
// unprotected tabs only.
if chrome_level < PressureLevelChrome::Critical {
return cached_actual;
}
// Tell VM MMS to reclaim memory at perceptible priority for any protected tabs
// that Chrome will kill after it kills all background tabs.
let protected_memory = get_chrome_memory_kb(ChromeProcessType::Protected, None, reclaim_target);
let perceptible_actual = if protected_memory > 0 {
let perceptible_target = if reclaim_entire_target {
reclaim_target
} else {
std::cmp::min(protected_memory, reclaim_target)
};
vmms_client
.try_reclaim_memory(
perceptible_target,
ResizePriority::RESIZE_PRIORITY_PERCEPTIBLE_TAB,
)
.await
} else {
0
};
// If the reclaimed perceptible guest plus protected tabs still isn't enough,
// we don't have anything else to reclaim. Tell that to VM MMS.
reclaim_target = reclaim_target.saturating_sub(protected_memory + perceptible_actual);
if reclaim_target > 0 {
vmms_client.send_no_kill_candidates().await;
}
cached_actual + perceptible_actual
}
// Attempts to reclaim memory from VMMS in concierge at the correct priority according to
// the current memory pressure level and reclaim targets.
// Returns the total amount of memory reclaimed from VMMMS.
async fn try_vmms_reclaim_memory(
vmms_client: &VmMemoryManagementClient,
chrome_level: PressureLevelChrome,
reclaim_target: u64,
chrome_background_memory: u64,
game_mode: common::GameMode,
discard_stale_at_moderate: bool,
) -> u64 {
let now = Instant::now();
// When the vmms client is not connected, nothing can be reclaimed.
if !vmms_client.is_active() {
return 0;
}
let vmms_reclaim_actual = match chrome_level {
// When there is no memory pressure, never attempt to reclaim from VMMS.
PressureLevelChrome::None => {
return 0;
}
PressureLevelChrome::Moderate => {
// If the discard stale at moderate feature is not enabled,
// do not attempt to reclaim at moderate memory pressure.
if !discard_stale_at_moderate {
return 0;
}
try_vmms_reclaim_memory_moderate(vmms_client, reclaim_target).await
}
PressureLevelChrome::DiscardUnprotected | PressureLevelChrome::Critical => {
try_vmms_reclaim_memory_critical(
vmms_client,
chrome_level,
reclaim_target,
chrome_background_memory,
game_mode,
)
.await
}
};
if let Err(e) = report_vmms_reclaim_memory_duration(chrome_level, now.elapsed()) {
error!("Failed to report try_vmms_reclaim_memory duration {e:?}");
}
vmms_reclaim_actual
}
fn report_vmms_reclaim_memory_duration(
chrome_level: PressureLevelChrome,
duration: Duration,
) -> Result<()> {
const DURATION_BASE: &str = "Platform.Resourced.VmmsReclaimMemoryDuration.";
const MODERATE: &str = "Moderate";
const CRITICAL: &str = "Critical";
metrics::send_to_uma(
&format!(
"{}{}",
DURATION_BASE,
match chrome_level {
// No VMMS reclaim for none level.
PressureLevelChrome::None => {
return Ok(());
}
PressureLevelChrome::Moderate => MODERATE,
PressureLevelChrome::DiscardUnprotected | PressureLevelChrome::Critical => CRITICAL,
}
), // Metric name
duration.as_millis() as i32, // Sample
0, // Min
5000, // Max
50, // Number of buckets
)
}
fn get_min_stale_discard_interval() -> Duration {
let min_discard_interval = get_individual_duration_param(
DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME,
DISCARD_STALE_AT_MODERATE_PRESSURE_MIN_DISCARD_INTERVAL,
);
let Ok(min_discard_interval) = min_discard_interval else {
return MIN_DISCARD_INTERVAL_DEFAULT;
};
min_discard_interval
}
// The time of the last stale tab discard attempt.
static LAST_STALE_TAB_DISCARD: Mutex<Option<Instant>> = Mutex::new(None);
fn try_discard_stale_at_moderate(
margins: &ComponentMarginsKb,
chrome_level: PressureLevelChrome,
chrome_reclaim_target_kb: u64,
) -> (PressureLevelChrome, u64) {
let stale_tab_cutoff =
get_stale_tab_age_cutoff(margins, chrome_level, chrome_reclaim_target_kb);
// Only one stale tab is discarded at a time, so using a threshold of 1 is fine.
// get_chrome_memory_kb will still return the size of the most stale tab (if any).
let stale_background_memory_kb =
get_chrome_memory_kb(ChromeProcessType::Background, Some(stale_tab_cutoff), 1);
let mut last_discard_time = LAST_STALE_TAB_DISCARD.do_lock();
let min_stale_discard_interval = get_min_stale_discard_interval();
// If chrome pressure is Moderate, and there are stale background tabs, send a
// one-off critical memory pressure signal to clear out a stale background tab.
// This signal is rate-limited will only be sent if the min discard interval has
// elapsed since the previous attempt.
if chrome_level == PressureLevelChrome::Moderate
&& stale_background_memory_kb > 0
&& last_discard_time.map_or(Duration::MAX, |d| d.elapsed()) > min_stale_discard_interval
{
info!(
"Discarding single tab at moderate memory pressure.
Stale Tab Cutoff: {}s Stale Background Memory: {}kb",
stale_tab_cutoff.as_secs(),
stale_background_memory_kb
);
*last_discard_time = Some(Instant::now());
(
PressureLevelChrome::DiscardUnprotected,
// Regardless of what the reclaim target is Chrome will kill at least one tab.
// Since this isn't critical memory pressure and memory doesn't need to be
// freed quickly, just send '1' so that a single stale tab will be discarded.
1,
)
} else {
(chrome_level, chrome_reclaim_target_kb)
}
}
static LAST_THRASHING_DISCOUNT_CRITICAL_PRESSURE_AT: Mutex<Option<Instant>> = Mutex::new(None);
fn should_do_thrashing_discount() -> bool {
if !feature::is_feature_enabled(PSI_ADJUST_AVAILABLE_FEATURE_NAME).unwrap_or(false) {
return false;
}
if let Some(last_critical_at) = *LAST_THRASHING_DISCOUNT_CRITICAL_PRESSURE_AT.do_lock() {
let cooldown_duration = match feature::get_feature_param_as::<u64>(
PSI_ADJUST_AVAILABLE_FEATURE_NAME,
PSI_ADJUST_AVAILABLE_COOLDOWN_MILLIS_PARAM,
) {
Ok(Some(millis)) => Duration::from_millis(millis),
_ => PSI_ADJUST_AVAILABLE_COOLDOWN_DEFAULT,
};
// Skip considering adjusting swap memory component based on PSI just after sending critical
// pressure signal to avoid discarding too many tabs because:
// * There can be a delay between the critical signal is sent and the tabs are actually
// discarded.
// * 10 second average of PSI can contain high value which is before discarding.
if Instant::now().duration_since(last_critical_at) <= cooldown_duration {
return false;
}
}
// If there is protected processes only, we need to use PressureLevelChrome::Critical to discard
// protected tabs. However PressureLevelChrome::Critical need to be judged by conservative
// available memory size.
background_tabs_exist()
}
pub async fn get_memory_pressure_status(
vmms_client: &VmMemoryManagementClient,
) -> Result<PressureStatus> {
let game_mode = common::get_game_mode();
let meminfo = MemInfo::load().context("load meminfo")?;
let margins = get_component_margins_kb();
let is_high_ram = meminfo.total > DISCARD_STALE_AT_MODERATE_HIGH_RAM_CUTOFF_KB;
// Never enable moderate pressure discards on devices with high ram.
let discard_stale_at_moderate = !is_high_ram
&& feature::is_feature_enabled(DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME)?;
let do_thrashing_discount = should_do_thrashing_discount();
let available = get_background_available_memory_kb(&meminfo, game_mode, do_thrashing_discount);
let (mut raw_chrome_level, raw_chrome_reclaim_target_kb) =
margins.compute_chrome_pressure(available);
if do_thrashing_discount && raw_chrome_level > PressureLevelChrome::DiscardUnprotected {
// Avoid discarding protected tabs based on non-conservative available memory size.
//
// Even if the actual memory pressure is critical enough without thrashing discount to
// discard protected tabs, PressureLevelChrome::DiscardUnprotected can discard some tabs
// because should_do_thrashing_discount() checks there is at least 1 background tabs.
// The background tabs may not be enough to satisfy reclaiming target. But protected tabs
// will be discarded in the next round of get_memory_pressure_status() which is called by
// margin_memory_handler_loop() every 1 second under high memory pressure anyway.
raw_chrome_level = PressureLevelChrome::DiscardUnprotected;
}
// We cap ARC pressure levels at cached if reclaiming Chrome background memory will
// be sufficient to push us back over the ARC perceptible margin. Compute the
// maximum we need to reclaim to short circult Chrome background memory measurement.
let arcvm_perceptible_target = margins.arcvm.perceptible.saturating_sub(available);
let arc_container_perceptible_target =
margins.arc_container.perceptible.saturating_sub(available);
let max_target = arcvm_perceptible_target
.max(arc_container_perceptible_target)
.max(raw_chrome_reclaim_target_kb);
let background_memory_kb =
get_chrome_memory_kb(ChromeProcessType::Background, None, max_target);
let vmms_reclaim_kb = try_vmms_reclaim_memory(
vmms_client,
raw_chrome_level,
raw_chrome_reclaim_target_kb,
background_memory_kb,
game_mode,
discard_stale_at_moderate,
)
.await;
let (after_vmms_chrome_level, after_vmms_chrome_reclaim_target_kb) =
margins.compute_chrome_pressure(available + vmms_reclaim_kb);
// Ensure that new chrome_level is lower than or equals to the original chrome level. The
// inversion can happen if raw_chrome_level is adjusted due to non-conservative available
// memory.
let after_vmms_chrome_level = std::cmp::min(after_vmms_chrome_level, raw_chrome_level);
let (arc_container_level, arc_container_reclaim_target_kb) =
get_arc_container_level(&margins.arc_container, available, background_memory_kb);
let (final_chrome_level, final_chrome_reclaim_target_kb) = if discard_stale_at_moderate {
try_discard_stale_at_moderate(
&margins,
after_vmms_chrome_level,
after_vmms_chrome_reclaim_target_kb,
)
} else {
(after_vmms_chrome_level, after_vmms_chrome_reclaim_target_kb)
};
if do_thrashing_discount && final_chrome_level > PressureLevelChrome::Moderate {
*LAST_THRASHING_DISCOUNT_CRITICAL_PRESSURE_AT.do_lock() = Some(Instant::now());
}
Ok(PressureStatus {
chrome_level: final_chrome_level,
chrome_reclaim_target_kb: final_chrome_reclaim_target_kb,
arc_container_level,
arc_container_reclaim_target_kb,
})
}
pub fn init_memory_configs() -> Result<()> {
init_memory_configs_impl(Path::new("/"))
}
/// Initialize the margins file if it doesn't exist.
/// These files are used as the default values.
fn init_memory_configs_impl(root: &Path) -> Result<()> {
// Checks the config directory.
let config_path = root.join(RESOURCED_CONFIG_DIR);
if !config_path.exists() {
bail!(
"The config directory {} doesn't exist.",
config_path.display()
);
} else if !config_path.is_dir() {
bail!(
"The config directory {} is not a directory.",
config_path.display()
);
}
// Creates the memory ram_swap_weight config file.
let ram_swap_weight_path = config_path.join(RAM_SWAP_WEIGHT_FILENAME);
if !ram_swap_weight_path.exists() {
let mut ram_swap_weight_file = File::create(ram_swap_weight_path)?;
ram_swap_weight_file.write_all(DEFAULT_RAM_SWAP_WEIGHT.to_string().as_bytes())?;
} else if !ram_swap_weight_path.is_file() {
bail!(
"The ram swap weight path {} is not a regular file.",
ram_swap_weight_path.display()
);
}
Ok(())
}
// The browser type of the process list.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BrowserType {
// Ash Chrome.
Ash = 0,
// Lacros Chrome.
Lacros = 1,
}
impl fmt::Display for BrowserType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BrowserType::Ash => write!(f, "Ash"),
BrowserType::Lacros => write!(f, "Lacros"),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ChromeProcessType {
Background,
Protected,
}
impl fmt::Display for ChromeProcessType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ChromeProcessType::Background => write!(f, "Background"),
ChromeProcessType::Protected => write!(f, "Protected"),
}
}
}
#[derive(Copy, Clone)]
pub struct TabProcess {
pub pid: i32,
pub last_visible: Instant,
}
// Lists of the processes to estimate the host memory usage.
static CHROME_TAB_PROCESSES: Mutex<BTreeMap<(BrowserType, ChromeProcessType), Vec<TabProcess>>> =
Mutex::new(BTreeMap::new());
// Returns the process list for a given browser/process type pair
fn get_chrome_tab_processes(
browser_type: BrowserType,
process_type: ChromeProcessType,
min_last_visible_age: Option<Duration>,
) -> Result<Vec<TabProcess>> {
// Panic on poisoned mutex.
let all_tab_processes = CHROME_TAB_PROCESSES.do_lock();
let Some(filtered_process_list) = all_tab_processes.get(&(browser_type, process_type)) else {
// Returns empty list if process list is not present.
// E.g., When Lacros Chrome is not running.
return Ok(Vec::new());
};
let now = Instant::now();
match min_last_visible_age {
Some(duration) => Ok(filtered_process_list
.iter()
.filter(|tab_process| now - tab_process.last_visible >= duration)
.cloned()
.collect()),
None => Ok(filtered_process_list.clone()),
}
}
fn background_tabs_exist() -> bool {
let all_tab_processes = CHROME_TAB_PROCESSES.do_lock();
all_tab_processes
.iter()
.any(|((_, process_type), processes)| {
if process_type == &ChromeProcessType::Background {
!processes.is_empty()
} else {
false
}
})
}
pub fn set_browser_tab_processes(
browser_type: BrowserType,
background_tab_processes: Vec<TabProcess>,
protected_tab_processes: Vec<TabProcess>,
) {
let mut chrome_tab_processes = CHROME_TAB_PROCESSES.do_lock();
chrome_tab_processes.insert(
(browser_type, ChromeProcessType::Background),
background_tab_processes,
);
chrome_tab_processes.insert(
(browser_type, ChromeProcessType::Protected),
protected_tab_processes,
);
}
// Returns the amount of memory in KiB of the given chrome process type where the
// last visible time of the tab is older than |min_last_visible_age|. To reduce
// the work when there are a lot of chrome processes, it would stop counting if the
// memory exceeds |threshold_kb|. When |threshold_kb| is 0, it returns 0.
fn get_chrome_memory_kb(
process_type: ChromeProcessType,
min_last_visible_age: Option<Duration>,
threshold_kb: u64,
) -> u64 {
if threshold_kb == 0 {
return 0;
}
let mut total_background_memory_kb = 0;
for browser_type in [BrowserType::Ash, BrowserType::Lacros] {
let tab_processes =
match get_chrome_tab_processes(browser_type, process_type, min_last_visible_age) {
Ok(tab_processes) => tab_processes,
Err(e) => {
error!("Failed to get chrome {browser_type} {process_type} tabs: {e}");
continue;
}
};
for tab_process in tab_processes {
match get_chrome_process_memory_usage(tab_process.pid) {
Ok(result) => {
total_background_memory_kb += result;
if total_background_memory_kb > threshold_kb {
return total_background_memory_kb;
}
}
Err(e) => {
// It's Ok to continue when failed to get memory usage for a pid.
// When get_chrome_process_memory_usage() failed, total_background_memory_kb
// would be less and the pressure level would tend to be unmodified.
error!(
"Failed to get memory usage, pid: {}, error: {}",
tab_process.pid, e
);
}
}
}
}
total_background_memory_kb
}
// Memory usage estimation of |pid|, it's the sum of anonymous RSS and swap.
// Returns error if the program name (Name in /proc/PID/status) is not chrome. The program name
// should be chrome for both Ash and Lacros Chrome.
fn get_chrome_process_memory_usage(pid: i32) -> Result<u64> {
let process = match procfs::process::Process::new(pid) {
Ok(p) => p,
Err(_) => {
// Returns 0 if the /proc/PID doesn't exist.
return Ok(0);
}
};
let status = process.status()?;
if status.name.ne("chrome") {
bail!("The program name {} is not chrome", status.name);
}
let rssanon = status
.rssanon
.with_context(|| format!("Couldn't get the RssAnon field in /proc/{pid}/status"))?;
let vmswap = status
.vmswap
.with_context(|| format!("Couldn't get the VmSwap field in /proc/{pid}/status"))?;
Ok(rssanon + vmswap)
}
fn get_stale_tab_age_cutoff(
margins: &ComponentMarginsKb,
chrome_level: PressureLevelChrome,
reclaim_target_kb: u64,
) -> Duration {
// If there is no memory pressure, nothing is treated as stale.
// Return the max cutoff value to exclude everything.
if chrome_level == PressureLevelChrome::None {
return Duration::MAX;
}
// For critical memory pressure, do not exclude anything, so return the
// zero duration.
if chrome_level == PressureLevelChrome::Critical {
return Duration::ZERO;
}
let (min_last_visible, max_last_visible) = get_last_visible_threshold_values();
// Find the actual cutoff as a linear scale between the min and max values above
// in relation to the moderate memory pressure range.
// Example:
// RAM Usage
// 0GB........................................4GB
// |------None------||---Moderate---||-Critical-|
// ^
// Current Usage
// In this case, used RAM is 80% of the way through the moderate memory
// pressure range.
// Return the value that is 80% of the way between min_last_visible and max_last_visible.
let percent_into_moderate =
((reclaim_target_kb * 100) / (margins.chrome_moderate - margins.chrome_critical)) as u32;
max_last_visible - ((max_last_visible - min_last_visible) * percent_into_moderate) / 100
}
#[cfg(not(test))]
fn get_last_visible_threshold_values() -> (Duration, Duration) {
// By default, use the same threshold values as Chrome memory saver which is
// 2 hours regardless of memory pressure level.
const LAST_VISIBLE_THRESHOLD_DEFAULT: Duration = Duration::from_secs(2 * 60 * 60);
let min_last_visible = get_individual_duration_param(
DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME,
DISCARD_STALE_AT_MODERATE_PRESSURE_MIN_VISIBLE_SECONDS_THRESHOLD_PARAM,
);
let max_last_visible = get_individual_duration_param(
DISCARD_STALE_AT_MODERATE_PRESSURE_FEATURE_NAME,
DISCARD_STALE_AT_MODERATE_PRESSURE_MAX_VISIBLE_SECONDS_THRESHOLD_PARAM,
);
let (Ok(min_last_visible), Ok(max_last_visible)) = (min_last_visible, max_last_visible) else {
return (
LAST_VISIBLE_THRESHOLD_DEFAULT,
LAST_VISIBLE_THRESHOLD_DEFAULT,
);
};
// Sanity check for the cutoffs since the threshold calculation requires
// min to be less than max.
// Min and max being the same is fine since we may want to test a constant cutoff.
if min_last_visible > max_last_visible {
return (
LAST_VISIBLE_THRESHOLD_DEFAULT,
LAST_VISIBLE_THRESHOLD_DEFAULT,
);
}
(min_last_visible, max_last_visible)
}
fn get_individual_duration_param(feature_name: &str, param_name: &str) -> Result<Duration> {
let threshold_seconds = feature::get_feature_param_as::<u64>(feature_name, param_name)?
.context("No valid feature param")?;
Ok(Duration::from_secs(threshold_seconds))
}
// For tests, have non-default min/max values.
#[cfg(test)]
fn get_last_visible_threshold_values() -> (Duration, Duration) {
(
Duration::from_secs(5 * 60),
Duration::from_secs(2 * 60 * 60),
)
}
#[cfg(test)]
mod tests {
use std::fs::OpenOptions;
use tempfile::tempdir;
use super::*;
#[test]
fn test_calculate_reserved_free_kb() {
let mock_partial_zoneinfo = r#"
Node 0, zone DMA
pages free 3968
min 137
low 171
high 205
spanned 4095
present 3999
managed 3976
protection: (0, 1832, 3000, 3786)
Node 0, zone DMA32
pages free 422432
min 16270
low 20337
high 24404
spanned 1044480
present 485541
managed 469149
protection: (0, 0, 1953, 1500)
Node 0, zone Normal
pages free 21708
min 17383
low 21728
high 26073
spanned 524288
present 524288
managed 501235
protection: (0, 0, 0, 0)"#;
let page_size_kb = 4;
let high_watermarks = 205 + 24404 + 26073;
let lowmem_reserves = 3786 + 1953;
let reserved = calculate_reserved_free_kb(mock_partial_zoneinfo.as_bytes()).unwrap();
assert_eq!(reserved, (high_watermarks + lowmem_reserves) * page_size_kb);
}
#[test]
fn test_calculate_available_memory_kb() {
let mut info = MemInfo::default();
let min_filelist = 400 * 1024;
let reserved_free = 0;
let ram_swap_weight = 4;
// Available determined by file cache.
info.active_file = 500 * 1024;
info.inactive_file = 500 * 1024;
info.dirty = 10 * 1024;
let file = info.active_file + info.inactive_file;
let available = calculate_available_memory_kb(
&info,
reserved_free,
min_filelist,
ram_swap_weight,
false,
);
assert_eq!(available, file - min_filelist - info.dirty);
// Available determined by swap free.
info.swap_free = 1200 * 1024;
info.active_anon = 1000 * 1024;
info.inactive_anon = 1000 * 1024;
info.active_file = 0;
info.inactive_file = 0;
info.dirty = 0;
let available = calculate_available_memory_kb(
&info,
reserved_free,
min_filelist,
ram_swap_weight,
false,
);
assert_eq!(available, info.swap_free / ram_swap_weight);
// Available determined by anonymous.
info.swap_free = 6000 * 1024;
info.active_anon = 500 * 1024;
info.inactive_anon = 500 * 1024;
let anon = info.active_anon + info.inactive_anon;
let available = calculate_available_memory_kb(
&info,
reserved_free,
min_filelist,
ram_swap_weight,
false,
);
assert_eq!(available, anon / ram_swap_weight);
// When ram_swap_weight is 0, swap is ignored in available.
info.swap_free = 1200 * 1024;
info.active_anon = 1000 * 1024;
info.inactive_anon = 1000 * 1024;
info.active_file = 500 * 1024;
info.inactive_file = 500 * 1024;
let file = info.active_file + info.inactive_file;
let ram_swap_weight = 0;
let available = calculate_available_memory_kb(
&info,
reserved_free,
min_filelist,
ram_swap_weight,
false,
);
assert_eq!(available, file - min_filelist);
}
#[test]
fn test_total_mem_bps_to_kb() {
let margin1 = total_mem_bps_to_kb(100000 /* 100mb */, 1200 /* 12% */);
assert_eq!(margin1, 12000 /* 12mb */);
let margin2 = total_mem_bps_to_kb(100000 /* 100mb */, 3600 /* 36% */);
assert_eq!(margin2, 36000 /* 36mb */);
let margin3 = total_mem_bps_to_kb(1000000 /* 1000mb */, 1250 /* 12.50% */);
assert_eq!(margin3, 125000 /* 125mb */);
let margin4 = total_mem_bps_to_kb(1000000 /* 1000mb */, 7340 /* 73.4% */);
assert_eq!(margin4, 734000 /* 734mb */);
}
#[test]
fn test_init_memory_configs_not_dir() {
let root = tempdir().unwrap();
std::fs::create_dir(root.path().join("run")).unwrap();
// Touches /run/resourced.
OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(root.path().join(RESOURCED_CONFIG_DIR))
.unwrap();
// Returns error when /run/resourced is not a directory.
assert!(init_memory_configs_impl(root.path()).is_err());
}
#[test]
fn test_init_memory_configs_create_margins() {
let root = tempdir().unwrap();
std::fs::create_dir_all(root.path().join(RESOURCED_CONFIG_DIR)).unwrap();
// Checks that config dir already exists. Creates the margins file.
assert!(init_memory_configs_impl(root.path()).is_ok());
}
#[test]
fn test_init_memory_configs_ram_swap_weight_exists() {
let root = tempdir().unwrap();
std::fs::create_dir_all(root.path().join(RESOURCED_CONFIG_DIR)).unwrap();
let mut ram_swap_weight_file = File::create(
root.path()
.join(RESOURCED_CONFIG_DIR)
.join(RAM_SWAP_WEIGHT_FILENAME),
)
.unwrap();
ram_swap_weight_file.write_all("2".as_bytes()).unwrap();
assert!(init_memory_configs_impl(root.path()).is_ok());
}
#[test]
fn test_init_memory_configs_ram_swap_weight_is_dir() {
let root = tempdir().unwrap();
std::fs::create_dir_all(
root.path()
.join(RESOURCED_CONFIG_DIR)
.join(RAM_SWAP_WEIGHT_FILENAME),
)
.unwrap();
assert!(init_memory_configs_impl(root.path()).is_err());
}
#[test]
fn test_get_background_available_memory_kb() {
let p = get_memory_parameters();
let meminfo = MemInfo {
free: 400 * 1024 + p.reserved_free,
..Default::default()
};
assert_eq!(
get_background_available_memory_kb(&meminfo, common::GameMode::Off, false),
400 * 1024
);
assert_eq!(
get_background_available_memory_kb(&meminfo, common::GameMode::Arc, false),
400 * 1024 - GAME_MODE_OFFSET_KB
);
assert_eq!(
get_background_available_memory_kb(&meminfo, common::GameMode::Borealis, false),
400 * 1024 - GAME_MODE_OFFSET_KB
);
// When available memory is less than GAME_MODE_OFFSET_KB.
let meminfo = MemInfo {
free: 200 * 1024 + p.reserved_free,
..Default::default()
};
assert_eq!(
get_background_available_memory_kb(&meminfo, common::GameMode::Off, false),
200 * 1024
);
assert_eq!(
get_background_available_memory_kb(&meminfo, common::GameMode::Arc, false),
0
);
}
#[test]
fn test_get_stale_tab_age_cutoff() {
let margins = ComponentMarginsKb {
chrome_moderate: 5000,
chrome_critical: 1000,
chrome_critical_protected: 1000,
arc_container: ArcMarginsKb {
foreground: 0,
perceptible: 0,
cached: 0,
},
arcvm: ArcMarginsKb {
foreground: 0,
perceptible: 0,
cached: 0,
},
};
// With no memory pressure, the max duration should be returned.
let stale_tab_cutoff = get_stale_tab_age_cutoff(&margins, PressureLevelChrome::None, 0);
assert!(stale_tab_cutoff == Duration::MAX);
// At the very beginning of moderate memory pressure, the max stale target should be
// returned.
let stale_tab_cutoff = get_stale_tab_age_cutoff(&margins, PressureLevelChrome::Moderate, 0);
assert!(stale_tab_cutoff == Duration::from_secs(7200));
// At moderate memory pressure with a reclaim target of 1000, the pressure level is 25% of
// the way into moderate. Therefore, the stale tab cutoff should be 25% of the way between
// 7200 seconds an 300 seconds.
let stale_tab_cutoff =
get_stale_tab_age_cutoff(&margins, PressureLevelChrome::Moderate, 1000);
assert!(stale_tab_cutoff == Duration::from_secs(5475));
// Halfway through moderate memory pressure should be halfway between 7200 and 300.
let stale_tab_cutoff =
get_stale_tab_age_cutoff(&margins, PressureLevelChrome::Moderate, 2000);
assert!(stale_tab_cutoff == Duration::from_secs(3750));
// 75% through moderate memory pressure should be 75% between 7200 and 300.
let stale_tab_cutoff =
get_stale_tab_age_cutoff(&margins, PressureLevelChrome::Moderate, 3000);
assert!(stale_tab_cutoff == Duration::from_secs(2025));
// All the way through should be the minimum stale value.
let stale_tab_cutoff =
get_stale_tab_age_cutoff(&margins, PressureLevelChrome::Moderate, 4000);
assert!(stale_tab_cutoff == Duration::from_secs(300));
// With critical memory pressure, the min duration should be returned.
let stale_tab_cutoff = get_stale_tab_age_cutoff(&margins, PressureLevelChrome::Critical, 0);
assert!(stale_tab_cutoff == Duration::ZERO);
}
}