use log::{debug, warn};
use memmap2::Mmap;
use rayon::iter::IntoParallelRefIterator;
use rayon::prelude::*;
use crate::config::RuntimeConfig;
use crate::parsers::{FileType, ParseResult};
use crate::utils::path_string_helper::format_bytes;
#[derive(Debug)]
pub struct ProcessingTask {
pub stats: ParseResult,
pub output_path: String,
pub mmap: Option<Mmap>,
}
#[derive(Debug, Clone)]
pub struct AdaptiveChunking {
pub chunks_per_file_multiplier: usize,
}
#[must_use]
pub fn optimal_chunk_size(
collection_size: usize,
target_chunks: usize,
min_collection_size_for_chunking: usize,
) -> usize {
if collection_size < min_collection_size_for_chunking || target_chunks == 0 {
return 1;
}
let chunk_size = collection_size / target_chunks.max(1);
chunk_size.max(1)
}
#[must_use]
pub fn calculate_adaptive_chunking(
tasks: &[ProcessingTask],
max_workers: usize,
config: &RuntimeConfig,
) -> AdaptiveChunking {
let num_files = tasks.len();
let byte_counts: Vec<usize> = tasks.iter().map(|t| t.stats.byte_count).collect();
let total_bytes: usize = byte_counts.iter().sum();
let mean_bytes = total_bytes.checked_div(num_files).unwrap_or(0);
let variance: f64 = if num_files > 0 {
byte_counts
.iter()
.map(|&bytes| {
let diff = bytes as f64 - mean_bytes as f64;
diff * diff
})
.sum::<f64>()
/ num_files as f64
} else {
0.0
};
let std_dev = variance.sqrt();
let coefficient_of_variation = if mean_bytes > 0 {
std_dev / mean_bytes as f64
} else {
0.0
};
let chunks_per_worker_multiplier = if num_files == 1 {
1
} else if tasks
.iter()
.all(|t| t.stats.file_type == FileType::Image || t.stats.file_type == FileType::Audio)
{
1
} else {
let small_threshold = config.small_file_threshold_bytes;
let large_threshold = config.large_file_threshold_bytes;
match (mean_bytes, coefficient_of_variation > 0.5) {
(bytes, _) if bytes < small_threshold => 1,
(bytes, high_variance) if bytes < large_threshold => {
if high_variance {
1 } else {
2
}
}
(_, high_variance) => {
if high_variance {
2 } else {
3
}
}
}
};
let target_chunks = chunks_per_worker_multiplier * max_workers;
let bytes_per_chunk = mean_bytes.checked_div(target_chunks).unwrap_or(0);
debug!(
"Adaptive chunking: {} files, mean={}, cv={:.2}, multiplier={}, target_chunks={}, bytes_per_chunk={}",
num_files,
format_bytes(mean_bytes),
coefficient_of_variation,
chunks_per_worker_multiplier,
target_chunks,
format_bytes(bytes_per_chunk)
);
AdaptiveChunking {
chunks_per_file_multiplier: chunks_per_worker_multiplier,
}
}
pub fn setup_rayon_thread_pool(max_workers: usize) {
rayon::ThreadPoolBuilder::new()
.num_threads(max_workers)
.build_global()
.unwrap_or_else(|_| {
warn!("Failed to set thread pool, using default");
});
}
const RESERVED_FDS: usize = 256;
const MAX_AUTO_BATCH: usize = 100_000;
pub const DEFAULT_AUTO_BATCH: usize = 10_000;
#[must_use]
pub fn fd_limit_batch_size() -> Option<usize> {
#[cfg(unix)]
{
let (soft, _hard) = rlimit::getrlimit(rlimit::Resource::NOFILE).ok()?;
let limit = if soft == rlimit::INFINITY || soft > MAX_AUTO_BATCH as u64 {
MAX_AUTO_BATCH as u64
} else {
soft
};
let batch_u64 = limit.saturating_sub(RESERVED_FDS as u64);
let batch = usize::try_from(batch_u64).unwrap_or(MAX_AUTO_BATCH);
Some(batch.max(1))
}
#[cfg(windows)]
{
let limit = rlimit::getmaxstdio();
if limit == 0 {
return Some(DEFAULT_AUTO_BATCH);
}
let batch = (limit as usize).saturating_sub(RESERVED_FDS).max(1);
Some(batch.min(MAX_AUTO_BATCH))
}
}
#[derive(Debug, Clone, Copy)]
pub enum PathBatchMode {
Single,
Batched { batch_size: usize },
}
#[must_use]
pub fn determine_batch_size(input_paths: &[String]) -> PathBatchMode {
let batch_size = fd_limit_batch_size().unwrap_or(DEFAULT_AUTO_BATCH);
if input_paths.len() > batch_size {
debug!(
"Path batch size: {} (path count {}), running in batches",
batch_size,
input_paths.len()
);
PathBatchMode::Batched { batch_size }
} else {
PathBatchMode::Single
}
}
pub fn process_files_with_adaptive_batching<R, F>(
tasks: &[ProcessingTask],
max_workers: usize,
threshold_multiplier: usize,
f: F,
) -> Vec<R>
where
F: Fn(&ProcessingTask) -> R + Send + Sync,
R: Send,
{
let batching_threshold = max_workers * threshold_multiplier;
if tasks.len() > batching_threshold {
let ratio = tasks.len() as f64 / batching_threshold as f64;
let batch_multiplier = if ratio < 2.0 {
2 } else if ratio < 3.0 {
3 } else {
4 };
let batch_size = (max_workers * batch_multiplier).max(1);
debug!(
"Adaptive batching: files={}, threshold={}, ratio={:.2}, batch_multiplier={}, batch_size={}",
tasks.len(),
batching_threshold,
ratio,
batch_multiplier,
batch_size
);
tasks.par_iter().with_min_len(batch_size).map(f).collect()
} else {
tasks.par_iter().map(f).collect()
}
}