diff --git a/.gitignore b/.gitignore index 02890df..d4f5a39 100644 --- a/.gitignore +++ b/.gitignore @@ -167,7 +167,3 @@ cython_debug/ # PyPI configuration file .pypirc - -# Claude Code -CLAUDE.md -.claude/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2529ef7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,208 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +systemd-swap is a dynamic swap management daemon for Linux, written in Rust. It automatically configures optimal swap strategies based on the system's filesystem and requirements, managing zswap, zram, and dynamic swap files (SwapFC). + +The project replaces a previous Python implementation with a lightweight (~250 KB) Rust binary that provides: +- Auto-detection of optimal swap strategy based on filesystem type +- Zswap (compressed RAM cache) + SwapFC (dynamic swap files) +- Zram (compressed block device in RAM) with optional writeback to disk +- Multi-filesystem support: btrfs, ext4, xfs + +## Building and Testing + +```bash +# Build the project (requires Rust 1.70+) +cargo build --release + +# Install to system (requires root) +sudo make install + +# Enable and start the service +sudo systemctl enable --now systemd-swap + +# Check service status +systemctl status systemd-swap + +# View detailed swap status (with extra stats as root) +systemd-swap status + +# View logs +journalctl -u systemd-swap -f +``` + +## Build for Arch Linux/BigLinux + +```bash +cd pkgbuild +makepkg -si +``` + +## Architecture + +### Swap Modes + +The daemon operates in one of several modes (auto-detected or manually configured): + +1. **auto** (default): Detects filesystem and chooses optimal strategy + - btrfs/ext4/xfs → zswap+swapfc + - other filesystems → zram only + +2. **zswap+swapfc**: Zswap compresses pages in RAM, SwapFC provides backing swap files + - Default for btrfs/ext4/xfs systems + - Zswap shrinker proactively moves cold pages to disk + - Best desktop performance with efficient memory usage + +3. **zram+swapfc**: Zram as primary compressed swap with swap files for overflow + - Optional zram writeback moves idle pages to disk + +4. **zram**: Zram-only mode (no disk swap) + +5. **manual**: Explicit configuration via config file flags + +### Core Components + +**src/main.rs** (479 lines) +- Entry point with CLI parsing (start/stop/status commands) +- Swap mode selection logic based on filesystem detection +- Main daemon loop orchestrating zswap/zram/swapfc +- Signal handling for graceful shutdown + +**src/config.rs** (172 lines) +- Configuration parser supporting hierarchical config files: + - `/usr/share/systemd-swap/swap-default.conf` (defaults) + - `/etc/systemd/swap.conf` (user overrides) + - `{/usr/lib,/run,/etc}/systemd/swap.conf.d/*.conf` (fragments) +- Shell variable expansion in config values +- Type-safe getters for configuration values + +**src/swapfc.rs** (558 lines) +- Dynamic swap file management +- Creates/removes swap files based on memory pressure +- Supports both pre-allocated (fallocate) and sparse files +- Btrfs-specific: subvolume setup, compression mode support +- Auto-detects filesystem type and validates support (btrfs/ext4/xfs) + +**src/zswap.rs** (265 lines) +- Zswap configuration via `/sys/module/zswap/parameters` +- Parameter backup/restore on daemon stop +- Status reporting with debugfs statistics (pool size, compression ratio, etc.) + +**src/zram.rs** (380 lines) +- Zram device creation and configuration +- Writeback support (CONFIG_ZRAM_WRITEBACK kernel feature) +- Auto-creation of loop devices for writeback when no partition specified +- Size parsing: absolute (1G, 512M) or percentage (50%, 100%) + +**src/systemd.rs** (145 lines) +- Systemd integration: sd_notify, swap unit generation +- Swap unit management via systemctl + +**src/meminfo.rs** (300 lines) +- Memory statistics parsing from `/proc/meminfo` +- CPU count, RAM size, free RAM/swap percentage calculations +- Page size detection + +**src/helpers.rs** (148 lines) +- Common utilities: file I/O, directory creation, command execution +- Root privilege checking + +**src/pre-systemd-swap** (124 lines, bash) +- Pre-service script run before main daemon +- Btrfs subvolume setup for swap files +- Disables existing swap partitions +- Adds fstab entries for swap subvolumes + +### Configuration Flow + +1. Load defaults from `/usr/share/systemd-swap/swap-default.conf` +2. Override with `/etc/systemd/swap.conf` +3. Apply fragments from conf.d directories (etc > run > lib precedence) +4. Expand shell variables in values + +### Filesystem Detection Logic + +- Uses `findmnt` to detect filesystem type +- For swap file paths, checks both the path and its parent +- Validates filesystem against `SWAPFILE_SUPPORTED_FS` (btrfs/ext4/xfs) +- Falls back to zram-only for unsupported filesystems + +### Pre-allocated vs Sparse Files + +By default, SwapFC uses **pre-allocated** files via fallocate (disabled sparse): +- More stable under memory pressure +- Reserves disk space upfront +- Set `swapfc_use_sparse=1` to enable sparse/thin provisioning + +### Btrfs-Specific Handling + +- Automatically creates `@swapfc` subvolume on btrfs root +- Optional compression mode: creates loop device over file on compressed subvolume +- Disables CoW (copy-on-write) for swap files + +## File Locations + +| Path | Description | +|------|-------------| +| `/usr/bin/systemd-swap` | Main Rust binary | +| `/usr/bin/pre-systemd-swap` | Bash pre-setup script | +| `/etc/systemd/swap.conf` | User configuration | +| `/usr/share/systemd-swap/swap-default.conf` | Default configuration | +| `/run/systemd/swap/` | Runtime state directory | +| `/swapfc/` | Default swap file location | +| `/usr/lib/systemd/system/systemd-swap.service` | Main systemd unit | +| `/usr/lib/systemd/system/pre-systemd-swap.service` | Pre-setup systemd unit | + +## Key Configuration Parameters + +**swap_mode**: auto | zswap+swapfc | zram+swapfc | zram | manual + +**Zswap (for zswap+swapfc mode)**: +- `zswap_compressor`: zstd (default), lzo, lz4, lzo-rle, lz4hc +- `zswap_max_pool_percent`: Max RAM % for pool (default: 45) +- `zswap_shrinker_enabled`: Proactive writeback (default: 1) + +**Zram (for zram modes)**: +- `zram_size`: 1G, 512M, 50%, 100% (default: 80%) +- `zram_alg`: Compression algorithm (default: zstd) +- `zram_writeback`: Enable writeback to disk (default: 0) + +**SwapFC**: +- `swapfc_chunk_size`: Per-file size, e.g., 512M, 10% of RAM +- `swapfc_path`: Location for swap files (default: /swapfc/swapfile) +- `swapfc_free_ram_perc`: Trigger creation when free RAM < % (default: 35) +- `swapfc_use_sparse`: Enable sparse files (default: 0) + +## Dependencies + +- Rust 1.70+ (build time) +- `libsystemd` (for sd_notify integration) +- `btrfs-progs` (for btrfs features) +- `util-linux` (zramctl, losetup, findmnt) + +## Testing Configuration Changes + +After modifying `/etc/systemd/swap.conf`: + +```bash +sudo systemctl restart systemd-swap +journalctl -u systemd-swap -f +systemd-swap status +``` + +## Package Building + +The project uses GitHub Actions to trigger builds on push: +- Webhook dispatches to BigLinux-Package-Build repositories +- Builds for both x86_64 and aarch64 (ARM) + +## Code Style Notes + +- Error handling uses `thiserror` crate for typed errors +- Logging via custom macros: `info!()`, `warn!()`, `error!()`, `debug!()` +- Systemd integration via `libsystemd` crate +- Signal handling for graceful shutdown (SIGTERM, SIGINT) +- Configuration uses lazy string parsing with typed getters diff --git a/Makefile b/Makefile index 949db9b..d3a4fb7 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,6 @@ SVC_T := $(DESTDIR)$(libdir)/systemd/system/systemd-swap.service PRE_SVC_T := $(DESTDIR)$(libdir)/systemd/system/pre-systemd-swap.service DFL_T := $(DESTDIR)$(datadir)/systemd-swap/swap-default.conf CNF_T := $(DESTDIR)$(sysconfdir)/systemd/swap.conf -SYSCTL_T := $(DESTDIR)$(libdir)/sysctl.d/99-systemd-swap.conf MAN5_T := $(DESTDIR)$(mandir)/man5/swap.conf.5 MAN8_T := $(DESTDIR)$(mandir)/man8/systemd-swap.8 @@ -67,9 +66,6 @@ $(DFL_T): include/swap-default.conf $(CNF_T): swap.conf install -p -bDm644 -S .old $< $@ -$(SYSCTL_T): include/99-systemd-swap.conf - install -p -Dm644 $< $@ - $(MAN5_T): man/swap.conf.5 install -p -Dm644 $< $@ @@ -87,7 +83,7 @@ swap.conf: include/swap-default.conf ## Generate swap.conf target/release/systemd-swap: build -files: $(BIN_T) $(PRE_BIN_T) $(SVC_T) $(PRE_SVC_T) $(DFL_T) $(CNF_T) $(SYSCTL_T) $(MAN5_T) $(MAN8_T) +files: $(BIN_T) $(PRE_BIN_T) $(SVC_T) $(PRE_SVC_T) $(DFL_T) $(CNF_T) $(MAN5_T) $(MAN8_T) install: ## Install systemd-swap install: build dirs files @@ -95,7 +91,7 @@ install: build dirs files uninstall: ## Delete systemd-swap (stop systemd-swap first) uninstall: test ! -f /run/systemd/swap/swap.conf - rm -v $(BIN_T) $(PRE_BIN_T) $(SVC_T) $(PRE_SVC_T) $(DFL_T) $(CNF_T) $(SYSCTL_T) $(MAN5_T) $(MAN8_T) + rm -v $(BIN_T) $(PRE_BIN_T) $(SVC_T) $(PRE_SVC_T) $(DFL_T) $(CNF_T) $(MAN5_T) $(MAN8_T) rm -rv $(LIB_T) $(DESTDIR)$(datadir)/systemd-swap clean: ## Remove generated files diff --git a/include/swap-default.conf b/include/swap-default.conf index 93b08de..b2309a6 100644 --- a/include/swap-default.conf +++ b/include/swap-default.conf @@ -57,7 +57,7 @@ swap_mode=auto ## swapfile_enabled=1 # Enable swap file management ## swapfile_path=/swapfile # Base path for swap files -## swapfile_chunk_size=1G # Size of each swap file (larger = less fragmentation) +## swapfile_chunk_size=512M # Size of each swap file ## swapfile_max_count=32 # Maximum number of swap files ################################################################################ diff --git a/src/autoconfig.rs b/src/autoconfig.rs index ecb2f0e..b39c80e 100644 --- a/src/autoconfig.rs +++ b/src/autoconfig.rs @@ -351,11 +351,11 @@ impl RamProfile { pub fn recommended_mglru_min_ttl(&self) -> u32 { match self { RamProfile::UltraLow => 5000, // 5s - maximum protection - RamProfile::Low => 4000, // 4s - RamProfile::Medium => 3000, // 3s - RamProfile::Standard => 3000, // 3s - better desktop protection - RamProfile::High => 2000, // 2s - RamProfile::VeryHigh => 2000, // 2s - workstations can have heavy loads too + RamProfile::Low => 3000, // 3s + RamProfile::Medium => 2000, // 2s + RamProfile::Standard => 1000, // 1s + RamProfile::High => 500, // 0.5s + RamProfile::VeryHigh => 250, // 0.25s - RAM is abundant } } @@ -366,22 +366,6 @@ impl RamProfile { _ => "lz4", } } - - /// Adjust MGLRU based on current memory pressure - /// Call this at daemon startup to adapt to real conditions - pub fn recommended_mglru_with_pressure(&self, pressure: crate::meminfo::MemoryPressure) -> u32 { - use crate::meminfo::MemoryPressure; - - let base = self.recommended_mglru_min_ttl(); - - // Increase protection under pressure - match pressure { - MemoryPressure::Low => base, - MemoryPressure::Medium => base * 15 / 10, // +50% - MemoryPressure::High => base * 2, // +100% - MemoryPressure::Critical => base * 3, // +200% - } - } } /// Full system capabilities @@ -612,7 +596,7 @@ impl RecommendedConfig { zswap_max_pool_percent: 25, // Uniform for all RAM profiles swapfc_enabled: true, swapfc_directio: is_nvme, // Direct I/O only on NVMe - swapfc_chunk_size: if is_nvme { "1G" } else { "512M" }.to_string(), + swapfc_chunk_size: if is_nvme { "512M" } else { "256M" }.to_string(), mglru_min_ttl_ms: ram.recommended_mglru_min_ttl(), } } diff --git a/src/main.rs b/src/main.rs index b7de817..783ab8e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,15 +128,7 @@ fn configure_mglru(config: &Config, recommended: Option<&RecommendedConfig>) { recommended.map(|r| r.mglru_min_ttl_ms).unwrap_or_else(|| { // Fallback: detect RAM and use appropriate value use systemd_swap::autoconfig::RamProfile; - let ram_profile = RamProfile::detect(); - - // Detect memory pressure and adjust MGLRU accordingly - if let Ok(pressure) = systemd_swap::meminfo::get_memory_pressure() { - info!("Memory pressure detected: {:?}, adjusting MGLRU", pressure); - ram_profile.recommended_mglru_with_pressure(pressure) - } else { - ram_profile.recommended_mglru_min_ttl() - } + RamProfile::detect().recommended_mglru_min_ttl() }) }); diff --git a/src/meminfo.rs b/src/meminfo.rs index 88f984f..6a6c562 100644 --- a/src/meminfo.rs +++ b/src/meminfo.rs @@ -108,39 +108,6 @@ pub fn get_cpu_count() -> usize { .unwrap_or(1) } -/// Memory pressure levels -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum MemoryPressure { - Low, // > 40% available - Medium, // 20-40% available - High, // 10-20% available - Critical, // < 10% available -} - -/// Detect current memory pressure based on MemAvailable -pub fn get_memory_pressure() -> Result { - let stats = get_mem_stats(&["MemTotal", "MemAvailable"])?; - let percent = (stats["MemAvailable"] * 100) / stats["MemTotal"]; - - let pressure = match percent { - p if p > 40 => MemoryPressure::Low, - p if p > 20 => MemoryPressure::Medium, - p if p > 10 => MemoryPressure::High, - _ => MemoryPressure::Critical, - }; - - Ok(pressure) -} - -/// Get percentage of available memory (MemAvailable) -/// This is more accurate than MemFree for detecting usable memory -/// as it includes reclaimable cache and buffers -pub fn get_available_ram_percent() -> Result { - let stats = get_mem_stats(&["MemTotal", "MemAvailable"])?; - let percent = (stats["MemAvailable"] * 100) / stats["MemTotal"]; - Ok(percent as u8) -} - /// Zswap statistics from debugfs #[derive(Debug, Default, Clone)] pub struct ZswapStats {