Introduce ZramWriteback
This implements the basic logic of periodical zram writeback.
mmd marks pages in zram as idle which have been in zram for more than a
specified duration between min_idle and max_idle.
The logic mainly comes from swap_management of ChromeOS.
Bug: 375432468
Test: atest mmd_unit_tests
Change-Id: I2d2a5bcabd7f2632d5707f001547d5052f3f8a3a
diff --git a/Android.bp b/Android.bp
index 9c0c74c..047bd46 100644
--- a/Android.bp
+++ b/Android.bp
@@ -8,14 +8,28 @@
"src/main.rs",
],
rustlibs: [
+ "libanyhow",
"libbinder_rs",
"liblogger",
"liblog_rust",
+ "libmmd",
"libmmd_flags_rust",
+ "libmockall",
"mmd_aidl_interface-rust",
],
}
+rust_defaults {
+ name: "libmmd_defaults",
+ srcs: [
+ "src/lib.rs",
+ ],
+ rustlibs: [
+ "libmockall",
+ "libthiserror",
+ ],
+}
+
rust_binary {
name: "mm_daemon",
defaults: ["mmd_defaults"],
@@ -23,6 +37,13 @@
init_rc: ["mmd.rc"],
}
+rust_library {
+ name: "libmmd",
+ crate_name: "mmd",
+ defaults: ["libmmd_defaults"],
+ host_supported: true,
+}
+
rust_test {
name: "mmd_unit_tests",
defaults: ["mmd_defaults"],
@@ -30,6 +51,12 @@
auto_gen_config: true,
}
+rust_test_host {
+ name: "libmmd_unit_tests",
+ defaults: ["libmmd_defaults"],
+ test_suites: ["general-tests"],
+}
+
aconfig_declarations {
name: "mmd_flags",
package: "android.mmd.flags",
diff --git a/TEST_MAPPING b/TEST_MAPPING
index b738f38..88354a1 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -2,6 +2,9 @@
"postsubmit": [
{
"name": "mmd_unit_tests"
+ },
+ {
+ "name": "libmmd_unit_tests"
}
]
}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..66db064
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,20 @@
+// 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 is library part of mmd which does not depends on Android specific APIs.
+
+pub mod os;
+#[cfg(test)]
+mod test_helper;
+pub mod zram;
diff --git a/src/main.rs b/src/main.rs
index 4d66a12..187023e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,7 +36,7 @@
}
}
- let mmd_service = service::MmdService;
+ let mmd_service = service::MmdService::new();
let mmd_service_binder = BnMmd::new_binder(mmd_service, BinderFeatures::default());
binder::add_service("mmd", mmd_service_binder.as_binder()).expect("register service");
diff --git a/src/os.rs b/src/os.rs
new file mode 100644
index 0000000..6c132a9
--- /dev/null
+++ b/src/os.rs
@@ -0,0 +1,50 @@
+// 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 mockable interface and its implementation of os
+//! operations.
+
+use std::io;
+use std::path::Path;
+
+/// [Ops] is the mockable interface for accesses to system global os properties
+/// (e.g. read/write on procfs, sysfs).
+///
+/// generic functions with AsRef requires #[mockall::concretize] which has a
+/// downside that we can't use `.with` but need to use `.withf` or `.withf_st`.
+///
+/// https://docs.rs/mockall/latest/mockall/attr.concretize.html
+///
+/// `mod test_helpers` defines helper macros to generate matcher for `.withf`.
+#[cfg_attr(test, mockall::automock)]
+pub trait Ops {
+ /// mockable wrapper for [std::fs::read_to_string].
+ #[mockall::concretize]
+ fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String>;
+ /// mockable wrapper for [std::fs::write].
+ #[mockall::concretize]
+ fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()>;
+}
+
+/// Real implementation of [Ops].
+pub struct OpsImpl;
+
+impl Ops for OpsImpl {
+ fn read_to_string<P: AsRef<Path>>(path: P) -> io::Result<String> {
+ std::fs::read_to_string(path)
+ }
+ fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result<()> {
+ std::fs::write(path, contents)
+ }
+}
diff --git a/src/service.rs b/src/service.rs
index b51aee5..2634fb0 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -12,18 +12,43 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+use std::sync::Mutex;
+use std::time::Instant;
+
use binder::Interface;
use binder::Result as BinderResult;
-use log::info;
+use log::error;
use mmd_aidl_interface::aidl::android::os::IMmd::IMmd;
-pub struct MmdService;
+use mmd::os::OpsImpl;
+use mmd::zram::writeback::Error as ZramWritebackError;
+use mmd::zram::writeback::ZramWriteback;
+
+pub struct MmdService {
+ zram_writeback: Mutex<ZramWriteback>,
+}
+
+impl MmdService {
+ pub fn new() -> Self {
+ Self { zram_writeback: Mutex::new(ZramWriteback::new()) }
+ }
+}
impl Interface for MmdService {}
impl IMmd for MmdService {
fn doZramMaintenance(&self) -> BinderResult<()> {
- info!("doZramMaintenance");
+ 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) => {}
+ Err(e) => error!("failed to zram writeback: {e:?}"),
+ }
Ok(())
}
}
+
+fn load_zram_writeback_params() -> mmd::zram::writeback::Params {
+ // TODO: load params from system properties.
+ mmd::zram::writeback::Params::default()
+}
diff --git a/src/test_helper.rs b/src/test_helper.rs
new file mode 100644
index 0000000..bad369c
--- /dev/null
+++ b/src/test_helper.rs
@@ -0,0 +1,49 @@
+// 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 helpers for tests.
+//!
+//! `*_params` are the helper functions to use with `.withf` of [crate::os::MockOps].
+
+use std::path::Path;
+use std::sync::Mutex;
+
+/// Mutex to synchronize tests using [crate::os::MockOps].
+///
+/// mockall for static functions requires synchronization.
+///
+/// https://docs.rs/mockall/latest/mockall/#static-methods
+pub static OPS_MTX: Mutex<()> = Mutex::new(());
+
+/// This is helper function for [Ops::write].
+pub fn write_params<P: AsRef<Path>, C: AsRef<[u8]>>(
+ p: P,
+ c: C,
+) -> impl Fn(&dyn AsRef<Path>, &dyn AsRef<[u8]>) -> bool {
+ move |path, contents| path.as_ref() == p.as_ref() && contents.as_ref() == c.as_ref()
+}
+
+/// This is helper function for [Ops::write]. This consider the path only.
+pub fn write_path_params<P: AsRef<Path>>(
+ p: P,
+) -> impl Fn(&dyn AsRef<Path>, &dyn AsRef<[u8]>) -> bool {
+ move |path, _| path.as_ref() == p.as_ref()
+}
+
+/// This is helper function for functions with single `AsRef<Path>` argument.
+///
+/// * [Ops::read_to_string]
+pub fn path_params<P: AsRef<Path>>(p: P) -> impl Fn(&dyn AsRef<Path>) -> bool {
+ move |path| path.as_ref() == p.as_ref()
+}
diff --git a/src/zram.rs b/src/zram.rs
new file mode 100644
index 0000000..0685f9e
--- /dev/null
+++ b/src/zram.rs
@@ -0,0 +1,18 @@
+// 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 policies to manage zram features.
+
+mod idle;
+pub mod writeback;
diff --git a/src/zram/idle.rs b/src/zram/idle.rs
new file mode 100644
index 0000000..8853d16
--- /dev/null
+++ b/src/zram/idle.rs
@@ -0,0 +1,263 @@
+// 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 std::time::Duration;
+
+use crate::os::Ops;
+
+pub const ZRAM_IDLE_PATH: &str = "/sys/block/zram0/idle";
+pub const MEMINFO_PATH: &str = "/proc/meminfo";
+
+/// Sets idle duration in seconds to "/sys/block/zram0/idle".
+///
+/// Fractions of a second are truncated.
+pub fn set_zram_idle_time<O: Ops>(idle_age: Duration) -> std::io::Result<()> {
+ O::write(ZRAM_IDLE_PATH, idle_age.as_secs().to_string())
+}
+
+/// This parses the content of "/proc/meminfo" and returns the number of "MemTotal" and
+/// "MemAvailable".
+///
+/// This does not care about the unit, because the user `calculate_idle_time()` use the values to
+/// calculate memory utilization rate. The unit should be always "kB".
+///
+/// This returns `None` if this fails to parse the content.
+fn parse_meminfo(content: &str) -> Option<(u64, u64)> {
+ let mut total = None;
+ let mut available = None;
+ for line in content.split("\n") {
+ let container = if line.contains("MemTotal:") {
+ &mut total
+ } else if line.contains("MemAvailable:") {
+ &mut available
+ } else {
+ continue;
+ };
+ let Some(number_str) = line.split_whitespace().nth(1) else {
+ continue;
+ };
+ let Ok(number) = number_str.parse::<u64>() else {
+ continue;
+ };
+ *container = Some(number);
+ }
+ if let (Some(total), Some(available)) = (total, available) {
+ Some((total, available))
+ } else {
+ None
+ }
+}
+
+/// Error from [calculate_idle_time].
+#[derive(Debug, thiserror::Error)]
+pub enum CalculateError {
+ #[error("min_idle is longer than max_idle")]
+ InvalidMinAndMax,
+ #[error("failed to parse meminfo")]
+ InvalidMeminfo,
+ #[error("failed to read meminfo: {0}")]
+ ReadMeminfo(std::io::Error),
+}
+
+/// Calculates idle duration from min_idle and max_idle using meminfo.
+pub fn calculate_idle_time<O: Ops>(
+ min_idle: Duration,
+ max_idle: Duration,
+) -> std::result::Result<Duration, CalculateError> {
+ if min_idle > max_idle {
+ return Err(CalculateError::InvalidMinAndMax);
+ }
+ let content = match O::read_to_string(MEMINFO_PATH) {
+ Ok(v) => v,
+ Err(e) => return Err(CalculateError::ReadMeminfo(e)),
+ };
+ let (total, available) = match parse_meminfo(&content) {
+ Some((total, available)) if total > 0 => (total, available),
+ _ => {
+ // Fallback to use the safest value.
+ return Err(CalculateError::InvalidMeminfo);
+ }
+ };
+
+ let mem_utilization = 1.0 - (available as f64) / (total as f64);
+
+ // Exponentially decay the age vs. memory utilization. The reason we choose exponential decay is
+ // because we want to do as little work as possible when the system is under very low memory
+ // pressure. As pressure increases we want to start aggressively shrinking our idle age to force
+ // newer pages to be written back/recompressed.
+ const LAMBDA: f64 = 5.0;
+ let seconds = ((max_idle - min_idle).as_secs() as f64)
+ * std::f64::consts::E.powf(-LAMBDA * mem_utilization)
+ + (min_idle.as_secs() as f64);
+
+ Ok(Duration::from_secs(seconds as u64))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::os::MockOps;
+ use crate::test_helper::path_params;
+ use crate::test_helper::write_params;
+ use crate::test_helper::OPS_MTX;
+
+ #[test]
+ fn test_set_zram_idle_time() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::write_context();
+ mock.expect().withf(write_params(ZRAM_IDLE_PATH, "3600")).returning(|_, _| Ok(()));
+
+ assert!(set_zram_idle_time::<MockOps>(Duration::from_secs(3600)).is_ok());
+ }
+
+ #[test]
+ fn test_set_zram_idle_time_in_seconds() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::write_context();
+ mock.expect().withf(write_params(ZRAM_IDLE_PATH, "3600")).returning(|_, _| Ok(()));
+
+ assert!(set_zram_idle_time::<MockOps>(Duration::from_millis(3600567)).is_ok());
+ }
+
+ #[test]
+ fn test_parse_meminfo() {
+ let content = "MemTotal: 123456789 kB
+MemFree: 12345 kB
+MemAvailable: 67890 kB
+ ";
+ assert_eq!(parse_meminfo(content).unwrap(), (123456789, 67890));
+ }
+
+ #[test]
+ fn test_parse_meminfo_invalid_format() {
+ // empty
+ assert!(parse_meminfo("").is_none());
+ // no number
+ let content = "MemTotal:
+MemFree: 12345 kB
+MemAvailable: 67890 kB
+ ";
+ assert!(parse_meminfo(content).is_none());
+ // no number
+ let content = "MemTotal: kB
+MemFree: 12345 kB
+MemAvailable: 67890 kB
+ ";
+ assert!(parse_meminfo(content).is_none());
+ // total memory missing
+ let content = "MemFree: 12345 kB
+MemAvailable: 67890 kB
+ ";
+ assert!(parse_meminfo(content).is_none());
+ // available memory missing
+ let content = "MemTotal: 123456789 kB
+MemFree: 12345 kB
+ ";
+ assert!(parse_meminfo(content).is_none());
+ }
+
+ #[test]
+ fn test_calculate_idle_time() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let meminfo = "MemTotal: 8144296 kB
+ MemAvailable: 346452 kB";
+ mock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+
+ assert_eq!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(72000), Duration::from_secs(90000))
+ .unwrap(),
+ Duration::from_secs(72150)
+ );
+ }
+
+ #[test]
+ fn test_calculate_idle_time_same_min_max() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let meminfo = "MemTotal: 8144296 kB
+ MemAvailable: 346452 kB";
+ mock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+
+ assert_eq!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(90000), Duration::from_secs(90000))
+ .unwrap(),
+ Duration::from_secs(90000)
+ );
+ }
+
+ #[test]
+ fn test_calculate_idle_time_min_is_bigger_than_max() {
+ assert!(matches!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(90000), Duration::from_secs(72000)),
+ Err(CalculateError::InvalidMinAndMax)
+ ));
+ }
+
+ #[test]
+ fn test_calculate_idle_time_no_available() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let meminfo = "MemTotal: 8144296 kB
+ MemAvailable: 0 kB";
+ mock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+
+ assert_eq!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(72000), Duration::from_secs(90000))
+ .unwrap(),
+ Duration::from_secs(72121)
+ );
+ }
+
+ #[test]
+ fn test_calculate_idle_time_meminfo_fail() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ mock.expect()
+ .withf(path_params(MEMINFO_PATH))
+ .returning(|_| Err(std::io::Error::other("error")));
+
+ assert!(matches!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(72000), Duration::from_secs(90000)),
+ Err(CalculateError::ReadMeminfo(_))
+ ));
+ }
+
+ #[test]
+ fn test_calculate_idle_time_invalid_meminfo() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let meminfo = "";
+ mock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+
+ assert!(matches!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(72000), Duration::from_secs(90000)),
+ Err(CalculateError::InvalidMeminfo)
+ ));
+ }
+
+ #[test]
+ fn test_calculate_idle_time_zero_total_memory() {
+ let _m = OPS_MTX.lock();
+ let mock = MockOps::read_to_string_context();
+ let meminfo = "MemTotal: 0 kB
+ MemAvailable: 346452 kB";
+ mock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+
+ assert!(matches!(
+ calculate_idle_time::<MockOps>(Duration::from_secs(72000), Duration::from_secs(90000)),
+ Err(CalculateError::InvalidMeminfo)
+ ));
+ }
+}
diff --git a/src/zram/writeback.rs b/src/zram/writeback.rs
new file mode 100644
index 0000000..80a7474
--- /dev/null
+++ b/src/zram/writeback.rs
@@ -0,0 +1,152 @@
+// 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 policy to manage zram writeback feature.
+//!
+//! See "writeback" section in the kernel document for details.
+//!
+//! https://www.kernel.org/doc/Documentation/blockdev/zram.txt
+
+#[cfg(test)]
+mod tests;
+
+use std::time::Duration;
+use std::time::Instant;
+
+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";
+
+/// Error from [ZramWriteback].
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// writeback too frequently
+ #[error("writeback too frequently")]
+ BackoffTime,
+ /// failure on setting zram idle
+ #[error("calculate zram idle {0}")]
+ CalculateIdle(#[from] crate::zram::idle::CalculateError),
+ /// failure on setting zram idle
+ #[error("set zram idle {0}")]
+ MarkIdle(std::io::Error),
+ /// failure on writing to /sys/block/zram0/writeback
+ #[error("writeback: {0}")]
+ Writeback(std::io::Error),
+}
+
+type Result<T> = std::result::Result<T, Error>;
+
+/// The parameters for zram writeback.
+pub struct Params {
+ /// The backoff time since last writeback.
+ pub backoff_duration: Duration,
+ /// The minimum idle duration to writeback. This is used for [calculate_idle_time].
+ pub min_idle: Duration,
+ /// The maximum idle duration to writeback. This is used for [calculate_idle_time].
+ pub max_idle: Duration,
+ /// Whether writeback huge and idle pages or not.
+ pub huge_idle: bool,
+ /// Whether writeback idle pages or not.
+ pub idle: bool,
+ /// Whether writeback huge pages or not.
+ pub huge: bool,
+}
+
+impl Default for Params {
+ fn default() -> Self {
+ Self {
+ // 10 minutes
+ backoff_duration: Duration::from_secs(600),
+ // 20 hours
+ min_idle: Duration::from_secs(20 * 3600),
+ // 25 hours
+ max_idle: Duration::from_secs(25 * 3600),
+ huge_idle: true,
+ idle: true,
+ huge: true,
+ }
+ }
+}
+
+enum Mode {
+ HugeIdle,
+ Idle,
+ Huge,
+}
+
+/// ZramWriteback manages zram writeback policies.
+pub struct ZramWriteback {
+ last_writeback_at: Option<Instant>,
+}
+
+impl ZramWriteback {
+ /// Creates a new [ZramWriteback].
+ pub fn new() -> Self {
+ Self { last_writeback_at: None }
+ }
+
+ /// Writes back idle or huge zram pages to disk.
+ pub fn mark_and_flush_pages<O: Ops>(&mut self, params: &Params, now: Instant) -> Result<()> {
+ if let Some(last_at) = self.last_writeback_at {
+ if now - last_at < params.backoff_duration {
+ return Err(Error::BackoffTime);
+ }
+ }
+
+ if params.huge_idle {
+ self.writeback::<O>(params, Mode::HugeIdle, now)?;
+ }
+ if params.idle {
+ self.writeback::<O>(params, Mode::Idle, now)?;
+ }
+ if params.huge {
+ self.writeback::<O>(params, Mode::Huge, now)?;
+ }
+
+ Ok(())
+ }
+
+ fn writeback<O: Ops>(&mut self, params: &Params, mode: Mode, now: Instant) -> Result<()> {
+ match mode {
+ Mode::HugeIdle | Mode::Idle => {
+ let idle_age = calculate_idle_time::<O>(params.min_idle, params.max_idle)?;
+ // TODO: adjust the idle_age by suspend duration.
+ set_zram_idle_time::<O>(idle_age).map_err(Error::MarkIdle)?;
+ }
+ Mode::Huge => {}
+ }
+
+ let mode = match mode {
+ Mode::HugeIdle => "huge_idle",
+ Mode::Idle => "idle",
+ Mode::Huge => "huge",
+ };
+
+ O::write(ZRAM_WRITEBACK_PATH, mode).map_err(Error::Writeback)?;
+
+ self.last_writeback_at = Some(now);
+
+ 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
new file mode 100644
index 0000000..6728d12
--- /dev/null
+++ b/src/zram/writeback/tests.rs
@@ -0,0 +1,263 @@
+// 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::write_params;
+use crate::test_helper::write_path_params;
+use crate::test_helper::OPS_MTX;
+use crate::zram::idle::MEMINFO_PATH;
+use crate::zram::idle::ZRAM_IDLE_PATH;
+use mockall::Sequence;
+
+fn setup_default_meminfo(rmock: &crate::os::__mock_MockOps_Ops::__read_to_string::Context) {
+ let meminfo = "MemTotal: 8144296 kB
+ MemAvailable: 346452 kB";
+ rmock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+}
+
+#[test]
+fn mark_and_flush_pages() {
+ let _m = OPS_MTX.lock();
+ let mut seq = Sequence::new();
+ let wmock = MockOps::write_context();
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params::default();
+ let mut zram_writeback = ZramWriteback::new();
+
+ wmock
+ .expect()
+ .withf(write_path_params(ZRAM_IDLE_PATH))
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge_idle"))
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_path_params(ZRAM_IDLE_PATH))
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "idle"))
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge"))
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+}
+
+#[test]
+fn mark_and_flush_pages_before_backoff() {
+ let _m = OPS_MTX.lock();
+ let wmock = MockOps::write_context();
+ wmock.expect().returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params { backoff_duration: Duration::from_secs(100), ..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());
+ wmock.checkpoint();
+
+ wmock.expect().times(0);
+
+ assert!(matches!(
+ zram_writeback
+ .mark_and_flush_pages::<MockOps>(¶ms, base_time + Duration::from_secs(99)),
+ Err(Error::BackoffTime)
+ ));
+}
+
+#[test]
+fn mark_and_flush_pages_after_backoff() {
+ let _m = OPS_MTX.lock();
+ let wmock = MockOps::write_context();
+ wmock.expect().returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params { backoff_duration: Duration::from_secs(100), ..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());
+ wmock.checkpoint();
+ 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))
+ .is_ok());
+}
+
+#[test]
+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_PATH)).returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ let meminfo = "MemTotal: 10000 kB
+ MemAvailable: 8000 kB";
+ rmock.expect().withf(path_params(MEMINFO_PATH)).returning(|_| Ok(meminfo.to_string()));
+ let params = Params {
+ min_idle: Duration::from_secs(3600),
+ max_idle: Duration::from_secs(4000),
+ ..Default::default()
+ };
+ let mut zram_writeback = ZramWriteback::new();
+
+ 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());
+}
+
+#[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_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();
+
+ assert!(matches!(
+ zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()),
+ Err(Error::CalculateIdle(_))
+ ));
+}
+
+#[test]
+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_PATH)).returning(|_, _| Ok(()));
+ let rmock = MockOps::read_to_string_context();
+ setup_default_meminfo(&rmock);
+ let params = Params::default();
+ let mut zram_writeback = ZramWriteback::new();
+
+ wmock
+ .expect()
+ .withf(write_path_params(ZRAM_IDLE_PATH))
+ .returning(|_, _| Err(std::io::Error::other("error")));
+
+ assert!(matches!(
+ zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()),
+ Err(Error::MarkIdle(_))
+ ));
+}
+
+#[test]
+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_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();
+
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge_idle"))
+ .times(0)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "idle"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, 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_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();
+
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge_idle"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "idle"))
+ .times(0)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, 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_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();
+
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge_idle"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "idle"))
+ .times(1)
+ .returning(|_, _| Ok(()));
+ wmock
+ .expect()
+ .withf(write_params(ZRAM_WRITEBACK_PATH, "huge"))
+ .times(0)
+ .returning(|_, _| Ok(()));
+
+ assert!(zram_writeback.mark_and_flush_pages::<MockOps>(¶ms, Instant::now()).is_ok());
+}