use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use ignore::{WalkBuilder, WalkState};
use rayon::prelude::*;
pub const EVENT_CHANNEL_CAP: usize = crate::constants::EVENT_CHANNEL_CAP;
pub const RAM_PER_TASK_BYTES: u64 = crate::constants::RAM_PER_TASK_BYTES;
const RAM_SAFETY_FRACTION_NUM: u64 = 1;
const RAM_SAFETY_FRACTION_DEN: u64 = 2;
static CONFIGURED_SIZE: OnceLock<usize> = OnceLock::new();
#[must_use]
pub fn available_cpus() -> usize {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
.max(1)
}
#[must_use]
pub fn free_ram_bytes() -> Option<u64> {
free_ram_bytes_impl()
}
#[cfg(target_os = "linux")]
fn free_ram_bytes_impl() -> Option<u64> {
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
for line in text.lines() {
if let Some(rest) = line.strip_prefix("MemAvailable:") {
let kb: u64 = rest.split_whitespace().next()?.parse().ok()?;
return Some(kb.saturating_mul(1024));
}
}
None
}
#[cfg(not(target_os = "linux"))]
fn free_ram_bytes_impl() -> Option<u64> {
None
}
#[must_use]
pub fn ram_concurrency_cap() -> Option<usize> {
let free = free_ram_bytes()?;
let usable = free.saturating_mul(RAM_SAFETY_FRACTION_NUM) / RAM_SAFETY_FRACTION_DEN;
let cap = (usable / RAM_PER_TASK_BYTES).max(1);
usize::try_from(cap).ok().map(|n| n.max(1))
}
#[must_use]
pub fn effective_threads(cli_threads: Option<usize>) -> usize {
match cli_threads {
Some(0) | None => {
let cpus = available_cpus();
match ram_concurrency_cap() {
Some(ram) => cpus.min(ram).max(1),
None => cpus,
}
}
Some(n) => n.max(1),
}
}
#[must_use]
pub fn should_parallelize(item_count: usize) -> bool {
item_count > 1
}
pub fn apply_walk_threads(builder: &mut WalkBuilder, cli_threads: Option<usize>) {
builder.threads(effective_threads(cli_threads));
}
pub const PAR_SORT_THRESHOLD: usize = 4096;
pub fn sort_paths_parallel(paths: &mut [PathBuf]) {
if paths.len() >= PAR_SORT_THRESHOLD {
paths.par_sort_unstable();
} else {
paths.sort();
}
}
pub fn sort_parallel<T: Ord + Send>(items: &mut [T]) {
if items.len() >= PAR_SORT_THRESHOLD {
items.par_sort_unstable();
} else {
items.sort();
}
}
pub fn sort_by_parallel<T: Send>(items: &mut [T], cmp: impl Fn(&T, &T) -> std::cmp::Ordering + Sync) {
if items.len() >= PAR_SORT_THRESHOLD {
items.par_sort_unstable_by(cmp);
} else {
items.sort_by(cmp);
}
}
include!("concurrency_walk.inc.rs");
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn available_cpus_at_least_one() {
assert!(available_cpus() >= 1);
}
#[test]
fn effective_threads_explicit_honored() {
assert_eq!(effective_threads(Some(1)), 1);
assert_eq!(effective_threads(Some(3)), 3);
}
#[test]
fn effective_threads_zero_uses_cpus() {
let n = effective_threads(Some(0));
assert!(n >= 1);
assert!(n <= available_cpus().max(ram_concurrency_cap().unwrap_or(usize::MAX)));
}
#[test]
fn should_parallelize_threshold() {
assert!(!should_parallelize(0));
assert!(!should_parallelize(1));
assert!(should_parallelize(2));
}
#[test]
fn ram_cap_sane_when_present() {
if let Some(cap) = ram_concurrency_cap() {
assert!(cap >= 1);
}
}
#[test]
fn collect_files_parallel_matches_sequential_build() {
let dir = tempfile::tempdir().expect("tempdir");
for name in ["a.txt", "b.txt", "c.txt"] {
std::fs::write(dir.path().join(name), b"x").expect("write");
}
std::fs::create_dir(dir.path().join("sub")).expect("mkdir");
std::fs::write(dir.path().join("sub/d.txt"), b"y").expect("write");
let mut builder = WalkBuilder::new(dir.path());
builder.hidden(false).git_ignore(false);
apply_walk_threads(&mut builder, Some(2));
let mut sequential = Vec::new();
for entry in builder.build().flatten() {
if entry.file_type().is_some_and(|ft| ft.is_file()) {
sequential.push(entry.into_path());
}
}
sequential.sort();
let parallel = collect_files_parallel(&builder);
assert_eq!(
parallel, sequential,
"parallel collect must match sequential file set"
);
assert_eq!(parallel.len(), 4);
}
#[test]
fn collect_mapped_parallel_includes_dirs() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("f.txt"), b"x").expect("write");
std::fs::create_dir(dir.path().join("sub")).expect("mkdir");
let mut builder = WalkBuilder::new(dir.path());
builder.hidden(false).git_ignore(false);
apply_walk_threads(&mut builder, Some(2));
let mut kinds: Vec<String> = collect_mapped_parallel(&builder, |e| {
let ft = e.file_type()?;
let kind = if ft.is_dir() {
"dir"
} else if ft.is_file() {
"file"
} else {
"other"
};
Some(kind.to_string())
});
kinds.sort();
assert!(kinds.iter().any(|k| k == "dir"));
assert!(kinds.iter().any(|k| k == "file"));
}
#[test]
fn collect_mapped_parallel_budgeted_respects_cap() {
let dir = tempfile::tempdir().expect("tempdir");
for i in 0..20 {
std::fs::write(dir.path().join(format!("f{i}.txt")), b"x").expect("write");
}
let mut builder = WalkBuilder::new(dir.path());
builder.hidden(false).git_ignore(false);
apply_walk_threads(&mut builder, Some(2));
let (paths, truncated) = collect_mapped_parallel_budgeted(&builder, 5, |e| {
if e.file_type().is_some_and(|ft| ft.is_file()) {
Some(e.path().to_path_buf())
} else {
None
}
});
assert!(truncated, "budget of 5 over 20 files must truncate");
assert!(
paths.len() <= 8,
"budgeted collect should not wildly overshoot (got {})",
paths.len()
);
assert!(!paths.is_empty());
}
#[test]
fn sort_paths_parallel_orders_small_lists() {
let mut paths = vec![
PathBuf::from("c"),
PathBuf::from("a"),
PathBuf::from("b"),
];
sort_paths_parallel(&mut paths);
assert_eq!(
paths,
vec![PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")]
);
}
#[test]
fn configure_global_pool_records_size() {
configure_global_pool(Some(2));
let n = configured_pool_size();
assert!(n.is_some(), "pool size must be recorded");
assert!(n.unwrap() >= 1);
assert!(effective_threads(Some(2)) == 2);
}
#[test]
fn effective_threads_never_exceeds_explicit_cap() {
let n = effective_threads(Some(1));
assert_eq!(n, 1, "peak workers must not exceed operator --threads 1");
}
}