Skip to content

Commit 5b7866a

Browse files
committed
Alternative options
1 parent 08dcd2a commit 5b7866a

6 files changed

Lines changed: 375 additions & 71 deletions

File tree

include/swap-default.conf

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,51 @@
77
# Swap Mode - How swap is configured
88
#
99
# auto - Auto-detect: btrfs → zswap+swapfc, non-btrfs → zram only
10-
# zswap+swapfc - Force zswap with swap files (requires btrfs)
11-
# zram - Force zram only
10+
# zswap+swapfc - Zswap cache + swap files (default for btrfs)
11+
# zram+swapfc - Zram (fast) + swap files for overflow
12+
# zram - Zram only (no disk swap)
1213
# manual - Use explicit settings below (legacy mode)
1314
################################################################################
1415

1516
swap_mode=auto
1617

1718
################################################################################
1819
# Zswap - Compressed RAM cache for swap pages
20+
# Used in zswap+swapfc mode or manual with zswap_enabled=1
1921
#
2022
# zswap compresses pages before writing to swap, reducing I/O.
21-
# Automatically enabled in zswap+swapfc mode. Use zswap_enabled=0 to disable.
2223
# Requires a backing swap device (swap file or partition).
2324
################################################################################
2425

25-
zswap_compressor=zstd # lzo lz4 zstd lzo-rle lz4hc
26-
zswap_max_pool_percent=45 # Max % of RAM for compressed pool
27-
zswap_zpool=zsmalloc # Memory allocator
26+
zswap_compressor=zstd # lzo lz4 zstd lzo-rle lz4hc
27+
zswap_max_pool_percent=45 # Max % of RAM for compressed pool
28+
zswap_zpool=zsmalloc # Memory allocator (zsmalloc better than zbud)
29+
zswap_shrinker_enabled=1 # Proactively move cold pages to swap
30+
zswap_accept_threshold=80 # Accept pages again after pool was full (%)
2831

2932
################################################################################
3033
# Zram - Compressed RAM disk
31-
# Only used when swap_mode=zram or manual with zram_enabled=1
34+
# Used in zram+swapfc, zram-only modes, or manual with zram_enabled=1
3235
#
3336
# zram creates a compressed block device in RAM for swap
34-
# More efficient than zswap for systems without backing swap
37+
# Faster than zswap - ideal for desktop systems
3538
################################################################################
3639

37-
zram_size=$RAM_SIZE # Size (default: equal to RAM)
40+
# zram_size supports: 1G, 512M, 50%, 100% (full RAM)
41+
zram_size=50% # Size of zram device
3842
zram_alg=zstd # Compression: lzo lz4 zstd lzo-rle lz4hc
39-
zram_prio=32767 # Swap priority
43+
zram_prio=32767 # Swap priority (highest = used first)
44+
45+
# Zram writeback: move idle/incompressible pages to disk
46+
# Requires CONFIG_ZRAM_WRITEBACK in kernel
47+
# When enabled with empty dev, auto-creates sparse file + loop device
48+
zram_writeback=0 # 0=disabled, 1=enabled
49+
zram_writeback_dev= # Backing device (partition or empty for auto loop)
50+
zram_writeback_size=1G # Size of auto-created backing file
4051

4152
################################################################################
4253
# SwapFC - Dynamic swap file management (btrfs only)
43-
# Only used when swap_mode=zswap+swapfc or manual with swapfc_enabled=1
54+
# Used in zram+swapfc, zswap+swapfc modes, or manual with swapfc_enabled=1
4455
#
4556
# Creates swap files on-demand as memory pressure increases
4657
################################################################################
@@ -55,6 +66,11 @@ swapfc_priority=50 # Swap priority (decreases per file)
5566
swapfc_path=/swapfc/swapfile # Path for swap files (must be btrfs)
5667
swapfc_frequency=1 # Check interval in seconds
5768

69+
# Btrfs compression mode: uses loop device over sparse file on compressed btrfs
70+
# This enables swap data to be compressed on disk by btrfs (zstd)
71+
# RAM: zswap compresses → Disk: btrfs compresses again (double compression!)
72+
swapfc_use_btrfs_compression=0
73+
5874
################################################################################
5975
# Enable/disable settings
6076
# zswap_enabled: Set to 0 to disable zswap (default: 1, enabled when not using zram)

