Support writeback_limit of zram
mmd applies writeback limit to each writeback. The limit is calculated
from zram statistics and system properties.
The logic mainly comes from swap_management of ChromeOS.
Bug: 375432468
Test: atest libmmd_unit_tests
Change-Id: I40f1c4b1c408a89cce975b493310f762f159d32a
diff --git a/Android.bp b/Android.bp
index 047bd46..efe34e0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -25,6 +25,7 @@
"src/lib.rs",
],
rustlibs: [
+ "liblibc",
"libmockall",
"libthiserror",
],
diff --git a/src/main.rs b/src/main.rs
index 187023e..ebe7f92 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,8 +21,12 @@
use std::time::Duration;
use binder::BinderFeatures;
+use log::error;
use log::info;
use log::LevelFilter;
+use mmd::os::OpsImpl;
+use mmd::zram::stats::load_total_zram_size;
+use mmd::zram::writeback::ZramWriteback;
use mmd_aidl_interface::aidl::android::os::IMmd::BnMmd;
fn main() {
@@ -36,13 +40,27 @@
}
}
- let mmd_service = service::MmdService::new();
+ // TODO: This is to wait until swapon_all command sets up zram. Remove this when mmd sets up
+ // zram.
+ std::thread::sleep(Duration::from_secs(60));
+
+ let total_zram_size = match load_total_zram_size::<OpsImpl>() {
+ Ok(v) => v,
+ Err(e) => {
+ error!("failed to load total zram size: {e:?}");
+ std::process::exit(1);
+ }
+ };
+ // TODO: load zram writeback size.
+ let zram_writeback = ZramWriteback::new(total_zram_size, total_zram_size);
+
+ let mmd_service = service::MmdService::new(zram_writeback);
let mmd_service_binder = BnMmd::new_binder(mmd_service, BinderFeatures::default());
binder::add_service("mmd", mmd_service_binder.as_binder()).expect("register service");
info!("mmd started");
- binder::ProcessState::join_thread_pool()
+ binder::ProcessState::join_thread_pool();
}
#[cfg(test)]
diff --git a/src/os.rs b/src/os.rs
index 6c132a9..59b1d1a 100644
--- a/src/os.rs
+++ b/src/os.rs
@@ -48,3 +48,9 @@
std::fs::write(path, contents)
}
}
+
+/// Returns the page size of the system.
+pub fn get_page_size() -> u64 {
+ // SAFETY: `sysconf` simply returns an integer.
+ unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 }
+}
diff --git a/src/service.rs b/src/service.rs
index 2634fb0..c5e3043 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -15,6 +15,7 @@
use std::sync::Mutex;
use std::time::Instant;
+use anyhow::Context;
use binder::Interface;
use binder::Result as BinderResult;
use log::error;
@@ -29,8 +30,8 @@
}
impl MmdService {
- pub fn new() -> Self {
- Self { zram_writeback: Mutex::new(ZramWriteback::new()) }
+ pub fn new(zram_writeback: ZramWriteback) -> Self {
+ Self { zram_writeback: Mutex::new(zram_writeback) }
}
}
@@ -40,8 +41,15 @@
fn doZramMaintenance(&self) -> BinderResult<()> {
let mut zram_writeback = self.zram_writeback.lock().expect("mmd aborts on panics");
let params = load_zram_writeback_params();
- match zram_writeback.mark_and_flush_pages::<OpsImpl>(¶ms, Instant::now()) {
- Ok(_) | Err(ZramWritebackError::BackoffTime) => {}
+ let stats = match load_zram_writeback_stats() {
+ Ok(v) => v,
+ Err(e) => {
+ error!("failed to load zram writeback stats: {e:?}");
+ return Ok(());
+ }
+ };
+ match zram_writeback.mark_and_flush_pages::<OpsImpl>(¶ms, &stats, Instant::now()) {
+ Ok(_) | Err(ZramWritebackError::BackoffTime) | Err(ZramWritebackError::Limit) => {}
Err(e) => error!("failed to zram writeback: {e:?}"),
}
Ok(())
@@ -52,3 +60,12 @@
// TODO: load params from system properties.
mmd::zram::writeback::Params::default()
}
+
+fn load_zram_writeback_stats() -> anyhow::Result<mmd::zram::writeback::Stats> {
+ let mm_stat = mmd::zram::stats::ZramMmStat::load::<OpsImpl>().context("load mm_stat")?;
+ let bd_stat = mmd::zram::stats::ZramBdStat::load::<OpsImpl>().context("load bd_stat")?;
+ Ok(mmd::zram::writeback::Stats {
+ orig_data_size: mm_stat.orig_data_size,
+ current_writeback_pages: bd_stat.bd_count_pages,
+ })
+}
diff --git a/src/zram.rs b/src/zram.rs
index 0685f9e..19cab47 100644
--- a/src/zram.rs
+++ b/src/zram.rs
@@ -15,4 +15,5 @@
//! This module provides policies to manage zram features.
mod idle;
+pub mod stats;
pub mod writeback;
diff --git a/src/zram/stats.rs b/src/zram/stats.rs
new file mode 100644
index 0000000..b267654
--- /dev/null
+++ b/src/zram/stats.rs
@@ -0,0 +1,131 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! This module provides functions to load zram stats.
+
+#[cfg(test)]
+mod tests;
+
+use crate::os::Ops;
+use std::ops::Deref;
+
+const ZRAM_DISKSIZE_PATH: &str = "/sys/block/zram0/disksize";
+const ZRAM_MM_STAT_PATH: &str = "/sys/block/zram0/mm_stat";
+const ZRAM_BD_STAT_PATH: &str = "/sys/block/zram0/bd_stat";
+
+/// Error from loading zram stats.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// Stat file format is invalid.
+ #[error("failed to parse")]
+ Parse,
+ /// Failed to read stat file.
+ #[error("failed to read: {0}")]
+ Io(#[from] std::io::Error),
+}
+
+type Result<T> = std::result::Result<T, Error>;
+
+fn parse_next<T: std::str::FromStr>(
+ iter: &mut impl Iterator<Item = impl Deref<Target = str>>,
+) -> Result<T> {
+ iter.next().ok_or(Error::Parse)?.parse().map_err(|_| Error::Parse)
+}
+
+fn parse_next_optional<T: std::str::FromStr>(
+ iter: &mut impl Iterator<Item = impl Deref<Target = str>>,
+) -> Result<Option<T>> {
+ iter.next().map(|v| v.parse()).transpose().map_err(|_| Error::Parse)
+}
+
+/// Loads /sys/block/zram0/disksize
+pub fn load_total_zram_size<O: Ops>() -> Result<u64> {
+ let contents = O::read_to_string(ZRAM_DISKSIZE_PATH)?;
+ contents.trim().parse().map_err(|_| Error::Parse)
+}
+
+/// Stats from /sys/block/zram0/mm_stat
+#[derive(Debug, Default, PartialEq, Eq)]
+pub struct ZramMmStat {
+ /// Uncompressed size of data stored in this disk. This excludes same-element-filled pages
+ /// (same_pages) since no memory is allocated for them. Unit: bytes
+ pub orig_data_size: u64,
+ /// Compressed size of data stored in this disk.
+ pub compr_data_size: u64,
+ /// The amount of memory allocated for this disk. This includes allocator fragmentation and
+ /// metadata overhead, allocated for this disk. So, allocator space efficiency can be calculated
+ /// using compr_data_size and this statistic. Unit: bytes
+ pub mem_used_total: u64,
+ /// The maximum amount of memory ZRAM can use to store The compressed data.
+ pub mem_limit: u32,
+ /// The maximum amount of memory zram have consumed to store the data.
+ ///
+ /// In zram_drv.h we define max_used_pages as atomic_long_t which could be negative, but
+ /// negative value does not make sense for the variable.
+ pub mem_used_max: i64,
+ /// The number of same element filled pages written to this disk. No memory is allocated for
+ /// such pages.
+ pub same_pages: u64,
+ /// The number of pages freed during compaction.
+ pub pages_compacted: u32,
+ /// The number of incompressible pages.
+ /// Start supporting from v4.19.
+ pub huge_pages: Option<u64>,
+ /// The number of huge pages since zram set up.
+ /// Start supporting from v5.15.
+ pub huge_pages_since: Option<u64>,
+}
+
+impl ZramMmStat {
+ /// Parse /sys/block/zram0/mm_stat.
+ pub fn load<O: Ops>() -> Result<Self> {
+ let contents = O::read_to_string(ZRAM_MM_STAT_PATH)?;
+ let mut values = contents.split_whitespace();
+ Ok(ZramMmStat {
+ orig_data_size: parse_next(&mut values)?,
+ compr_data_size: parse_next(&mut values)?,
+ mem_used_total: parse_next(&mut values)?,
+ mem_limit: parse_next(&mut values)?,
+ mem_used_max: parse_next(&mut values)?,
+ same_pages: parse_next(&mut values)?,
+ pages_compacted: parse_next(&mut values)?,
+ huge_pages: parse_next_optional(&mut values)?,
+ huge_pages_since: parse_next_optional(&mut values)?,
+ })
+ }
+}
+
+/// Stats from /sys/block/zram0/bd_stat
+#[derive(Debug, Default, PartialEq, Eq)]
+pub struct ZramBdStat {
+ /// Size of data written in backing device. Unit: page
+ pub bd_count_pages: u64,
+ /// The number of reads from backing device. Unit: page
+ pub bd_reads_pages: u64,
+ /// The number of writes to backing device. Unit: page
+ pub bd_writes_pages: u64,
+}
+
+impl ZramBdStat {
+ /// Parse /sys/block/zram0/bd_stat.
+ pub fn load<O: Ops>() -> Result<Self> {
+ let contents = O::read_to_string(ZRAM_BD_STAT_PATH)?;
+ let mut values = contents.split_whitespace();
+ Ok(ZramBdStat {
+ bd_count_pages: parse_next(&mut values)?,
+ bd_reads_pages: parse_next(&mut values)?,
+ bd_writes_pages: parse_next(&mut values)?,
+ })
+ }
+}
diff --git a/src/zram/stats/tests.rs b/src/zram/stats/tests.rs
new file mode 100644
index 0000000..9dd7602
--- /dev/null
+++ b/src/zram/stats/tests.rs
@@ -0,0 +1,217 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::*;
+
+use crate::os::MockOps;
+use crate::test_helper::path_params;
+use crate::test_helper::OPS_MTX;
+
+#[test]
+fn test_load_total_zram_size() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = "12345\n";
+ mock.expect().withf(path_params(ZRAM_DISKSIZE_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert_eq!(load_total_zram_size::<MockOps>().unwrap(), 12345);
+}
+
+#[test]
+fn test_load_total_zram_size_invalid_value() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = "a";
+ mock.expect().withf(path_params(ZRAM_DISKSIZE_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert!(load_total_zram_size::<MockOps>().is_err());
+}
+
+#[test]
+fn test_load_total_zram_size_fail_read() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ mock.expect()
+ .withf(path_params(ZRAM_DISKSIZE_PATH))
+ .returning(|_| Err(std::io::Error::other("error")));
+
+ assert!(load_total_zram_size::<MockOps>().is_err());
+}
+
+#[test]
+fn test_zram_mm_stat() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = " 1 2 3 4 5 6 7 8 9";
+ mock.expect().withf(path_params(ZRAM_MM_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert_eq!(
+ ZramMmStat::load::<MockOps>().unwrap(),
+ ZramMmStat {
+ orig_data_size: 1,
+ compr_data_size: 2,
+ mem_used_total: 3,
+ mem_limit: 4,
+ mem_used_max: 5,
+ same_pages: 6,
+ pages_compacted: 7,
+ huge_pages: Some(8),
+ huge_pages_since: Some(9),
+ }
+ );
+}
+
+#[test]
+fn test_zram_mm_stat_skip_huge_pages_since() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = " 1 2 3 4 5 6 7 8";
+ mock.expect().withf(path_params(ZRAM_MM_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert_eq!(
+ ZramMmStat::load::<MockOps>().unwrap(),
+ ZramMmStat {
+ orig_data_size: 1,
+ compr_data_size: 2,
+ mem_used_total: 3,
+ mem_limit: 4,
+ mem_used_max: 5,
+ same_pages: 6,
+ pages_compacted: 7,
+ huge_pages: Some(8),
+ huge_pages_since: None,
+ }
+ );
+}
+
+#[test]
+fn test_zram_mm_stat_skip_huge_pages() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = " 1 2 3 4 5 6 7";
+ mock.expect().withf(path_params(ZRAM_MM_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert_eq!(
+ ZramMmStat::load::<MockOps>().unwrap(),
+ ZramMmStat {
+ orig_data_size: 1,
+ compr_data_size: 2,
+ mem_used_total: 3,
+ mem_limit: 4,
+ mem_used_max: 5,
+ same_pages: 6,
+ pages_compacted: 7,
+ huge_pages: None,
+ huge_pages_since: None,
+ }
+ );
+}
+
+#[test]
+fn test_zram_mm_stat_negative_mem_used_max() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = " 1 2 3 4 -5 6 7 8 9";
+ mock.expect().withf(path_params(ZRAM_MM_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert_eq!(
+ ZramMmStat::load::<MockOps>().unwrap(),
+ ZramMmStat {
+ orig_data_size: 1,
+ compr_data_size: 2,
+ mem_used_total: 3,
+ mem_limit: 4,
+ mem_used_max: -5,
+ same_pages: 6,
+ pages_compacted: 7,
+ huge_pages: Some(8),
+ huge_pages_since: Some(9),
+ }
+ );
+}
+
+#[test]
+fn test_zram_mm_stat_fail_read() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ mock.expect()
+ .withf(path_params(ZRAM_MM_STAT_PATH))
+ .returning(|_| Err(std::io::Error::other("error")));
+
+ assert!(ZramMmStat::load::<MockOps>().is_err());
+}
+
+#[test]
+fn test_zram_mm_stat_less_values() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = " 1 2 3 4 5 6";
+ mock.expect().withf(path_params(ZRAM_MM_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert!(ZramMmStat::load::<MockOps>().is_err());
+}
+
+#[test]
+fn test_zram_mm_stat_invalid_value() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = " 1 2 3 4 5 6 a";
+ mock.expect().withf(path_params(ZRAM_MM_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert!(ZramMmStat::load::<MockOps>().is_err());
+}
+
+#[test]
+fn test_zram_bd_stat() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = "1 2 3";
+ mock.expect().withf(path_params(ZRAM_BD_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert_eq!(
+ ZramBdStat::load::<MockOps>().unwrap(),
+ ZramBdStat { bd_count_pages: 1, bd_reads_pages: 2, bd_writes_pages: 3 }
+ );
+}
+
+#[test]
+fn test_zram_bd_stat_fail_read() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ mock.expect()
+ .withf(path_params(ZRAM_BD_STAT_PATH))
+ .returning(|_| Err(std::io::Error::other("error")));
+
+ assert!(ZramBdStat::load::<MockOps>().is_err());
+}
+
+#[test]
+fn test_zram_bd_stat_less_values() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = "1 2";
+ mock.expect().withf(path_params(ZRAM_BD_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert!(ZramBdStat::load::<MockOps>().is_err());
+}
+
+#[test]
+fn test_zram_bd_stat_invalid_value() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let contents = "1 2 a";
+ mock.expect().withf(path_params(ZRAM_BD_STAT_PATH)).returning(|_| Ok(contents.to_string()));
+
+ assert!(ZramBdStat::load::<MockOps>().is_err());
+}
diff --git a/src/zram/writeback.rs b/src/zram/writeback.rs
index 80a7474..a51f2d3 100644
--- a/src/zram/writeback.rs
+++ b/src/zram/writeback.rs
@@ -24,11 +24,13 @@
use std::time::Duration;
use std::time::Instant;
+use crate::os::get_page_size;
use crate::os::Ops;
use crate::zram::idle::calculate_idle_time;
use crate::zram::idle::set_zram_idle_time;
const ZRAM_WRITEBACK_PATH: &str = "/sys/block/zram0/writeback";
+const ZRAM_WRITEBACK_LIMIT_PATH: &str = "/sys/block/zram0/writeback_limit";
/// Error from [ZramWriteback].
#[derive(Debug, thiserror::Error)]
@@ -36,6 +38,9 @@
/// writeback too frequently
#[error("writeback too frequently")]
BackoffTime,
+ /// no more space for zram writeback
+ #[error("no pages in zram for zram writeback")]
+ Limit,
/// failure on setting zram idle
#[error("calculate zram idle {0}")]
CalculateIdle(#[from] crate::zram::idle::CalculateError),
@@ -45,6 +50,9 @@
/// failure on writing to /sys/block/zram0/writeback
#[error("writeback: {0}")]
Writeback(std::io::Error),
+ /// failure on writing to /sys/block/zram0/writeback_limit
+ #[error("writeback_limit: {0}")]
+ WritebackLimit(std::io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -63,6 +71,10 @@
pub idle: bool,
/// Whether writeback huge pages or not.
pub huge: bool,
+ /// Minimum bytes to writeback in 1 round.
+ pub min_bytes: u64,
+ /// Maximum bytes to writeback in 1 round.
+ pub max_bytes: u64,
}
impl Default for Params {
@@ -77,10 +89,23 @@
huge_idle: true,
idle: true,
huge: true,
+ // 5 MiB
+ min_bytes: 5 << 20,
+ // 300 MiB
+ max_bytes: 300 << 20,
}
}
}
+/// The stats for zram writeback.
+#[derive(Debug, Default)]
+pub struct Stats {
+ /// orig_data_size of [crate::zram::stats::ZramMmStat].
+ pub orig_data_size: u64,
+ /// bd_count_pages of [crate::zram::stats::ZramBdStat].
+ pub current_writeback_pages: u64,
+}
+
enum Mode {
HugeIdle,
Idle,
@@ -90,22 +115,50 @@
/// ZramWriteback manages zram writeback policies.
pub struct ZramWriteback {
last_writeback_at: Option<Instant>,
+ total_zram_pages: u64,
+ zram_writeback_pages: u64,
+ page_size: u64,
}
impl ZramWriteback {
/// Creates a new [ZramWriteback].
- pub fn new() -> Self {
- Self { last_writeback_at: None }
+ pub fn new(total_zram_size: u64, zram_writeback_size: u64) -> Self {
+ Self::new_with_page_size(total_zram_size, zram_writeback_size, get_page_size())
+ }
+
+ /// Creates a new [ZramWriteback] with a specified page size.
+ pub fn new_with_page_size(
+ total_zram_size: u64,
+ zram_writeback_size: u64,
+ page_size: u64,
+ ) -> Self {
+ assert!(page_size != 0);
+ let total_zram_pages = total_zram_size / page_size;
+ let zram_writeback_pages = zram_writeback_size / page_size;
+ assert!(total_zram_pages != 0);
+ Self { last_writeback_at: None, total_zram_pages, zram_writeback_pages, page_size }
}
/// Writes back idle or huge zram pages to disk.
- pub fn mark_and_flush_pages<O: Ops>(&mut self, params: &Params, now: Instant) -> Result<()> {
+ pub fn mark_and_flush_pages<O: Ops>(
+ &mut self,
+ params: &Params,
+ stats: &Stats,
+ now: Instant,
+ ) -> Result<()> {
if let Some(last_at) = self.last_writeback_at {
if now - last_at < params.backoff_duration {
return Err(Error::BackoffTime);
}
}
+ let limit_pages = self.calculate_writeback_limit(params, stats);
+ if limit_pages == 0 {
+ return Err(Error::Limit);
+ }
+ O::write(ZRAM_WRITEBACK_LIMIT_PATH, limit_pages.to_string())
+ .map_err(Error::WritebackLimit)?;
+
if params.huge_idle {
self.writeback::<O>(params, Mode::HugeIdle, now)?;
}
@@ -119,6 +172,34 @@
Ok(())
}
+ fn calculate_writeback_limit(&self, params: &Params, stats: &Stats) -> u64 {
+ let min_pages = params.min_bytes / self.page_size;
+ let max_pages = params.max_bytes / self.page_size;
+ // All calculations are performed in basis points, 100 bps = 1.00%. The number of pages
+ // allowed to be written back follows a simple linear relationship. The allowable range is
+ // [min_pages, max_pages], and the writeback limit will be the (zram utilization) * the
+ // range, that is, the more zram we're using the more we're going to allow to be written
+ // back.
+ const BPS: u64 = 100 * 100;
+ let zram_utilization_bps =
+ stats.orig_data_size / self.page_size * BPS / self.total_zram_pages;
+ let limit_pages = zram_utilization_bps * max_pages / BPS;
+
+ // And try to limit it to the approximate number of free backing device pages (if it's
+ // less).
+ let free_bd_pages = self.zram_writeback_pages - stats.current_writeback_pages;
+ let limit_pages = std::cmp::min(limit_pages, free_bd_pages);
+
+ if limit_pages < min_pages {
+ // Configured to not writeback fewer than configured min_pages.
+ return 0;
+ }
+
+ // Finally enforce the limits, we won't even attempt writeback if we cannot writeback at
+ // least the min, and we will cap to the max if it's greater.
+ std::cmp::min(limit_pages, max_pages)
+ }
+
fn writeback<O: Ops>(&mut self, params: &Params, mode: Mode, now: Instant) -> Result<()> {
match mode {
Mode::HugeIdle | Mode::Idle => {
@@ -142,11 +223,3 @@
Ok(())
}
}
-
-// TODO: remove this when we add parameters to [ZramWriteback::new].
-// This is just to suppress clippy::new_without_default.
-impl Default for ZramWriteback {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/src/zram/writeback/tests.rs b/src/zram/writeback/tests.rs
index 6728d12..44ed5e4 100644
--- a/src/zram/writeback/tests.rs
+++ b/src/zram/writeback/tests.rs
@@ -22,6 +22,10 @@
use crate::zram::idle::ZRAM_IDLE_PATH;
use mockall::Sequence;
+const DEFAULT_TOTAL_ZRAM_SIZE: u64 = 4 << 30;
+const DEFAULT_ZRAM_WRITEBACK_SIZE: u64 = 1 << 30;
+const DEFAULT_PAGE_SIZE: u64 = 4096;
+
fn setup_default_meminfo(rmock: &crate::os::__mock_MockOps_Ops::__read_to_string::Context) {
let meminfo = "MemTotal: 8144296 kB
MemAvailable: 346452 kB";
@@ -36,10 +40,18 @@
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params::default();
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
wmock
.expect()
+ .withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH))
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
.withf(write_path_params(ZRAM_IDLE_PATH))
.times(1)
.in_sequence(&mut seq)
@@ -69,7 +81,9 @@
.in_sequence(&mut seq)
.returning(|_, _| Ok(()));
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
}
#[test]
@@ -80,16 +94,21 @@
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params { backoff_duration: Duration::from_secs(100), ..Default::default() };
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
let base_time = Instant::now();
- let mut zram_writeback = ZramWriteback::new();
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, base_time).is_ok());
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
+ assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, &stats, base_time).is_ok());
wmock.checkpoint();
wmock.expect().times(0);
assert!(matches!(
- zram_writeback
- .mark_and_flush_pages::<MockOps>(¶ms, base_time + Duration::from_secs(99)),
+ zram_writeback.mark_and_flush_pages::<MockOps>(
+ ¶ms,
+ &stats,
+ base_time + Duration::from_secs(99)
+ ),
Err(Error::BackoffTime)
));
}
@@ -102,16 +121,19 @@
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params { backoff_duration: Duration::from_secs(100), ..Default::default() };
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
let base_time = Instant::now();
- let mut zram_writeback = ZramWriteback::new();
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, base_time).is_ok());
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
+ assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, &stats, base_time).is_ok());
wmock.checkpoint();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).times(3).returning(|_, _| Ok(()));
assert!(zram_writeback
- .mark_and_flush_pages::<MockOps>(¶ms, base_time + Duration::from_secs(100))
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, base_time + Duration::from_secs(100))
.is_ok());
}
@@ -119,6 +141,7 @@
fn mark_and_flush_pages_idle_time() {
let _m = OPS_MTX.lock();
let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
let rmock = MockOps::read_to_string_context();
let meminfo = "MemTotal: 10000 kB
@@ -129,27 +152,34 @@
max_idle: Duration::from_secs(4000),
..Default::default()
};
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
wmock.expect().withf(write_params(ZRAM_IDLE_PATH, "3747")).times(2).returning(|_, _| Ok(()));
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
}
#[test]
fn mark_and_flush_pages_calculate_idle_failure() {
let _m = OPS_MTX.lock();
let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
let params = Params {
min_idle: Duration::from_secs(4000),
max_idle: Duration::from_secs(3600),
..Default::default()
};
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
assert!(matches!(
- zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()),
+ zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now()),
Err(Error::CalculateIdle(_))
));
}
@@ -158,11 +188,14 @@
fn mark_and_flush_pages_mark_idle_failure() {
let _m = OPS_MTX.lock();
let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params::default();
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
wmock
.expect()
@@ -170,7 +203,7 @@
.returning(|_, _| Err(std::io::Error::other("error")));
assert!(matches!(
- zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()),
+ zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now()),
Err(Error::MarkIdle(_))
));
}
@@ -179,11 +212,14 @@
fn mark_and_flush_pages_skip_huge_idle() {
let _m = OPS_MTX.lock();
let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params { huge_idle: false, ..Default::default() };
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
wmock
.expect()
@@ -201,18 +237,23 @@
.times(1)
.returning(|_, _| Ok(()));
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
}
#[test]
fn mark_and_flush_pages_skip_idle() {
let _m = OPS_MTX.lock();
let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params { idle: false, ..Default::default() };
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
wmock
.expect()
@@ -230,18 +271,23 @@
.times(1)
.returning(|_, _| Ok(()));
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
}
#[test]
fn mark_and_flush_pages_skip_huge() {
let _m = OPS_MTX.lock();
let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH)).returning(|_, _| Ok(()));
wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
let rmock = MockOps::read_to_string_context();
setup_default_meminfo(&rmock);
let params = Params { huge: false, ..Default::default() };
- let mut zram_writeback = ZramWriteback::new();
+ let stats = Stats { orig_data_size: params.max_bytes, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new(DEFAULT_TOTAL_ZRAM_SIZE, DEFAULT_ZRAM_WRITEBACK_SIZE);
wmock
.expect()
@@ -259,5 +305,136 @@
.times(0)
.returning(|_, _| Ok(()));
- assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
+}
+
+#[test]
+fn mark_and_flush_pages_write_limit_from_orig_data_size() {
+ let _m = OPS_MTX.lock();
+ let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params {
+ max_bytes: 600 * DEFAULT_PAGE_SIZE,
+ min_bytes: 10 * DEFAULT_PAGE_SIZE,
+ ..Default::default()
+ };
+ // zram utilization is 25%
+ let stats = Stats { orig_data_size: 500 * DEFAULT_PAGE_SIZE, ..Default::default() };
+ let mut zram_writeback = ZramWriteback::new_with_page_size(
+ 2000 * DEFAULT_PAGE_SIZE,
+ 1000 * DEFAULT_PAGE_SIZE,
+ DEFAULT_PAGE_SIZE,
+ );
+
+ // Writeback limit is 25% of max_bytes.
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_LIMIT_PATH, "150"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
+}
+
+#[test]
+fn mark_and_flush_pages_write_limit_from_orig_data_size_with_big_page_size() {
+ let _m = OPS_MTX.lock();
+ let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let page_size = 2 * DEFAULT_PAGE_SIZE;
+ let params =
+ Params { max_bytes: 600 * page_size, min_bytes: 10 * page_size, ..Default::default() };
+ // zram utilization is 25%
+ let stats = Stats { orig_data_size: 500 * page_size, ..Default::default() };
+ let mut zram_writeback =
+ ZramWriteback::new_with_page_size(2000 * page_size, 1000 * page_size, page_size);
+
+ // Writeback limit is 25% of maxPages.
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_LIMIT_PATH, "150"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
+}
+
+#[test]
+fn mark_and_flush_pages_write_limit_capped_by_current_writeback_size() {
+ let _m = OPS_MTX.lock();
+ let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params {
+ max_bytes: 600 * DEFAULT_PAGE_SIZE,
+ min_bytes: 10 * DEFAULT_PAGE_SIZE,
+ ..Default::default()
+ };
+ // zram utilization is 25%
+ let stats =
+ Stats { orig_data_size: 500 * DEFAULT_PAGE_SIZE, current_writeback_pages: 1000 - 50 };
+ let mut zram_writeback = ZramWriteback::new_with_page_size(
+ 2000 * DEFAULT_PAGE_SIZE,
+ 1000 * DEFAULT_PAGE_SIZE,
+ DEFAULT_PAGE_SIZE,
+ );
+
+ // Writeback disk only has 50 pages left.
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_LIMIT_PATH, "50"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now())
+ .is_ok());
+}
+
+#[test]
+fn mark_and_flush_pages_write_limit_capped_by_min_pages() {
+ let _m = OPS_MTX.lock();
+ let wmock = MockOps::write_context();
+ wmock.expect().withf(write_path_params(ZRAM_IDLE_PATH)).returning(|_, _| Ok(()));
+ wmock.expect().withf(write_path_params(ZRAM_WRITEBACK_PATH)).returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params {
+ max_bytes: 500 * DEFAULT_PAGE_SIZE,
+ min_bytes: 6 * DEFAULT_PAGE_SIZE,
+ ..Default::default()
+ };
+ // zram utilization is 1%
+ let stats = Stats { orig_data_size: 20 * DEFAULT_PAGE_SIZE, ..Default::default() };
+ let mut zram_writeback = ZramWriteback::new_with_page_size(
+ 2000 * DEFAULT_PAGE_SIZE,
+ 1000 * DEFAULT_PAGE_SIZE,
+ DEFAULT_PAGE_SIZE,
+ );
+
+ // Writeback limit is 5 pages (= 1% of 500 pages). But it is lower than min pages.
+ wmock
+ .expect()
+ .withf(write_path_params(ZRAM_WRITEBACK_LIMIT_PATH))
+ .times(0)
+ .returning(|_, _| Ok(()));
+
+ assert!(matches!(
+ zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, &stats, Instant::now()),
+ Err(Error::Limit)
+ ));
}