Skip to content

Commit c1244cb

Browse files
committed
Use sparse file as default
1 parent 9d6134f commit c1244cb

5 files changed

Lines changed: 297 additions & 21 deletions

File tree

include/swap-default.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ swapfc_priority=50 # Swap priority (decreases per file)
6666
swapfc_path=/swapfc/swapfile # Path for swap files (must be btrfs)
6767
swapfc_frequency=1 # Check interval in seconds
6868

69+
# Sparse files (thin provisioning) - ENABLED BY DEFAULT
70+
# Creates swap files that only allocate disk space when data is actually written.
71+
# This is ideal with zswap: pages stay in RAM until pool is full, then disk is used.
72+
# Uses loop device automatically (required for sparse swap files).
73+
# swapfc_use_sparse_disable=1 # Uncomment to pre-allocate disk space
74+
6975
# Btrfs compression mode: uses loop device over sparse file on compressed btrfs
7076
# This enables swap data to be compressed on disk by btrfs (zstd)
7177
# RAM: zswap compresses → Disk: btrfs compresses again (double compression!)

src/main.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -362,21 +362,62 @@ fn stop(on_init: bool) -> Result<(), Box<dyn std::error::Error>> {
362362

363363
/// Show swap status
364364
fn status() -> Result<(), Box<dyn std::error::Error>> {
365-
if am_i_root().is_err() {
365+
let is_root = am_i_root().is_ok();
366+
if !is_root {
366367
warn!("Not root! Some output might be missing.");
367368
}
368369

369-
let swap_stats = get_mem_stats(&["SwapTotal", "SwapFree"])?;
370-
let swap_used = swap_stats["SwapTotal"] - swap_stats["SwapFree"];
370+
let swap_stats = get_mem_stats(&["MemTotal", "SwapTotal", "SwapFree"])?;
371+
let mem_total = swap_stats["MemTotal"];
372+
let swap_total = swap_stats["SwapTotal"];
373+
let swap_used = swap_total - swap_stats["SwapFree"];
371374

372375
// Zswap status
373376
if let Some(zswap) = systemd_swap::zswap::get_status() {
374377
println!("Zswap:");
375378
println!(" enabled: {}", zswap.enabled);
376379
println!(" compressor: {}", zswap.compressor);
377380
println!(" zpool: {}", zswap.zpool);
381+
println!(" max_pool_percent: {}%", zswap.max_pool_percent);
378382

379-
if zswap.pool_size > 0 || zswap.stored_pages > 0 {
383+
if is_root && (zswap.pool_size > 0 || zswap.stored_pages > 0) {
384+
let page_size = get_page_size();
385+
let stored_bytes = zswap.stored_pages * page_size;
386+
let ratio = zswap.compression_ratio(page_size);
387+
let pool_util = zswap.pool_utilization_percent(mem_total);
388+
389+
println!();
390+
println!(" === Pool Statistics (debugfs) ===");
391+
println!(" pool_size: {} ({:.1} MiB)", zswap.pool_size, zswap.pool_size as f64 / 1024.0 / 1024.0);
392+
println!(" stored_pages: {} ({:.1} MiB uncompressed)", zswap.stored_pages, stored_bytes as f64 / 1024.0 / 1024.0);
393+
println!(" pool_utilization: {}%", pool_util);
394+
println!(" compress_ratio: {:.0}%", ratio * 100.0);
395+
println!(" same_filled_pages: {}", zswap.same_filled_pages);
396+
println!();
397+
println!(" === Writeback Statistics ===");
398+
println!(" written_back_pages: {} ({:.1} MiB)",
399+
zswap.written_back_pages,
400+
(zswap.written_back_pages * page_size) as f64 / 1024.0 / 1024.0);
401+
println!(" pool_limit_hit: {}", zswap.pool_limit_hit);
402+
println!(" reject_reclaim_fail: {}", zswap.reject_reclaim_fail);
403+
404+
// Show effective swap usage
405+
if swap_used > 0 {
406+
let disk_used = swap_used.saturating_sub(stored_bytes);
407+
println!();
408+
println!(" === Effective Swap Usage ===");
409+
println!(" kernel_reported_used: {:.1} MiB", swap_used as f64 / 1024.0 / 1024.0);
410+
println!(" in_zswap_pool (RAM): {:.1} MiB", stored_bytes as f64 / 1024.0 / 1024.0);
411+
println!(" actual_disk_used: {:.1} MiB", disk_used as f64 / 1024.0 / 1024.0);
412+
let percent_in_ram = if swap_used > 0 {
413+
(stored_bytes as f64 / swap_used as f64) * 100.0
414+
} else {
415+
0.0
416+
};
417+
println!(" swap_in_ram: {:.0}%", percent_in_ram);
418+
}
419+
} else if zswap.pool_size > 0 || zswap.stored_pages > 0 {
420+
// Non-root, basic info only
380421
let page_size = get_page_size();
381422
let stored_bytes = zswap.stored_pages * page_size;
382423
let ratio = if zswap.stored_pages > 0 {

src/meminfo.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,146 @@ pub fn get_cpu_count() -> usize {
108108
.unwrap_or(1)
109109
}
110110

111+
/// Zswap statistics from debugfs
112+
#[derive(Debug, Default, Clone)]
113+
pub struct ZswapStats {
114+
/// Pages currently stored in zswap pool (RAM)
115+
pub stored_pages: u64,
116+
/// Total bytes used by zswap pool in RAM
117+
pub pool_total_size: u64,
118+
/// Pages that have been written back to disk swap
119+
pub written_back_pages: u64,
120+
/// Pages rejected due to reclaim failure
121+
pub reject_reclaim_fail: u64,
122+
/// Same-value filled pages (often zeros)
123+
pub same_filled_pages: u64,
124+
/// Pool limit hit count
125+
pub pool_limit_hit: u64,
126+
}
127+
128+
const ZSWAP_DEBUG_DIR: &str = "/sys/kernel/debug/zswap";
129+
130+
/// Read zswap statistics from debugfs (requires root)
131+
pub fn get_zswap_stats() -> Option<ZswapStats> {
132+
let debug_path = std::path::Path::new(ZSWAP_DEBUG_DIR);
133+
if !debug_path.is_dir() {
134+
return None;
135+
}
136+
137+
let read_stat = |name: &str| -> u64 {
138+
std::fs::read_to_string(debug_path.join(name))
139+
.ok()
140+
.and_then(|s| s.trim().parse().ok())
141+
.unwrap_or(0)
142+
};
143+
144+
Some(ZswapStats {
145+
stored_pages: read_stat("stored_pages"),
146+
pool_total_size: read_stat("pool_total_size"),
147+
written_back_pages: read_stat("written_back_pages"),
148+
reject_reclaim_fail: read_stat("reject_reclaim_fail"),
149+
same_filled_pages: read_stat("same_filled_pages"),
150+
pool_limit_hit: read_stat("pool_limit_hit"),
151+
})
152+
}
153+
154+
/// Effective swap usage information accounting for zswap
155+
#[derive(Debug, Default)]
156+
pub struct EffectiveSwapUsage {
157+
/// Total swap space (bytes)
158+
pub swap_total: u64,
159+
/// Free swap space as reported by kernel (bytes)
160+
pub swap_free: u64,
161+
/// Swap used as reported by kernel (bytes) - includes zswap cached pages
162+
pub swap_used_kernel: u64,
163+
/// Bytes stored in zswap RAM pool (not on disk)
164+
pub zswap_pool_bytes: u64,
165+
/// Estimated bytes actually written to disk swap
166+
pub swap_used_disk: u64,
167+
/// Zswap pool utilization percentage (0-100)
168+
pub zswap_pool_percent: u8,
169+
/// Whether zswap is active and has stored pages
170+
pub zswap_active: bool,
171+
}
172+
173+
/// Get effective swap usage accounting for zswap compression
174+
///
175+
/// When zswap is active, the kernel reports swap usage based on allocated slots,
176+
/// but most of those pages may still be in zswap's RAM pool and not written to disk.
177+
/// This function calculates the actual disk pressure.
178+
pub fn get_effective_swap_usage() -> Result<EffectiveSwapUsage> {
179+
let stats = get_mem_stats(&["MemTotal", "SwapTotal", "SwapFree"])?;
180+
let swap_total = stats["SwapTotal"];
181+
let swap_free = stats["SwapFree"];
182+
let swap_used_kernel = swap_total.saturating_sub(swap_free);
183+
let mem_total = stats["MemTotal"];
184+
185+
let mut result = EffectiveSwapUsage {
186+
swap_total,
187+
swap_free,
188+
swap_used_kernel,
189+
zswap_pool_bytes: 0,
190+
swap_used_disk: swap_used_kernel, // Default: assume all on disk
191+
zswap_pool_percent: 0,
192+
zswap_active: false,
193+
};
194+
195+
// Check zswap status
196+
if let Some(zswap_stats) = get_zswap_stats() {
197+
let page_size = get_page_size();
198+
let stored_bytes = zswap_stats.stored_pages * page_size;
199+
200+
if stored_bytes > 0 {
201+
result.zswap_active = true;
202+
result.zswap_pool_bytes = zswap_stats.pool_total_size;
203+
204+
// Actual disk usage = kernel reported usage - pages in zswap pool
205+
// Pages in zswap pool are still in RAM, not written to disk
206+
result.swap_used_disk = swap_used_kernel.saturating_sub(stored_bytes);
207+
208+
// Calculate zswap pool utilization
209+
// Read max_pool_percent from sysfs
210+
let max_pool_percent: u64 = std::fs::read_to_string(
211+
"/sys/module/zswap/parameters/max_pool_percent"
212+
)
213+
.ok()
214+
.and_then(|s| s.trim().parse().ok())
215+
.unwrap_or(20);
216+
217+
let max_pool_size = mem_total * max_pool_percent / 100;
218+
if max_pool_size > 0 {
219+
result.zswap_pool_percent =
220+
((zswap_stats.pool_total_size * 100) / max_pool_size).min(100) as u8;
221+
}
222+
}
223+
}
224+
225+
Ok(result)
226+
}
227+
228+
/// Get effective free swap percentage accounting for zswap
229+
///
230+
/// This returns a more accurate picture of swap pressure:
231+
/// - If zswap is inactive, returns normal SwapFree percentage
232+
/// - If zswap is active, considers both pool utilization and disk pressure
233+
pub fn get_effective_free_swap_percent() -> Result<u8> {
234+
let usage = get_effective_swap_usage()?;
235+
236+
if !usage.zswap_active || usage.swap_total == 0 {
237+
// No zswap, use traditional calculation
238+
return Ok(((usage.swap_free * 100) / usage.swap_total.max(1)) as u8);
239+
}
240+
241+
// With zswap active, calculate based on actual disk usage
242+
let disk_used_percent = if usage.swap_total > 0 {
243+
((usage.swap_used_disk * 100) / usage.swap_total) as u8
244+
} else {
245+
0
246+
};
247+
248+
Ok(100u8.saturating_sub(disk_used_percent))
249+
}
250+
111251
#[cfg(test)]
112252
mod tests {
113253
use super::*;
@@ -123,4 +263,10 @@ mod tests {
123263
let percent = get_free_ram_percent().unwrap();
124264
assert!(percent <= 100);
125265
}
266+
267+
#[test]
268+
fn test_get_effective_swap_usage() {
269+
// This test may not work without swap, but should not panic
270+
let _ = get_effective_swap_usage();
271+
}
126272
}

src/swapfc.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub struct SwapFcConfig {
4949
pub force_use_loop: bool,
5050
pub directio: bool,
5151
pub use_btrfs_compression: bool,
52+
/// Use sparse files (thin provisioning) - only allocate disk space when needed
53+
pub use_sparse: bool,
5254
}
5355

5456
impl SwapFcConfig {
@@ -80,6 +82,10 @@ impl SwapFcConfig {
8082
force_use_loop: config.get_bool("swapfc_force_use_loop"),
8183
directio: config.get_bool("swapfc_directio"),
8284
use_btrfs_compression: config.get_bool("swapfc_use_btrfs_compression"),
85+
// Sparse files (thin provisioning) - enabled by default for all
86+
// Disk space is only allocated when data is actually written (zswap writeback)
87+
// Disable with swapfc_use_sparse_disable=1 to pre-allocate disk space
88+
use_sparse: !config.get_bool("swapfc_use_sparse_disable"),
8389
})
8490
}
8591
}
@@ -200,12 +206,15 @@ impl SwapFc {
200206

201207
let free_swap = get_free_swap_percent().unwrap_or(100);
202208

209+
// Allocate more swap chunks when free swap is low
210+
// With sparse files, this is fine - disk space is only used when zswap writes back
203211
if free_swap < self.config.free_swap_perc && self.allocated < self.config.max_count {
204212
info!("swapFC: swap {}% < {}% - allocating chunk #{}", free_swap, self.config.free_swap_perc, self.allocated + 1);
205213
let _ = self.create_swapfile();
206214
continue;
207215
}
208216

217+
// Free swap chunks when swap usage is low
209218
if self.allocated > self.config.min_count.max(2) && free_swap > self.config.remove_free_swap_perc {
210219
info!("swapFC: swap {}% > {}% - freeing chunk #{}", free_swap, self.config.remove_free_swap_perc, self.allocated);
211220
let _ = self.destroy_swapfile();
@@ -267,24 +276,38 @@ impl SwapFc {
267276
.open(&swapfile_path)?;
268277
}
269278

270-
// Determine if we're using btrfs compression mode
279+
// Determine allocation mode:
280+
// - Sparse: use truncate, only allocate disk space when data is written
281+
// - Preallocated: use fallocate, reserve all disk space upfront
271282
let use_compression = self.config.use_btrfs_compression;
272-
let use_loop = self.config.force_use_loop || use_compression;
273-
274-
if use_compression {
275-
// For btrfs compression: use sparse file with truncate
276-
// This allows btrfs to compress the data and only allocate used blocks
277-
info!("swapFC: creating sparse file for btrfs compression");
283+
let use_sparse = self.config.use_sparse || use_compression;
284+
285+
// Sparse files require loop device for safe swap operation
286+
// Direct swap on sparse files can cause issues when kernel tries to write
287+
let use_loop = self.config.force_use_loop || use_sparse;
288+
289+
if use_sparse {
290+
// Create sparse file (thin provisioning)
291+
// Disk space is only allocated when zswap/kernel actually writes data
292+
info!("swapFC: creating sparse file (thin provisioning)");
278293
Command::new("truncate")
279294
.args(["--size", &self.config.chunk_size.to_string()])
280295
.arg(&swapfile_path)
281296
.status()?;
282-
// Do NOT use chattr +C - we want compression!
297+
298+
if !use_compression {
299+
// Disable COW for btrfs when not using compression
300+
// This improves performance for swap workloads
301+
let _ = Command::new("chattr").args(["+C"]).arg(&swapfile_path).status();
302+
}
303+
// When using compression, do NOT set +C - we want compression!
283304
} else {
284-
// Disable COW for btrfs (normal mode)
305+
// Preallocated mode: reserve all disk space upfront
306+
// Disable COW for btrfs
285307
let _ = Command::new("chattr").args(["+C"]).arg(&swapfile_path).status();
286308

287309
// Allocate space with fallocate
310+
info!("swapFC: creating preallocated file");
288311
Command::new("fallocate")
289312
.args(["-l", &self.config.chunk_size.to_string()])
290313
.arg(&swapfile_path)
@@ -293,7 +316,8 @@ impl SwapFc {
293316

294317
let (swapfile, loop_device) = if use_loop {
295318
// Create loop device
296-
let directio = if self.config.directio && !use_compression { "on" } else { "off" };
319+
// For sparse files, disable direct-io to allow proper thin provisioning
320+
let directio = if self.config.directio && !use_sparse { "on" } else { "off" };
297321
let loop_dev = run_cmd_output(&[
298322
"losetup", "-f", "--show",
299323
&format!("--direct-io={}", directio),

0 commit comments

Comments
 (0)