include/systemd-swap.service

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,7 @@ RemainAfterExit=yes
1212
TimeoutStopSec=5
1313
OOMScoreAdjust=-500
1414
CapabilityBoundingSet=CAP_SYS_ADMIN
15-
DeviceAllow=block-blkext r
16-
DeviceAllow=block-device-mapper r
17-
DeviceAllow=block-loop
18-
DeviceAllow=block-sd r
19-
DeviceAllow=block-virtblk r
20-
DeviceAllow=block-zram
21-
IOSchedulingPriority=2
15+
DevicePolicy=auto
2216
NoNewPrivileges=yes
2317
PrivateNetwork=yes
2418
PrivateTmp=yes

src/main.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ enum Commands {
4040
#[derive(Debug, Clone, Copy, PartialEq)]
4141
enum SwapMode {
4242
Auto,
43-
ZswapSwapfc, // zswap + swap files (for btrfs)
43+
ZramSwapfc, // zram + writeback to swap files (best for desktop!)
44+
ZswapSwapfc, // zswap + swap files (alternative)
4445
ZramOnly, // zram only (for non-btrfs)
4546
Manual, // Use explicit config values
4647
}
@@ -105,6 +106,7 @@ fn is_path_btrfs(path: &str) -> bool {
105106
/// Parse swap_mode from config
106107
fn get_swap_mode(config: &Config) -> SwapMode {
107108
match config.get("swap_mode").unwrap_or("auto").to_lowercase().as_str() {
109+
"zram+swapfc" | "zram_swapfc" => SwapMode::ZramSwapfc,
108110
"zswap+swapfc" | "zswap" => SwapMode::ZswapSwapfc,
109111
"zram" | "zram_only" => SwapMode::ZramOnly,
110112
"manual" => SwapMode::Manual,
@@ -132,6 +134,7 @@ fn start() -> Result<(), Box<dyn std::error::Error>> {
132134
SwapMode::Auto => {
133135
let swapfc_path = config.get("swapfc_path").unwrap_or("/swapfc/swapfile");
134136
if is_path_btrfs(swapfc_path) || is_root_btrfs() {
137+
// Desktop optimization: zswap + swapfc - tested to be faster
135138
info!("Auto-detected btrfs: using zswap + swapfc");
136139
SwapMode::ZswapSwapfc
137140
} else {
@@ -146,6 +149,36 @@ fn start() -> Result<(), Box<dyn std::error::Error>> {
146149
let mut zswap_backup: Option<ZswapBackup> = None;
147150

148151
match effective_mode {
152+
SwapMode::ZramSwapfc => {
153+
// Desktop-optimized mode: zram for speed + swapfc for overflow
154+
// zram is faster than zswap because it's a dedicated block device
155+
156+
// Set up signal handler
157+
signal_hook::flag::register(
158+
signal_hook::consts::SIGTERM,
159+
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
160+
)?;
161+
ctrlc::set_handler(move || {
162+
request_shutdown();
163+
})?;
164+
165+
// Start zram first (primary high-priority swap)
166+
info!("Setting up zram as primary swap...");
167+
if let Err(e) = systemd_swap::zram::start(&config) {
168+
error!("Zram: {}", e);
169+
}
170+
171+
// Create swapfc for overflow/writeback (lower priority)
172+
info!("Setting up swapfc as secondary swap for overflow...");
173+
let mut swapfc = SwapFc::new(&config)?;
174+
175+
// Create initial swap file
176+
swapfc.create_initial_swap()?;
177+
178+
// Run swapfc monitoring loop
179+
swapfc.run()?;
180+
}
181+
149182
SwapMode::ZswapSwapfc => {
150183
// For zswap: create swap file FIRST, then enable zswap
151184
// zswap needs a backing swap device to work

src/swapfc.rs

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub struct SwapFcConfig {
4848
pub priority: i32,
4949
pub force_use_loop: bool,
5050
pub directio: bool,
51+
pub use_btrfs_compression: bool,
5152
}
5253

5354
impl SwapFcConfig {
@@ -78,6 +79,7 @@ impl SwapFcConfig {
7879
priority: config.get_as("swapfc_priority").unwrap_or(50),
7980
force_use_loop: config.get_bool("swapfc_force_use_loop"),
8081
directio: config.get_bool("swapfc_directio"),
82+
use_btrfs_compression: config.get_bool("swapfc_use_btrfs_compression"),
8183
})
8284
}
8385
}
@@ -265,26 +267,42 @@ impl SwapFc {
265267
.open(&swapfile_path)?;
266268
}
267269

268-
// Disable COW for btrfs
269-
let _ = Command::new("chattr").args(["+C"]).arg(&swapfile_path).status();
270+
// Determine if we're using btrfs compression mode
271+
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");
278+
Command::new("truncate")
279+
.args(["--size", &self.config.chunk_size.to_string()])
280+
.arg(&swapfile_path)
281+
.status()?;
282+
// Do NOT use chattr +C - we want compression!
283+
} else {
284+
// Disable COW for btrfs (normal mode)
285+
let _ = Command::new("chattr").args(["+C"]).arg(&swapfile_path).status();
270286

271-
// Allocate space
272-
Command::new("fallocate")
273-
.args(["-l", &self.config.chunk_size.to_string()])
274-
.arg(&swapfile_path)
275-
.status()?;
287+
// Allocate space with fallocate
288+
Command::new("fallocate")
289+
.args(["-l", &self.config.chunk_size.to_string()])
290+
.arg(&swapfile_path)
291+
.status()?;
292+
}
276293

277-
let swapfile = if self.config.force_use_loop {
278-
let directio = if self.config.directio { "on" } else { "off" };
294+
let (swapfile, loop_device) = if use_loop {
295+
// Create loop device
296+
let directio = if self.config.directio && !use_compression { "on" } else { "off" };
279297
let loop_dev = run_cmd_output(&[
280298
"losetup", "-f", "--show",
281299
&format!("--direct-io={}", directio),
282300
&swapfile_path.to_string_lossy(),
283301
])?;
284-
fs::remove_file(&swapfile_path)?;
285-
loop_dev
302+
let loop_dev = loop_dev.trim().to_string();
303+
(loop_dev.clone(), Some(loop_dev))
286304
} else {
287-
swapfile_path.to_string_lossy().to_string()
305+
(swapfile_path.to_string_lossy().to_string(), None)
288306
};
289307

290308
// mkswap
@@ -295,13 +313,21 @@ impl SwapFc {
295313
.status()?;
296314

297315
// Generate and start swap unit
316+
// Use discard=pages for compressed mode to release space when pages are freed
317+
let discard_option = if use_compression { "pages" } else { "discard" };
298318
let unit_name = gen_swap_unit(
299319
Path::new(&swapfile),
300320
Some(self.priority),
301-
Some("discard"),
321+
Some(discard_option),
302322
&format!("swapfc_{}", self.allocated),
303323
)?;
304324

325+
// Store loop device info for cleanup
326+
if let Some(ref loop_dev) = loop_device {
327+
let loop_info_path = format!("{}/swapfc/loop_{}", WORK_DIR, self.allocated);
328+
let _ = fs::write(&loop_info_path, format!("{}\n{}", loop_dev, swapfile_path.display()));
329+
}
330+
305331
self.priority -= 1;
306332

307333
systemctl("daemon-reload", "")?;
@@ -316,6 +342,10 @@ impl SwapFc {
316342

317343
let tag = format!("swapfc_{}", self.allocated);
318344

345+
// Check if we have loop device info for this swap
346+
let loop_info_path = format!("{}/swapfc/loop_{}", WORK_DIR, self.allocated);
347+
let loop_info = fs::read_to_string(&loop_info_path).ok();
348+
319349
for unit_path in crate::helpers::find_swap_units() {
320350
if let Ok(content) = crate::helpers::read_file(&unit_path) {
321351
if content.contains(&tag) {
@@ -331,7 +361,24 @@ impl SwapFc {
331361

332362
force_remove(&unit_path, true);
333363

334-
if Path::new(&dev).is_file() {
364+
// Clean up loop device and backing file if applicable
365+
if let Some(ref info) = loop_info {
366+
let lines: Vec<&str> = info.lines().collect();
367+
if lines.len() >= 2 {
368+
let loop_dev = lines[0];
369+
let backing_file = lines[1];
370+
371+
// Detach loop device
372+
let _ = Command::new("losetup")
373+
.args(["-d", loop_dev])
374+
.status();
375+
376+
// Remove backing file
377+
force_remove(backing_file, false);
378+
}
379+
// Remove loop info file
380+
force_remove(&loop_info_path, false);
381+
} else if Path::new(&dev).is_file() {
335382
force_remove(&dev, false);
336383
}
337384
}

0 commit comments

Comments
 (0)