use std::io::Write;
use super::args::OpMode;
use super::messages::{self as m, printf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Hardware {
pub cpus: u32,
pub physmem: u64,
}
const ASSUME_RAM_MIB: u64 = 128;
pub(crate) const THREADS_MAX: u64 = 16384;
impl Hardware {
pub(crate) const SANDBOX: Hardware = Hardware {
cpus: 2,
physmem: 4 << 30,
};
pub(crate) fn detect() -> Hardware {
Hardware {
cpus: cpucores(),
physmem: physmem(),
}
}
}
#[cfg(unix)]
fn physmem() -> u64 {
let (pagesize, pages) = unsafe {
(
libc::sysconf(libc::_SC_PAGESIZE),
libc::sysconf(libc::_SC_PHYS_PAGES),
)
};
if pagesize != -1 && pages != -1 {
(pagesize as u64).wrapping_mul(pages as u64)
} else {
0
}
}
#[cfg(not(unix))]
fn physmem() -> u64 {
0
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn cpucores() -> u32 {
unsafe {
let mut set: libc::cpu_set_t = std::mem::zeroed();
if libc::sched_getaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &mut set) == 0 {
libc::CPU_COUNT(&set) as u32
} else {
0
}
}
}
#[cfg(target_vendor = "apple")]
fn cpucores() -> u32 {
let mut name = [libc::CTL_HW, libc::HW_NCPU];
let mut cpus: libc::c_int = 0;
let mut size = std::mem::size_of::<libc::c_int>();
let r = unsafe {
libc::sysctl(
name.as_mut_ptr(),
2,
(&mut cpus as *mut libc::c_int).cast(),
&mut size,
std::ptr::null_mut(),
0,
)
};
if r != -1 && size == std::mem::size_of::<libc::c_int>() && cpus > 0 {
cpus as u32
} else {
0
}
}
#[cfg(all(
unix,
not(any(target_os = "linux", target_os = "android", target_vendor = "apple"))
))]
fn cpucores() -> u32 {
let cpus = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) };
if cpus > 0 { cpus as u32 } else { 0 }
}
#[cfg(not(unix))]
fn cpucores() -> u32 {
0
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Limits {
threads_max: u32,
threads_are_automatic: bool,
use_mt_mode_with_one_thread: bool,
memlimit_compress: u64,
memlimit_decompress: u64,
memlimit_mtdec: u64,
memlimit_mt_default: u64,
total_ram: u64,
cputhreads: u32,
decoder_cap: Option<u64>,
}
pub(crate) const SANDBOX_DECODER_CAP: u64 = 256 << 20;
impl Limits {
pub(crate) fn init(hw: Hardware, rlimits: bool, decoder_cap: Option<u64>) -> Limits {
let total_ram = if hw.physmem == 0 {
ASSUME_RAM_MIB * 1024 * 1024
} else {
hw.physmem
};
let mut memlimit_mt_default = total_ram / 4;
if rlimits && let Some(cap) = rlimit_with_margin() {
memlimit_mt_default = memlimit_mt_default.min(cap);
}
if usize::BITS == 32 {
memlimit_mt_default = memlimit_mt_default.min(1400 << 20);
}
let mut l = Limits {
threads_max: 1,
threads_are_automatic: false,
use_mt_mode_with_one_thread: false,
memlimit_compress: 0,
memlimit_decompress: 0,
memlimit_mtdec: 0,
memlimit_mt_default,
total_ram,
cputhreads: hw.cpus,
decoder_cap,
};
l.threads_set(0);
l
}
pub(crate) fn threads_set(&mut self, n: u32) {
self.threads_are_automatic = false;
self.use_mt_mode_with_one_thread = false;
if n == 0 {
self.threads_are_automatic = true;
self.use_mt_mode_with_one_thread = true;
self.threads_max = self.cputhreads.max(1);
} else if n == u32::MAX {
self.use_mt_mode_with_one_thread = true;
self.threads_max = 1;
} else {
self.threads_max = n;
}
}
pub(crate) fn threads_get(&self) -> u32 {
self.threads_max
}
pub(crate) fn threads_is_mt(&self) -> bool {
self.threads_max > 1 || self.use_mt_mode_with_one_thread
}
pub(crate) fn memlimit_set(
&mut self,
value: u64,
compress: bool,
decompress: bool,
mtdec: bool,
percentage: bool,
) {
let mut v = value;
if percentage {
v = v.wrapping_mul(self.total_ram) / 100;
}
if compress {
self.memlimit_compress = v;
if usize::BITS == 32 && v != u64::MAX {
self.memlimit_compress = v.min(4020 << 20);
}
}
if decompress {
self.memlimit_decompress = v;
}
if mtdec {
self.memlimit_mtdec = v;
}
}
pub(crate) fn memlimit_get(&self, mode: OpMode) -> u64 {
let set = if mode == OpMode::Compress {
self.memlimit_compress
} else {
self.memlimit_decompress
};
let limit = if set != 0 { set } else { u64::MAX };
match (mode, self.decoder_cap) {
(OpMode::Compress, _) | (_, None) => limit,
(_, Some(cap)) => limit.min(cap),
}
}
pub(crate) fn memlimit_mtenc_get(&self) -> u64 {
if self.memlimit_mtenc_is_default() {
self.memlimit_mt_default
} else {
self.memlimit_get(OpMode::Compress)
}
}
pub(crate) fn memlimit_mtenc_is_default(&self) -> bool {
self.memlimit_compress == 0 && self.threads_are_automatic
}
pub(crate) fn memlimit_mtdec_get(&self) -> u64 {
let mut v = if self.memlimit_mtdec != 0 {
self.memlimit_mtdec
} else {
self.memlimit_mt_default
};
if self.memlimit_decompress != 0 && v > self.memlimit_decompress {
v = self.memlimit_decompress;
}
match self.decoder_cap {
Some(cap) => v.min(cap),
None => v,
}
}
pub(crate) fn show(&self, robot: bool, out: &mut dyn Write) {
let cputhreads = self.cputhreads.max(1);
let n = |v: u64| v.to_string().into_bytes();
if robot {
printf(
out,
m::INFO_MEMORY_ROBOT,
&[
&n(self.total_ram),
&n(self.memlimit_compress),
&n(self.memlimit_decompress),
&n(self.memlimit_mtdec_get()),
&n(self.memlimit_mt_default),
&n(u64::from(cputhreads)),
],
);
return;
}
let width = m::INFO_LABELS.iter().map(|l| l.len()).max().unwrap_or(1);
let pad = |l: &str| format!("{l:<width$}").into_bytes();
let row = |out: &mut dyn Write, label: &str, value: u64| {
if value == 0 || value == u64::MAX {
printf(out, m::INFO_ROW_DISABLED, &[&pad(label)]);
} else {
printf(
out,
m::INFO_ROW_LIMIT,
&[&pad(label), &n(round_up_to_mib(value)), &n(value)],
);
}
};
printf(out, m::INFO_HARDWARE, &[]);
row(out, m::INFO_LABELS[0], self.total_ram);
printf(
out,
m::INFO_ROW_THREADS,
&[&pad(m::INFO_LABELS[1]), &n(u64::from(cputhreads))],
);
printf(out, m::INFO_LIMITS, &[]);
row(out, m::INFO_LABELS[2], self.memlimit_compress);
row(out, m::INFO_LABELS[3], self.memlimit_decompress);
row(out, m::INFO_LABELS[4], self.memlimit_mtdec_get());
row(out, m::INFO_LABELS[5], self.memlimit_mt_default);
}
}
pub(crate) fn round_up_to_mib(n: u64) -> u64 {
(n >> 20) + u64::from(n & ((1 << 20) - 1) != 0)
}
#[cfg(unix)]
fn rlimit_with_margin() -> Option<u64> {
const MARGIN: u64 = 64 << 20;
let mut lowest: Option<u64> = None;
for resource in [libc::RLIMIT_DATA, libc::RLIMIT_AS] {
let mut rl = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(resource, &mut rl) } != 0 {
continue;
}
if rl.rlim_cur == libc::RLIM_INFINITY {
continue;
}
#[allow(clippy::unnecessary_cast)]
let cur = rl.rlim_cur as u64;
let with_margin = if cur > MARGIN { cur - MARGIN } else { 1 };
lowest = Some(lowest.map_or(with_margin, |l: u64| l.min(with_margin)));
}
lowest
}
#[cfg(not(unix))]
fn rlimit_with_margin() -> Option<u64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
fn limits() -> Limits {
Limits::init(Hardware::SANDBOX, false, None)
}
#[test]
fn the_defaults_follow_hardware_init() {
let l = limits();
assert_eq!(l.threads_get(), 2);
assert!(l.threads_is_mt() && l.memlimit_mtenc_is_default());
assert_eq!(l.memlimit_get(OpMode::Compress), u64::MAX);
assert_eq!(l.memlimit_get(OpMode::Decompress), u64::MAX);
assert_eq!(l.memlimit_mtenc_get(), 1 << 30);
assert_eq!(l.memlimit_mtdec_get(), 1 << 30);
let unknown = Limits::init(
Hardware {
cpus: 0,
physmem: 0,
},
false,
None,
);
assert_eq!(unknown.threads_get(), 1);
assert_eq!(unknown.memlimit_mtenc_get(), 32 << 20);
}
#[test]
fn thread_counts_follow_hardware_threads_set() {
let mut l = limits();
l.threads_set(1);
assert!(!l.threads_is_mt());
l.threads_set(u32::MAX);
assert_eq!((l.threads_get(), l.threads_is_mt()), (1, true));
l.threads_set(4);
assert!(l.threads_is_mt() && !l.memlimit_mtenc_is_default());
}
#[test]
fn percentages_are_of_the_ram_and_mtdec_is_capped() {
let mut l = limits();
l.memlimit_set(50, true, true, true, true);
assert_eq!(l.memlimit_get(OpMode::Compress), 2 << 30);
l.memlimit_set(1 << 20, false, true, false, false);
assert_eq!(l.memlimit_mtdec_get(), 1 << 20);
l.memlimit_set(0, true, true, true, false);
assert_eq!(l.memlimit_get(OpMode::Test), u64::MAX);
}
#[test]
fn the_sandbox_caps_the_decoder() {
let mut l = Limits::init(Hardware::SANDBOX, false, Some(SANDBOX_DECODER_CAP));
assert_eq!(l.memlimit_get(OpMode::Decompress), SANDBOX_DECODER_CAP);
assert_eq!(l.memlimit_get(OpMode::Compress), u64::MAX);
l.memlimit_set(u64::MAX, true, true, true, false);
assert_eq!(l.memlimit_get(OpMode::List), SANDBOX_DECODER_CAP);
assert_eq!(l.memlimit_mtdec_get(), SANDBOX_DECODER_CAP);
l.memlimit_set(1 << 20, false, true, false, false);
assert_eq!(l.memlimit_get(OpMode::Decompress), 1 << 20);
}
#[test]
fn info_memory_renders_hardware_memlimit_show() {
let mut l = limits();
l.memlimit_set(1000, false, true, false, false);
let mut out = Vec::new();
l.show(false, &mut out);
let row = |label: &str, value: &str| format!(" {label:<32} {value}\n");
let want = [
"Hardware information:\n".to_string(),
row(
"Amount of physical memory (RAM):",
"4096 MiB (4294967296 B)",
),
row("Number of processor threads:", "2"),
"\nMemory usage limits:\n".to_string(),
row("Compression:", "Disabled"),
row("Decompression:", "1 MiB (1000 B)"),
row("Multi-threaded decompression:", "1 MiB (1000 B)"),
row("Default for -T0:", "1024 MiB (1073741824 B)"),
]
.concat();
assert_eq!(String::from_utf8(out).unwrap(), want);
let mut out = Vec::new();
l.show(true, &mut out);
assert_eq!(
String::from_utf8(out).unwrap(),
"4294967296\t0\t1000\t1000\t1073741824\t2\n"
);
}
#[cfg(unix)]
#[test]
fn detection_matches_the_reference() {
let exe = std::env::var_os("STUFFR_XZ_REFERENCE")
.filter(|v| !v.is_empty())
.map(std::path::PathBuf::from)
.unwrap_or_else(|| "/opt/homebrew/opt/xz/bin/xz".into());
if !exe.is_file() {
assert!(
std::env::var_os("CI").is_none(),
"CI is set but there is no reference xz at {}",
exe.display()
);
eprintln!("skipped: no reference xz at {}", exe.display());
return;
}
let out = std::process::Command::new(&exe)
.args(["--robot", "--info-memory"])
.env_clear()
.output()
.unwrap();
let text = String::from_utf8(out.stdout).unwrap();
let fields: Vec<&str> = text.trim_end().split('\t').collect();
assert_eq!(fields.len(), 6, "{text:?}");
let hw = Hardware::detect();
assert_eq!(hw.physmem.to_string(), fields[0], "physical memory");
assert_eq!(hw.cpus.max(1).to_string(), fields[5], "threads");
let l = Limits::init(hw, true, None);
assert_eq!(l.memlimit_mt_default.to_string(), fields[4], "-T0 default");
}
}