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>(&params, 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>(&params, &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>(&params, Instant::now()).is_ok());
+    assert!(zram_writeback
+        .mark_and_flush_pages::<MockOps>(&params, &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>(&params, 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>(&params, &stats, base_time).is_ok());
     wmock.checkpoint();
 
     wmock.expect().times(0);
 
     assert!(matches!(
-        zram_writeback
-            .mark_and_flush_pages::<MockOps>(&params, base_time + Duration::from_secs(99)),
+        zram_writeback.mark_and_flush_pages::<MockOps>(
+            &params,
+            &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>(&params, 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>(&params, &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>(&params, base_time + Duration::from_secs(100))
+        .mark_and_flush_pages::<MockOps>(&params, &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>(&params, Instant::now()).is_ok());
+    assert!(zram_writeback
+        .mark_and_flush_pages::<MockOps>(&params, &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>(&params, Instant::now()),
+        zram_writeback.mark_and_flush_pages::<MockOps>(&params, &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>(&params, Instant::now()),
+        zram_writeback.mark_and_flush_pages::<MockOps>(&params, &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>(&params, Instant::now()).is_ok());
+    assert!(zram_writeback
+        .mark_and_flush_pages::<MockOps>(&params, &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>(&params, Instant::now()).is_ok());
+    assert!(zram_writeback
+        .mark_and_flush_pages::<MockOps>(&params, &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>(&params, Instant::now()).is_ok());
+    assert!(zram_writeback
+        .mark_and_flush_pages::<MockOps>(&params, &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>(&params, &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>(&params, &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>(&params, &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>(&params, &stats, Instant::now()),
+        Err(Error::Limit)
+    ));
 }