use std::num::{NonZeroU64, NonZeroUsize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Threads {
#[default]
Auto,
Count(NonZeroUsize),
Single,
}
impl Threads {
pub fn count_or_single(n: usize) -> Self {
match NonZeroUsize::new(n) {
Some(count) => Self::Count(count),
None => Self::Single,
}
}
pub fn count(&self) -> usize {
match self {
Self::Auto => std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
Self::Count(n) => n.get(),
Self::Single => 1,
}
}
pub fn is_single(&self) -> bool {
self.count() == 1
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MemoryLimit {
#[default]
Auto,
Bytes(NonZeroU64),
}
const DEFAULT_MEMORY_LIMIT: u64 = 512 * 1024 * 1024;
const MINIMUM_MEMORY_LIMIT: u64 = 16 * 1024 * 1024;
impl MemoryLimit {
pub fn bytes_or_auto(bytes: u64) -> Self {
match NonZeroU64::new(bytes) {
Some(bytes) => Self::Bytes(bytes),
None => Self::Auto,
}
}
pub fn bytes(&self) -> u64 {
match self {
Self::Auto => Self::detected(),
Self::Bytes(bytes) => bytes.get().max(MINIMUM_MEMORY_LIMIT),
}
}
fn detected() -> u64 {
static DETECTED: std::sync::OnceLock<u64> = std::sync::OnceLock::new();
*DETECTED.get_or_init(Self::detect_once)
}
fn detect_once() -> u64 {
#[cfg(feature = "sysinfo")]
{
use sysinfo::System;
let mut system = System::new();
system.refresh_memory();
let total = system.total_memory();
if total == 0 {
return Self::budget_for(0, 0);
}
let available = match System::cgroup_limits(&system) {
Some(limits) => limits.free_memory.min(system.available_memory()),
None => system.available_memory(),
};
Self::budget_for(available, total)
}
#[cfg(not(feature = "sysinfo"))]
Self::budget_for(0, 0)
}
fn budget_for(available: u64, total: u64) -> u64 {
if available > 0 {
return (available / 4).max(MINIMUM_MEMORY_LIMIT);
}
if total > 0 {
return MINIMUM_MEMORY_LIMIT;
}
DEFAULT_MEMORY_LIMIT
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_threads_count_never_zero() {
assert!(Threads::Auto.count() >= 1);
assert_eq!(Threads::Single.count(), 1);
assert_eq!(Threads::count_or_single(0), Threads::Single);
assert_eq!(Threads::count_or_single(7).count(), 7);
}
#[test]
fn test_threads_is_single() {
assert!(Threads::Single.is_single());
assert!(Threads::count_or_single(1).is_single());
assert!(!Threads::count_or_single(4).is_single());
}
#[test]
fn test_memory_limit_has_a_floor() {
let tiny = MemoryLimit::bytes_or_auto(1);
assert_eq!(tiny.bytes(), MINIMUM_MEMORY_LIMIT);
}
#[test]
fn test_memory_limit_zero_means_auto() {
assert_eq!(MemoryLimit::bytes_or_auto(0), MemoryLimit::Auto);
assert!(MemoryLimit::Auto.bytes() >= MINIMUM_MEMORY_LIMIT);
}
#[test]
fn test_detected_limit_never_grows_as_memory_shrinks() {
const TOTAL: u64 = 8 * 1024 * 1024 * 1024;
let mut previous = u64::MAX;
for available in [
TOTAL,
1024 * 1024 * 1024,
256 * 1024 * 1024,
64 * 1024 * 1024,
63 * 1024 * 1024,
16 * 1024 * 1024,
1,
0,
] {
let budget = MemoryLimit::budget_for(available, TOTAL);
assert!(
budget <= previous,
"{available} bytes free yielded {budget}, more than the \
{previous} allowed with more memory",
);
assert!(budget >= MINIMUM_MEMORY_LIMIT, "below the floor");
previous = budget;
}
}
#[test]
fn test_the_default_is_only_for_an_unanswerable_machine() {
assert_eq!(MemoryLimit::budget_for(0, 0), DEFAULT_MEMORY_LIMIT);
assert_eq!(
MemoryLimit::budget_for(0, 8 * 1024 * 1024 * 1024),
MINIMUM_MEMORY_LIMIT
);
}
#[test]
fn test_memory_limit_honours_an_explicit_value() {
let limit = MemoryLimit::bytes_or_auto(256 * 1024 * 1024);
assert_eq!(limit.bytes(), 256 * 1024 * 1024);
}
}