# pagers
[](https://github.com/LilDojd/pagers/actions/workflows/ci.yml)
[](https://crates.io/crates/pagers)
[](https://codecov.io/gh/LilDojd/pagers)
Portable file system page cache diagnostics and control for Linux and macOS.
`pagers` queries, touches, evicts, and locks the page cache using `cachestat(2)`, `mincore(2)`, `posix_fadvise(2)`, and `mlock(2)`. When stdout is a terminal it renders a live TUI with per-file residency maps. When piped, it emits plain text, key=value pairs, or JSON.
## Install
From [crates.io](https://crates.io/crates/pagers):
```
cargo install pagers
```
From source:
```
cargo install --path crates/pagers-cli
```
With Nix:
```
nix profile install github:LilDojd/pagers
```
..or from [FlakeHub](https://flakehub.com):
```
nix profile install "https://flakehub.com/f/LilDojd/pagers/*"
```
Pre-built binaries for each release are available on the [GitHub releases page](https://github.com/LilDojd/pagers/releases).
### Docker
```
docker pull ghcr.io/lildojd/pagers:latest
docker run --rm -v /data:/data ghcr.io/lildojd/pagers query /data
```
MSRV: **1.88.0**
## Usage
```sh
# How much of /var/db is cached?
pagers query /var/db
# Load a database file into cache before starting your app
pagers touch /var/lib/mysql/ibdata1
# Evict log files to free memory for active data
pagers evict /var/log
# Lock critical files in RAM so they are never paged out
pagers lock -d /var/lib/redis/dump.rdb
# Lock files and the entire current address space
pagers lockall -d /data
# Machine-readable output
# Process a list of paths from find(1)
find /srv -name '*.db' -print0 | pagers query -b - -0
# Only consider .dat files under 500M, in the first 1G of each file
pagers query -I '*.dat' -m 500M -p 0..1G /data
```
## Subcommands
| `query` | Show which pages of a file are in the page cache |
| `touch` | Load pages into the page cache |
| `evict` | Drop pages from the page cache |
| `lock` | Touch + `mlock(2)` pages in physical memory |
| `lockall` | Lock + `mlockall(MCL_CURRENT)` |
## Options
All subcommands accept these flags:
```
PATHS Files or directories to process
-f Follow symbolic links
-F Stay on the same filesystem
-H Count hardlinked copies separately
-m, --max-file-size SIZE
Skip files larger than SIZE (e.g. 4k, 100M, 1.5G)
-p, --range RANGE Byte range to operate on (e.g. 10K-20G, 100M..500M, 0,1G)
-i, --ignore GLOB Ignore files matching pattern (repeatable)
-I, --filter GLOB Only process files matching pattern (repeatable)
-b, --batch FILE Read paths from FILE (- for stdin)
-0 NUL-delimited paths in batch mode
-o, --output FMT Output format: human (default), kv, json
-v Increase verbosity (repeatable)
-q Quiet (no output)
```
`lock` and `lockall` also accept:
```
-d, --daemon Run as a daemon (block until signal)
--wait Wait until all pages are locked (requires -d)
-P, --pidfile PATH Write PID to file
```
### Size and range syntax
Sizes accept decimal and binary units: `4k`, `100M`, `1.5G`, `2KiB`, `8MiB`. Scientific notation (`1e2K`) and fractional values (`1.5G`) work too. Ranges can be written as `10K-20G`, `10K..20G`, or `10K,20G`. Open-ended ranges like `-20G` (from start) and `10K-` (to end) are supported.
## How it works
**`query`** uses the `cachestat(2)` syscall on Linux 6.5+ for single-syscall cache stats per file, falling back to `mincore(2)` on older kernels and on macOS. Returns per-file page residency maps and aggregate statistics.
**`touch`** issues `posix_fadvise(POSIX_FADV_SEQUENTIAL | POSIX_FADV_WILLNEED)` to kick off kernel readahead, then walks every page with volatile reads to guarantee residency. Pages already loaded by the kernel are instant cache hits; the rest trigger demand faults.
**`evict`** calls `posix_fadvise(POSIX_FADV_DONTNEED)` on Linux and `msync(MS_INVALIDATE)` on macOS to advise the kernel to drop cached pages.
**`lock`** touches pages into cache and then calls `mlock(2)` to wire them into physical memory. **`lockall`** additionally calls `mlockall(MCL_CURRENT)` after locking individual files.
Files are traversed in parallel using [rayon](https://github.com/rayon-rs/rayon). Memory-mapped I/O is handled by [memmap2](https://github.com/RazrFalcon/memmap2-rs). The live TUI is built with [ratatui](https://github.com/ratatui/ratatui).
## Use cases
- **Database warm-up.** `pagers touch /var/lib/postgresql/data` before starting a service to avoid cold-start I/O.
- **Deployment pre-warming.** Touch shared libraries or application binaries after deploy so the first request does not pay the page-fault penalty.
- **Memory reclamation.** `pagers evict /var/log` to return pages used by rotated logs back to the free pool.
- **Pinning critical data.** `pagers lock -d /var/lib/redis/dump.rdb` keeps a dataset in RAM for the lifetime of the daemon process.
- **Cache auditing.** `pagers query -o json /data` for monitoring scripts that track page cache residency over time.
- **CI/benchmarking.** Evict file caches between benchmark runs for repeatable cold-start measurements.
- **Batch processing.** Pipe paths from `find(1)` or a manifest file via `-b -` to operate on arbitrary file sets.
## Comparison with vmtouch
| Language | C | Rust |
| Platforms | Linux, FreeBSD, Solaris, macOS, HP-UX, OpenBSD | Linux, macOS\* |
| Cache query | `mincore(2)` | `cachestat(2)` on Linux 6.5+, `mincore(2)` fallback |
| Live TUI | Sort of | Yes |
| Daemon mode | `-d` (requires `-l`/`-L`) | `-d` for `lock` and `lockall` |
| Parallel traversal | No | Yes (rayon) |
| Range operations | `-p` page ranges | `-p` byte ranges with unit suffixes |
### Performance
#### macOS (M1)
Query and evict perform on par with vmtouch. Touch is **2–6x faster** thanks to parallel traversal:
| Query 10 GiB file | 178 ms | 178 ms | 1.0x |
| Evict 10 GiB file | 207 ms | 208 ms | 1.0x |
| Touch 10 GiB file | 14.0 s | 5.1 s | **2.7x** |
| Touch 1000 × 1 MiB files | 1.51 s | 239 ms | **6.3x** |
| Evict 1000 × 1 MiB files | 49.7 ms | 18.9 ms | **2.6x** |
#### Linux (x86_64, kernel 6.19)
Query is **2–10x faster** thanks to `cachestat(2)`. Touch is **~2x faster** on directory trees via parallel traversal:
| Query 10 GiB cached | 42 ms | 18 ms | **2.3x** |
| Query 10 GiB uncached | 16 ms | 1.5 ms | **10.6x** |
| Evict 10 GiB file | 1.15 s | 1.16 s | 1.0x |
| Touch 10 GiB file | 1.16 s | 1.19 s | 1.0x |
| Touch 1000 × 1 MiB files | 173 ms | 90 ms | **1.9x** |
## See also
- [vmtouch](https://hoytech.com/vmtouch/) — the og page cache control tool
- [cachestat(2)](https://man7.org/linux/man-pages/man2/cachestat.2.html) — Linux 6.5+ syscall for page cache statistics
- [mincore(2)](https://man7.org/linux/man-pages/man2/mincore.2.html) — determine whether pages are resident in memory
- [posix_fadvise(2)](https://man7.org/linux/man-pages/man2/posix_fadvise.2.html) — predeclare an access pattern for file data
- [mlock(2)](https://man7.org/linux/man-pages/man2/mlock.2.html) — lock pages in memory