Skip to main content

trg/
resource.rs

1//! Staying out of the way on a loaded production server.
2//!
3//! Priority, not thread starvation, is the primary lever: a nice'd worker uses
4//! idle capacity and is preempted the instant Apache wants the core. Measured
5//! separately on 610 MB of real archives: `FADV_DONTNEED` takes page-cache
6//! residency from 610 MB to 0 MB at no cost in wall time or CPU (33.68 s vs
7//! 34.14 s, within noise), where `zgrep -a`, `rg -za` and `rg -za -j1` all
8//! leave the full 610 MB resident and evict whatever Apache and MariaDB had
9//! warm.
10//!
11//! **The platform asymmetry is real and is not papered over.** `FADV_DONTNEED`
12//! evicts pages *after* reading; macOS has no `posix_fadvise` at all and its
13//! nearest equivalent, `fcntl(fd, F_NOCACHE, 1)`, prevents caching *before*
14//! reading. So there are two hooks — [`prepare`] at open and [`finish`] after
15//! the archive has been read — and each platform does its real work in a
16//! different one:
17//!
18//! | | Linux | macOS |
19//! |---|---|---|
20//! | [`prepare`] | `FADV_SEQUENTIAL` (readahead hint) | `F_NOCACHE` (the actual work) |
21//! | [`finish`]  | `FADV_DONTNEED` (the actual work)  | no-op |
22
23use std::fs::File;
24
25/// Lower our own scheduling priority. Unprivileged processes may always lower
26/// (never raise) their own, so this needs no capabilities and cannot fail in a
27/// way worth reporting.
28pub fn set_priority(nice: Option<i32>) {
29    if let Some(n) = nice {
30        unsafe {
31            libc::setpriority(libc::PRIO_PROCESS, 0, n);
32        }
33    }
34}
35
36/// Put our reads in the idle I/O class so they queue behind Apache's.
37///
38/// `ioprio_set` has no libc wrapper on Linux, so it goes through `syscall(2)`
39/// with `SYS_ioprio_set` (30 on aarch64, 251 on x86_64 — `libc` picks the
40/// right one per arch). Arguments are widened to `c_long` explicitly: they
41/// travel through a variadic, and letting a `c_int` be promoted leaves the
42/// upper half of the register unspecified.
43#[cfg(target_os = "linux")]
44pub fn set_io_idle() {
45    const IOPRIO_WHO_PROCESS: libc::c_long = 1;
46    const IOPRIO_CLASS_IDLE: libc::c_long = 3;
47    const IOPRIO_CLASS_SHIFT: libc::c_long = 13;
48    unsafe {
49        libc::syscall(
50            libc::SYS_ioprio_set,
51            IOPRIO_WHO_PROCESS,
52            0 as libc::c_long,
53            IOPRIO_CLASS_IDLE << IOPRIO_CLASS_SHIFT,
54        );
55    }
56}
57
58/// macOS has no I/O scheduling classes; `IOPOL_THROTTLE` on the process-wide
59/// disk I/O policy is the closest analogue.
60#[cfg(target_os = "macos")]
61pub fn set_io_idle() {
62    const IOPOL_TYPE_DISK: libc::c_int = 0;
63    const IOPOL_SCOPE_PROCESS: libc::c_int = 0;
64    const IOPOL_THROTTLE: libc::c_int = 3;
65    unsafe extern "C" {
66        fn setiopolicy_np(
67            iotype: libc::c_int,
68            scope: libc::c_int,
69            policy: libc::c_int,
70        ) -> libc::c_int;
71    }
72    unsafe {
73        setiopolicy_np(IOPOL_TYPE_DISK, IOPOL_SCOPE_PROCESS, IOPOL_THROTTLE);
74    }
75}
76
77#[cfg(not(any(target_os = "linux", target_os = "macos")))]
78pub fn set_io_idle() {}
79
80/// Called immediately after opening an archive, before it is read.
81///
82/// Linux hints sequential readahead; macOS must set `F_NOCACHE` *here* because
83/// it prevents caching rather than evicting afterwards. `drop_cache` is
84/// therefore honoured on macOS at this point and on Linux only in [`finish`].
85///
86/// Callers must pass `drop_cache: false` for plain files — see [`finish`].
87#[cfg(target_os = "linux")]
88pub fn prepare(f: &File, _drop_cache: bool) {
89    use std::os::unix::io::AsRawFd;
90    unsafe {
91        libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
92    }
93}
94
95#[cfg(target_os = "macos")]
96pub fn prepare(f: &File, drop_cache: bool) {
97    use std::os::unix::io::AsRawFd;
98    if drop_cache {
99        unsafe {
100            libc::fcntl(f.as_raw_fd(), libc::F_NOCACHE, 1);
101        }
102    }
103}
104
105#[cfg(not(any(target_os = "linux", target_os = "macos")))]
106pub fn prepare(_f: &File, _drop_cache: bool) {}
107
108/// Called after an archive has been fully read.
109///
110/// Only ever called with `drop_cache: true` for archives, never for plain
111/// files: Apache holds live logs open for append, and evicting those pages
112/// would harm the very process this feature exists to protect. Compressed and
113/// tar inputs are cold and ours to drop.
114#[cfg(target_os = "linux")]
115pub fn finish(f: &File, drop_cache: bool) {
116    use std::os::unix::io::AsRawFd;
117    if drop_cache {
118        unsafe {
119            libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_DONTNEED);
120        }
121    }
122}
123
124#[cfg(not(target_os = "linux"))]
125pub fn finish(_f: &File, _drop_cache: bool) {
126    // macOS did its work in `prepare` via F_NOCACHE; nothing to undo.
127}
128
129/// One-minute load average, Linux only. Deliberately read once at startup —
130/// no feedback loop.
131pub fn load_average_1m() -> Option<f64> {
132    let s = std::fs::read_to_string("/proc/loadavg").ok()?;
133    s.split_whitespace().next()?.parse().ok()
134}
135
136/// Clamp the concurrency to 1 when the box is already busier than the operator
137/// said they would tolerate. No source of load (anything but Linux) means no
138/// clamping, never a guess.
139pub fn clamp_jobs(jobs: usize, limit: Option<f64>) -> usize {
140    // Match on `limit` first. Matching on the tuple would evaluate
141    // `load_average_1m()` eagerly, reading `/proc/loadavg` on every startup
142    // even when `--load-limit` was never given.
143    let Some(max) = limit else { return jobs };
144    match load_average_1m() {
145        Some(now) if now > max => 1,
146        _ => jobs,
147    }
148}