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;
24use std::sync::atomic::{AtomicUsize, Ordering};
25
26/// Lower our own scheduling priority. Unprivileged processes may always lower
27/// (never raise) their own, so this needs no capabilities and cannot fail in a
28/// way worth reporting.
29pub fn set_priority(nice: Option<i32>) {
30    if let Some(n) = nice {
31        unsafe {
32            libc::setpriority(libc::PRIO_PROCESS, 0, n);
33        }
34    }
35}
36
37/// Put our reads in the idle I/O class so they queue behind Apache's.
38///
39/// `ioprio_set` has no libc wrapper on Linux, so it goes through `syscall(2)`
40/// with `SYS_ioprio_set` (30 on aarch64, 251 on x86_64 — `libc` picks the
41/// right one per arch). Arguments are widened to `c_long` explicitly: they
42/// travel through a variadic, and letting a `c_int` be promoted leaves the
43/// upper half of the register unspecified.
44#[cfg(target_os = "linux")]
45pub fn set_io_idle() {
46    const IOPRIO_WHO_PROCESS: libc::c_long = 1;
47    const IOPRIO_CLASS_IDLE: libc::c_long = 3;
48    const IOPRIO_CLASS_SHIFT: libc::c_long = 13;
49    unsafe {
50        libc::syscall(
51            libc::SYS_ioprio_set,
52            IOPRIO_WHO_PROCESS,
53            0 as libc::c_long,
54            IOPRIO_CLASS_IDLE << IOPRIO_CLASS_SHIFT,
55        );
56    }
57}
58
59/// macOS has no I/O scheduling classes; `IOPOL_THROTTLE` on the process-wide
60/// disk I/O policy is the closest analogue.
61#[cfg(target_os = "macos")]
62pub fn set_io_idle() {
63    const IOPOL_TYPE_DISK: libc::c_int = 0;
64    const IOPOL_SCOPE_PROCESS: libc::c_int = 0;
65    const IOPOL_THROTTLE: libc::c_int = 3;
66    unsafe extern "C" {
67        fn setiopolicy_np(
68            iotype: libc::c_int,
69            scope: libc::c_int,
70            policy: libc::c_int,
71        ) -> libc::c_int;
72    }
73    unsafe {
74        setiopolicy_np(IOPOL_TYPE_DISK, IOPOL_SCOPE_PROCESS, IOPOL_THROTTLE);
75    }
76}
77
78#[cfg(not(any(target_os = "linux", target_os = "macos")))]
79pub fn set_io_idle() {}
80
81/// Called immediately after opening an archive, before it is read.
82///
83/// Linux hints sequential readahead; macOS must set `F_NOCACHE` *here* because
84/// it prevents caching rather than evicting afterwards. `drop_cache` is
85/// therefore honoured on macOS at this point and on Linux only in [`finish`].
86///
87/// Callers must pass `drop_cache: false` for plain files — see [`finish`].
88#[cfg(target_os = "linux")]
89pub fn prepare(f: &File, _drop_cache: bool) {
90    use std::os::unix::io::AsRawFd;
91    unsafe {
92        libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_SEQUENTIAL);
93    }
94}
95
96#[cfg(target_os = "macos")]
97pub fn prepare(f: &File, drop_cache: bool) {
98    use std::os::unix::io::AsRawFd;
99    if drop_cache {
100        unsafe {
101            libc::fcntl(f.as_raw_fd(), libc::F_NOCACHE, 1);
102        }
103    }
104}
105
106#[cfg(not(any(target_os = "linux", target_os = "macos")))]
107pub fn prepare(_f: &File, _drop_cache: bool) {}
108
109/// Called after an archive has been fully read.
110///
111/// Only ever called with `drop_cache: true` for archives, never for plain
112/// files: Apache holds live logs open for append, and evicting those pages
113/// would harm the very process this feature exists to protect. Compressed and
114/// tar inputs are cold and ours to drop.
115#[cfg(target_os = "linux")]
116pub fn finish(f: &File, drop_cache: bool) {
117    use std::os::unix::io::AsRawFd;
118    if drop_cache {
119        unsafe {
120            libc::posix_fadvise(f.as_raw_fd(), 0, 0, libc::POSIX_FADV_DONTNEED);
121        }
122    }
123}
124
125#[cfg(not(target_os = "linux"))]
126pub fn finish(_f: &File, _drop_cache: bool) {
127    // macOS did its work in `prepare` via F_NOCACHE; nothing to undo.
128}
129
130/// One-minute load average, from `getloadavg(3)`: `/proc/loadavg` on Linux,
131/// the kernel's own figure on macOS. `None` where there is none.
132#[cfg(any(target_os = "linux", target_os = "macos"))]
133pub fn load_average_1m() -> Option<f64> {
134    let mut avg = [0f64; 1];
135    let got = unsafe { libc::getloadavg(avg.as_mut_ptr(), 1) };
136    (got == 1).then_some(avg[0])
137}
138
139#[cfg(not(any(target_os = "linux", target_os = "macos")))]
140pub fn load_average_1m() -> Option<f64> {
141    None
142}
143
144/// How often the gate is reconsidered. Linux recomputes the load average every
145/// 5 s, so checking more often only reads the same number again.
146const LOAD_CHECK: std::time::Duration = std::time::Duration::from_secs(5);
147
148/// The gate before any worker has started. All of the load is someone else's
149/// at this point, so it is compared as it stands.
150pub fn starting_gate(jobs: usize, limit: f64, load: f64) -> usize {
151    if load > limit { 1 } else { jobs.max(1) }
152}
153
154/// The gate after one more look at the load.
155///
156/// The load average counts trg's own workers too, so up to `gate` of it is
157/// ours and comes off before the comparison. Without that, `-j8 --load-limit
158/// 4` closes the gate on itself on an idle box. Over the limit, it drops
159/// straight to one job: the point of the flag is to get out of Apache's way,
160/// and by the time a 1-minute average shows a spike, the spike has been going
161/// for a while. Under it, it opens one worker per check, because the average
162/// lags in the other direction too, and snapping wide open after a spike
163/// walks straight back into it.
164pub fn next_gate(gate: usize, jobs: usize, load: f64, limit: f64) -> usize {
165    let others = load - gate as f64;
166    if others > limit { 1 } else { (gate + 1).min(jobs).max(1) }
167}
168
169/// Start adapting the concurrency to the load, or `None` when there is nothing
170/// to adapt: no `--load-limit`, a single job, or no load average to read.
171///
172/// The returned gate is what [`crate::sched::run_gated`] admits. A detached
173/// thread moves it every [`LOAD_CHECK`] and stops once nobody else holds the
174/// gate, so it costs one sleeping thread for the length of the run.
175pub fn load_gate(jobs: usize, limit: Option<f64>) -> Option<std::sync::Arc<AtomicUsize>> {
176    let limit = limit?;
177    if jobs <= 1 {
178        return None;
179    }
180    let gate = std::sync::Arc::new(AtomicUsize::new(starting_gate(jobs, limit, load_average_1m()?)));
181    let held = std::sync::Arc::downgrade(&gate);
182    // No thread means no adapting, not no search and not no limit: the gate
183    // stays where it started, which is what the old one-off check did.
184    let _ = std::thread::Builder::new()
185        .name("trg-load".into())
186        .spawn(move || {
187            loop {
188                std::thread::sleep(LOAD_CHECK);
189                let Some(gate) = held.upgrade() else { return };
190                if let Some(load) = load_average_1m() {
191                    let now = gate.load(Ordering::Relaxed);
192                    gate.store(next_gate(now, jobs, load, limit), Ordering::Relaxed);
193                }
194            }
195        });
196    Some(gate)
197}