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}