pub fn collect_mapped_parallel<T, F>(builder: &WalkBuilder, map: F) -> Vec<T>
where
T: Send + 'static,
F: Fn(&ignore::DirEntry) -> Option<T> + Sync + Send + 'static,
{
collect_mapped_parallel_tallied(builder, map, None)
}
pub fn collect_mapped_parallel_tallied<T, F>(
builder: &WalkBuilder,
map: F,
failures: Option<std::sync::Arc<Mutex<crate::ndjson_types::FailureTally>>>,
) -> Vec<T>
where
T: Send + 'static,
F: Fn(&ignore::DirEntry) -> Option<T> + Sync + Send + 'static,
{
use std::sync::Arc;
let shards: Arc<Mutex<Vec<Vec<T>>>> = Arc::new(Mutex::new(Vec::new()));
let map = Arc::new(map);
struct ShardGuard<T> {
local: Vec<T>,
shards: Arc<Mutex<Vec<Vec<T>>>>,
}
impl<T> Drop for ShardGuard<T> {
fn drop(&mut self) {
let batch = std::mem::take(&mut self.local);
if batch.is_empty() {
return;
}
if let Ok(mut guard) = self.shards.lock() {
guard.push(batch);
}
}
}
builder.build_parallel().run(|| {
let mut guard = ShardGuard {
local: Vec::new(),
shards: Arc::clone(&shards),
};
let map = Arc::clone(&map);
let failures = failures.clone();
Box::new(move |result| {
if crate::signal::is_global_shutdown() || crate::output::limit_reached() {
return WalkState::Quit;
}
let entry = match result {
Ok(e) => e,
Err(e) => {
if let Some(ref tally) = failures
&& let Ok(mut tally) = tally.lock()
{
let (path, err) = classify_walk_error(&e);
tally.record(&path, err);
}
return WalkState::Continue;
}
};
if let Some(item) = map(&entry) {
guard.local.push(item);
}
WalkState::Continue
})
});
match Arc::try_unwrap(shards) {
Ok(mutex) => mutex.into_inner().unwrap_or_default(),
Err(arc) => arc
.lock()
.map(|mut g| std::mem::take(&mut *g))
.unwrap_or_default(),
}
.into_iter()
.flatten()
.collect()
}
pub fn walk_failure(err: &ignore::Error, path: &std::path::Path) -> anyhow::Error {
match crate::error::AtomwriteError::from_walk_error(err) {
Some((_, error)) => error.into(),
None => crate::error::AtomwriteError::from_io_at(
path,
std::io::Error::other(err.to_string()),
)
.into(),
}
}
pub const WALK_ERROR_PATH: &str = "<walk>";
#[must_use]
pub fn classify_walk_error(err: &ignore::Error) -> (std::path::PathBuf, anyhow::Error) {
match crate::error::AtomwriteError::from_walk_error(err) {
Some((path, error)) if !path.as_os_str().is_empty() => (path, error.into()),
Some((_, error)) => (std::path::PathBuf::from(WALK_ERROR_PATH), error.into()),
None => {
let path = std::path::PathBuf::from(WALK_ERROR_PATH);
let error = walk_failure(err, &path);
(path, error)
}
}
}
fn ignore_error_is_io(err: &ignore::Error) -> bool {
match err {
ignore::Error::Io(_) => true,
ignore::Error::Partial(errs) => errs.iter().any(ignore_error_is_io),
ignore::Error::WithLineNumber { err, .. }
| ignore::Error::WithPath { err, .. }
| ignore::Error::WithDepth { err, .. } => ignore_error_is_io(err),
_ => false,
}
}
#[must_use]
pub fn ignore_file_failures(
roots: &[std::path::PathBuf],
) -> Vec<(std::path::PathBuf, anyhow::Error)> {
let mut found = Vec::new();
for root in roots {
for name in [".gitignore", ".ignore"] {
let path = root.join(name);
if !path.is_file() {
continue;
}
let mut builder = ignore::gitignore::GitignoreBuilder::new(root);
let Some(err) = builder.add(&path) else {
continue;
};
let error: anyhow::Error = if ignore_error_is_io(&err) {
crate::error::AtomwriteError::from_io_at(
&path,
std::io::Error::other(err.to_string()),
)
.into()
} else {
crate::error::AtomwriteError::InvalidInput {
reason: format!("{}: {err}", path.display()),
}
.into()
};
found.push((path, error));
}
}
found
}
pub fn record_ignore_file_failures(
roots: &[std::path::PathBuf],
failures: Option<&std::sync::Arc<Mutex<crate::ndjson_types::FailureTally>>>,
) {
let Some(tally) = failures else {
return;
};
for (path, error) in ignore_file_failures(roots) {
if let Ok(mut tally) = tally.lock() {
tally.record(&path, error);
}
}
}
pub fn record_ignore_file_failures_into(
roots: &[std::path::PathBuf],
tally: &mut crate::ndjson_types::FailureTally,
) {
for (path, error) in ignore_file_failures(roots) {
tally.record(&path, error);
}
}
pub fn collect_mapped_parallel_budgeted<T, F>(
builder: &WalkBuilder,
max_items: u64,
map: F,
) -> (Vec<T>, bool)
where
T: Send + 'static,
F: Fn(&ignore::DirEntry) -> Option<T> + Sync + Send + 'static,
{
collect_mapped_parallel_budgeted_tallied(builder, max_items, map, None)
}
pub fn collect_mapped_parallel_budgeted_tallied<T, F>(
builder: &WalkBuilder,
max_items: u64,
map: F,
failures: Option<std::sync::Arc<Mutex<crate::ndjson_types::FailureTally>>>,
) -> (Vec<T>, bool)
where
T: Send + 'static,
F: Fn(&ignore::DirEntry) -> Option<T> + Sync + Send + 'static,
{
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
if max_items == 0 {
return (Vec::new(), true);
}
let accepted = Arc::new(AtomicU64::new(0));
let hit_budget = Arc::new(AtomicBool::new(false));
let shards: Arc<Mutex<Vec<Vec<T>>>> = Arc::new(Mutex::new(Vec::new()));
let map = Arc::new(map);
struct ShardGuard<T> {
local: Vec<T>,
shards: Arc<Mutex<Vec<Vec<T>>>>,
}
impl<T> Drop for ShardGuard<T> {
fn drop(&mut self) {
let batch = std::mem::take(&mut self.local);
if batch.is_empty() {
return;
}
if let Ok(mut guard) = self.shards.lock() {
guard.push(batch);
}
}
}
builder.build_parallel().run(|| {
let mut guard = ShardGuard {
local: Vec::new(),
shards: Arc::clone(&shards),
};
let map = Arc::clone(&map);
let accepted = Arc::clone(&accepted);
let hit_budget = Arc::clone(&hit_budget);
let failures = failures.clone();
Box::new(move |result| {
if crate::signal::is_global_shutdown() || crate::output::limit_reached() {
return WalkState::Quit;
}
if hit_budget.load(Ordering::Relaxed) {
return WalkState::Quit;
}
let entry = match result {
Ok(e) => e,
Err(e) => {
if let Some(ref tally) = failures
&& let Ok(mut tally) = tally.lock()
{
let (path, err) = classify_walk_error(&e);
tally.record(&path, err);
}
return WalkState::Continue;
}
};
let Some(item) = map(&entry) else {
return WalkState::Continue;
};
let prev = accepted.fetch_add(1, Ordering::Relaxed);
if prev >= max_items {
hit_budget.store(true, Ordering::Relaxed);
return WalkState::Quit;
}
guard.local.push(item);
if prev + 1 >= max_items {
hit_budget.store(true, Ordering::Relaxed);
return WalkState::Quit;
}
WalkState::Continue
})
});
let items = match Arc::try_unwrap(shards) {
Ok(mutex) => mutex.into_inner().unwrap_or_default(),
Err(arc) => arc
.lock()
.map(|mut g| std::mem::take(&mut *g))
.unwrap_or_default(),
}
.into_iter()
.flatten()
.collect();
let truncated =
hit_budget.load(Ordering::Relaxed) || accepted.load(Ordering::Relaxed) > max_items;
(items, truncated)
}
pub fn collect_files_parallel(builder: &WalkBuilder) -> Vec<PathBuf> {
collect_files_parallel_tallied(builder, None)
}
pub fn collect_files_parallel_tallied(
builder: &WalkBuilder,
failures: Option<std::sync::Arc<Mutex<crate::ndjson_types::FailureTally>>>,
) -> Vec<PathBuf> {
let mut paths = collect_mapped_parallel_tallied(
builder,
|entry| {
if entry.file_type().is_some_and(|ft| ft.is_file()) {
Some(entry.path().to_path_buf())
} else {
None
}
},
failures,
);
sort_paths_parallel(&mut paths);
paths
}
pub fn collect_files_from_roots(
roots: &[PathBuf],
cli_threads: Option<usize>,
configure: impl FnOnce(&mut WalkBuilder),
) -> Vec<PathBuf> {
collect_files_from_roots_tallied(roots, cli_threads, configure, None)
}
pub fn collect_files_from_roots_tallied(
roots: &[PathBuf],
cli_threads: Option<usize>,
configure: impl FnOnce(&mut WalkBuilder),
failures: Option<std::sync::Arc<Mutex<crate::ndjson_types::FailureTally>>>,
) -> Vec<PathBuf> {
if roots.is_empty() {
return Vec::new();
}
let mut builder = WalkBuilder::new(&roots[0]);
for root in roots.iter().skip(1) {
builder.add(root);
}
configure(&mut builder);
apply_walk_threads(&mut builder, cli_threads);
record_ignore_file_failures(roots, failures.as_ref());
collect_files_parallel_tallied(&builder, failures)
}
#[must_use]
pub fn command_uses_rayon(command: &crate::cli::Commands) -> bool {
use crate::cli::Commands;
!matches!(
command,
Commands::Read(_)
| Commands::Stat(_)
| Commands::List(_)
| Commands::Count(_)
| Commands::Write(_)
| Commands::Move(_)
| Commands::Get(_)
| Commands::Set(_)
| Commands::Del(_)
| Commands::Rollback(_)
| Commands::Calc(_)
| Commands::Regex(_)
| Commands::Extract(_)
| Commands::ExtractArtifact(_)
| Commands::Verify(_)
| Commands::Query(_)
| Commands::Outline(_)
| Commands::Scratch(_)
| Commands::Locale(_)
| Commands::CommandsTree(_)
| Commands::AgentSurface(_)
| Commands::Completions(_)
)
}
pub fn plan_global_pool(command: &crate::cli::Commands, cli_threads: Option<usize>) {
let requested = cli_threads.or_else(|| single_target_threads(command));
POOL_REQUEST.set(requested).ok();
if command_uses_rayon(command) {
ensure_global_pool();
}
}
fn single_target_threads(command: &crate::cli::Commands) -> Option<usize> {
if let crate::cli::Commands::Search(args) = command
&& args.paths.len() == 1
&& args.paths[0].is_file()
{
return Some(1);
}
None
}
pub fn ensure_global_pool() {
POOL_BUILD.call_once(|| {
let requested = POOL_REQUEST.get().copied().flatten();
let n = effective_threads(requested);
CONFIGURED_SIZE.set(n).ok();
match rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build_global()
{
Ok(()) => tracing::debug!(threads = n, "rayon global pool configured"),
Err(e) => {
tracing::debug!(error = %e, threads = n, "rayon global pool not reconfigured")
}
}
});
}
pub fn configure_global_pool(cli_threads: Option<usize>) {
POOL_REQUEST.set(cli_threads).ok();
ensure_global_pool();
}
#[must_use]
pub fn configured_pool_size() -> Option<usize> {
CONFIGURED_SIZE.get().copied()
}
#[must_use]
pub fn event_channel_cap() -> usize {
EVENT_CHANNEL_CAP
}
pub fn join_walker<T>(
rx: crossbeam_channel::Receiver<T>,
stop: &std::sync::atomic::AtomicBool,
walker: std::thread::JoinHandle<()>,
) {
use crossbeam_channel::RecvTimeoutError;
stop.store(true, std::sync::atomic::Ordering::Release);
while !walker.is_finished() {
let polled = rx.recv_timeout(std::time::Duration::from_millis(
crate::constants::JOIN_WALKER_POLL_MS,
));
if matches!(polled, Err(RecvTimeoutError::Disconnected)) {
break;
}
}
drop(rx);
if let Err(panic_payload) = walker.join() {
std::panic::resume_unwind(panic_payload);
}
}