Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,3 @@ cython_debug/

# PyPI configuration file
.pypirc

# Claude Code
CLAUDE.md
.claude/
208 changes: 208 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
8 changes: 2 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 $< $@

Expand All @@ -87,15 +83,15 @@ 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

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
Expand Down
2 changes: 1 addition & 1 deletion include/swap-default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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

################################################################################
Expand Down
28 changes: 6 additions & 22 deletions src/autoconfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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(),
}
}
Expand Down
10 changes: 1 addition & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
});

Expand Down
33 changes: 0 additions & 33 deletions src/meminfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MemoryPressure> {
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<u8> {
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 {
Expand Down