2026-08-02 19:04:30 -04:00
|
|
|
//! Parallel filesystem walk for the full indexing run.
|
|
|
|
|
//!
|
2026-08-09 16:25:43 -04:00
|
|
|
//! One shared queue of directories, N worker threads. A worker reads a
|
|
|
|
|
//! directory **and** does that directory's per-file work — stat, classify,
|
|
|
|
|
//! hash — before moving on. On SMB, one `QUERY_DIRECTORY` returns size, mtime
|
|
|
|
|
//! and attributes for every entry, and the cifs client primes its inode cache
|
|
|
|
|
//! from the reply — but only for `actimeo`, one second by default. A `stat`
|
|
|
|
|
//! right after the directory read is therefore free; the same `stat` a few
|
|
|
|
|
//! seconds later is a full network round trip.
|
2026-08-02 19:04:30 -04:00
|
|
|
//!
|
2026-08-09 16:25:43 -04:00
|
|
|
//! Every path below a root is canonical by construction: roots are
|
|
|
|
|
//! canonicalized once at seed time and directories are only ever reached by
|
|
|
|
|
//! joining names onto them. Symlinks are the exception and are resolved where
|
|
|
|
|
//! they are found.
|
2026-08-02 19:04:30 -04:00
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
use std::collections::HashSet;
|
2026-08-02 19:04:30 -04:00
|
|
|
use std::fs;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
2026-08-05 18:05:04 -04:00
|
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
2026-08-02 19:04:30 -04:00
|
|
|
use std::sync::{mpsc, Arc, Condvar, Mutex};
|
2026-08-05 19:17:11 -04:00
|
|
|
use std::thread::JoinHandle;
|
2026-08-02 19:04:30 -04:00
|
|
|
use std::time::UNIX_EPOCH;
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
use crate::config::{Config, IgnoreSet};
|
|
|
|
|
use crate::extract::Registry;
|
|
|
|
|
use crate::file_handling::{
|
2026-08-02 22:21:39 -04:00
|
|
|
classify_by_mtime, classify_for_indexing, path_to_db_string, prepare_file_record,
|
|
|
|
|
warn_if_unrepresentable, DirRows, FileIndexAction, OwnedNewFile, UnreadableDirs,
|
2026-08-02 19:04:30 -04:00
|
|
|
};
|
|
|
|
|
use crate::indexing::should_abort;
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
mod pool;
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests;
|
|
|
|
|
|
|
|
|
|
pub(crate) use pool::WorkerStats;
|
|
|
|
|
use pool::{Found, PrefetchWork, Queue, Shared};
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
/// Files one worker takes for itself before handing the rest to the pool.
|
|
|
|
|
const FILES_PER_JOB: usize = 128;
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Bounded hand-off to the DB writer.
|
2026-08-02 19:04:30 -04:00
|
|
|
const CHANNEL_CAP: usize = 4096;
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Worker threads for a root on local storage.
|
2026-08-02 19:04:30 -04:00
|
|
|
const LOCAL_THREADS: usize = 4;
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Worker threads for a root on a network filesystem, where every uncached
|
|
|
|
|
/// metadata operation is a round trip.
|
2026-08-02 19:04:30 -04:00
|
|
|
const NETWORK_THREADS: usize = 16;
|
|
|
|
|
|
|
|
|
|
/// One file the walk found, with everything the DB writer needs.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct WalkedFile {
|
|
|
|
|
/// Canonical path, and the `files.path` key.
|
|
|
|
|
pub path: String,
|
|
|
|
|
pub action: FileIndexAction,
|
2026-08-09 16:25:43 -04:00
|
|
|
/// `None` when there is nothing to write: unchanged, or the record could
|
|
|
|
|
/// not be built.
|
2026-08-02 19:04:30 -04:00
|
|
|
pub record: Option<OwnedNewFile>,
|
2026-08-02 22:21:39 -04:00
|
|
|
/// 128-bit truncated SHA-256 of [`WalkedFile::path`], for the writer's
|
2026-08-09 16:25:43 -04:00
|
|
|
/// duplicate-visit set.
|
2026-08-02 22:21:39 -04:00
|
|
|
pub digest: u128,
|
2026-08-09 16:25:43 -04:00
|
|
|
/// True when this file was reached by resolving a symlink. Its row is
|
|
|
|
|
/// invisible to its real parent's reconciliation, so the caller must
|
2026-08-02 22:21:39 -04:00
|
|
|
/// exempt it from the vanished-directory sweep.
|
|
|
|
|
pub aliased: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WalkedFile {
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Seen, but with nothing to write: the row stays.
|
2026-08-02 22:21:39 -04:00
|
|
|
fn skipped(path: String, digest: u128, aliased: bool) -> Self {
|
2026-08-04 03:27:05 -04:00
|
|
|
WalkedFile {
|
|
|
|
|
path,
|
|
|
|
|
action: FileIndexAction::Skip,
|
|
|
|
|
record: None,
|
|
|
|
|
digest,
|
|
|
|
|
aliased,
|
|
|
|
|
}
|
2026-08-02 22:21:39 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What the walk emits. Files as they are classified, plus the per-directory
|
|
|
|
|
/// verdict on which index rows no longer have a file behind them.
|
2026-08-05 18:05:04 -04:00
|
|
|
#[allow(clippy::large_enum_variant)]
|
2026-08-02 22:21:39 -04:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum WalkEvent {
|
|
|
|
|
File(WalkedFile),
|
|
|
|
|
/// Paths whose row should be deleted: present in one directory's index
|
|
|
|
|
/// rows, absent from that directory's listing. Emitted once per directory
|
|
|
|
|
/// read, and only for directories that were read successfully.
|
|
|
|
|
Stale(Vec<String>),
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// One file a directory read produced: its path, plus whatever that read
|
|
|
|
|
/// already told us about it.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// `cached` is `Some` only on Windows, where `FindNextFileW` returns size,
|
|
|
|
|
/// mtime and attributes alongside the name; Unix `getdents64` returns only
|
|
|
|
|
/// `d_type`.
|
2026-08-05 18:05:04 -04:00
|
|
|
struct PendingFile {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
cached: Option<crate::platform::CachedMetadata>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PendingFile {
|
|
|
|
|
/// A path that did not come from a directory entry — a resolved symlink
|
|
|
|
|
/// target — so there is nothing cached to carry.
|
|
|
|
|
fn uncached(path: PathBuf) -> Self {
|
|
|
|
|
PendingFile { path, cached: None }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
/// Work waiting for a thread.
|
|
|
|
|
enum Job {
|
2026-08-02 22:21:39 -04:00
|
|
|
/// Read this directory and process its files. Carries the directory's
|
|
|
|
|
/// index rows, fetched by the prefetcher before the job became runnable.
|
|
|
|
|
Dir(PathBuf, Arc<DirRows>),
|
2026-08-09 16:25:43 -04:00
|
|
|
/// A slice of one directory's files, sharing that directory's rows.
|
2026-08-05 18:05:04 -04:00
|
|
|
Files(Vec<PendingFile>, Arc<DirRows>),
|
2026-08-02 22:21:39 -04:00
|
|
|
/// A resolved symlink target, with the stored mtime for its own path.
|
|
|
|
|
Alias(PathBuf, Option<u64>),
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// How many entries each filter rejected, for the one-line summary a run logs
|
|
|
|
|
/// when it finishes.
|
|
|
|
|
///
|
|
|
|
|
/// A pruned *directory* is one increment, not one per file beneath it: the
|
|
|
|
|
/// subtree is never enumerated.
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
pub struct PruneCounts {
|
|
|
|
|
/// Names beginning with a dot.
|
|
|
|
|
pub dot_named: AtomicU64,
|
|
|
|
|
/// Windows entries carrying `FILE_ATTRIBUTE_HIDDEN`.
|
|
|
|
|
pub attribute: AtomicU64,
|
|
|
|
|
/// Rejected by a configured ignore pattern, of either kind.
|
|
|
|
|
pub ignored: AtomicU64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PruneCounts {
|
|
|
|
|
pub fn total(&self) -> u64 {
|
|
|
|
|
self.dot_named.load(Ordering::Relaxed)
|
|
|
|
|
+ self.attribute.load(Ordering::Relaxed)
|
|
|
|
|
+ self.ignored.load(Ordering::Relaxed)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// The summary line, or `None` when nothing was pruned.
|
2026-08-05 18:05:04 -04:00
|
|
|
pub fn summary(&self) -> Option<String> {
|
|
|
|
|
if self.total() == 0 {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
Some(format!(
|
|
|
|
|
"pruned {} entries: {} hidden by attribute, {} dot-named, {} by ignore pattern",
|
|
|
|
|
self.total(),
|
|
|
|
|
self.attribute.load(Ordering::Relaxed),
|
|
|
|
|
self.dot_named.load(Ordering::Relaxed),
|
|
|
|
|
self.ignored.load(Ordering::Relaxed),
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
struct Ctx {
|
|
|
|
|
follow_symlinks: bool,
|
|
|
|
|
include_hidden: bool,
|
|
|
|
|
ignore: IgnoreSet,
|
2026-08-05 18:05:04 -04:00
|
|
|
pruned: PruneCounts,
|
2026-08-02 19:04:30 -04:00
|
|
|
config: Config,
|
|
|
|
|
registry: Arc<Registry>,
|
|
|
|
|
unreadable: UnreadableDirs,
|
2026-08-03 03:06:19 -04:00
|
|
|
stop_flag: Arc<AtomicBool>,
|
2026-08-02 19:04:30 -04:00
|
|
|
suspend_flag: Arc<AtomicBool>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// Individual unreadable-directory warnings allowed per run before only the
|
|
|
|
|
/// count is kept. Reset by [`reset_run_warnings`].
|
|
|
|
|
static UNREADABLE_WARNINGS: crate::log::Throttle = crate::log::Throttle::new(20);
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Arm this module's per-run warning throttle.
|
2026-08-05 18:05:04 -04:00
|
|
|
pub fn reset_run_warnings() {
|
|
|
|
|
UNREADABLE_WARNINGS.reset();
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
/// Read one directory, apply the hidden/ignore rules, and split the result:
|
|
|
|
|
/// subdirectories and overflow file chunks go to `found` for the pool, the
|
|
|
|
|
/// remaining files come back for this worker to handle immediately.
|
2026-08-02 22:21:39 -04:00
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Also reconciles the directory against its index rows: `stale` receives the
|
|
|
|
|
/// paths whose row has no file behind it any more.
|
2026-08-02 22:21:39 -04:00
|
|
|
///
|
|
|
|
|
/// A directory that cannot be read returns before reconciling, so nothing
|
|
|
|
|
/// under it is ever deleted — an unreadable directory must not read as an
|
|
|
|
|
/// empty one.
|
|
|
|
|
fn read_directory(
|
|
|
|
|
dir: &Path,
|
|
|
|
|
rows: &Arc<DirRows>,
|
|
|
|
|
ctx: &Ctx,
|
|
|
|
|
found: &mut Vec<Found>,
|
|
|
|
|
stale: &mut Vec<String>,
|
2026-08-05 18:05:04 -04:00
|
|
|
) -> Vec<PendingFile> {
|
2026-08-02 19:04:30 -04:00
|
|
|
let entries = match fs::read_dir(dir) {
|
|
|
|
|
Ok(entries) => entries,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
// Not the same as "this directory is empty": see UnreadableDirs.
|
2026-08-05 18:05:04 -04:00
|
|
|
if UNREADABLE_WARNINGS.allow() {
|
|
|
|
|
crate::log_warn!("cannot read {}: {}", dir.display(), e);
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
ctx.unreadable.record(dir.to_path_buf());
|
|
|
|
|
return Vec::new();
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-08-09 16:25:43 -04:00
|
|
|
// Names surviving the filters, for the stale diff below. Every `continue`
|
|
|
|
|
// in the loop must be a genuine "not indexable", or the diff deletes live
|
|
|
|
|
// rows.
|
2026-08-02 22:21:39 -04:00
|
|
|
let mut present: HashSet<String> = HashSet::new();
|
|
|
|
|
let mut unreadable_entry = false;
|
2026-08-02 19:04:30 -04:00
|
|
|
|
|
|
|
|
let mut files = Vec::new();
|
|
|
|
|
for entry in entries {
|
|
|
|
|
let entry = match entry {
|
|
|
|
|
Ok(entry) => entry,
|
|
|
|
|
Err(e) => {
|
2026-08-05 18:05:04 -04:00
|
|
|
if UNREADABLE_WARNINGS.allow() {
|
|
|
|
|
crate::log_warn!("cannot read an entry of {}: {}", dir.display(), e);
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
ctx.unreadable.record(dir.to_path_buf());
|
2026-08-09 16:25:43 -04:00
|
|
|
// An incomplete listing cannot decide what is missing: an
|
|
|
|
|
// entry we failed to read looks identical to a deleted one.
|
2026-08-02 22:21:39 -04:00
|
|
|
unreadable_entry = true;
|
2026-08-02 19:04:30 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let name = entry.file_name();
|
|
|
|
|
let name = name.to_string_lossy();
|
2026-08-09 16:25:43 -04:00
|
|
|
// The closure runs only on Windows, where `entry.metadata()` is free —
|
2026-08-05 18:05:04 -04:00
|
|
|
// the attributes came back with the directory read, and it reports the
|
2026-08-09 16:25:43 -04:00
|
|
|
// entry itself rather than a link target.
|
2026-08-05 18:05:04 -04:00
|
|
|
if !ctx.include_hidden {
|
|
|
|
|
if let Some(reason) =
|
|
|
|
|
crate::platform::entry_hidden_reason(&name, || entry.metadata().ok())
|
|
|
|
|
{
|
|
|
|
|
match reason {
|
|
|
|
|
crate::platform::HiddenReason::DotPrefix => {
|
|
|
|
|
ctx.pruned.dot_named.fetch_add(1, Ordering::Relaxed);
|
|
|
|
|
}
|
|
|
|
|
crate::platform::HiddenReason::Attribute => {
|
|
|
|
|
ctx.pruned.attribute.fetch_add(1, Ordering::Relaxed);
|
2026-08-09 16:25:43 -04:00
|
|
|
// Announced because a plainly visible folder skipped
|
|
|
|
|
// over an attribute Explorer does not show has no
|
|
|
|
|
// other way of being discovered.
|
2026-08-05 18:05:04 -04:00
|
|
|
if entry.file_type().is_ok_and(|ft| ft.is_dir()) {
|
|
|
|
|
crate::log_info!(
|
|
|
|
|
"skipping {}: hidden attribute set (enable \"include hidden \
|
|
|
|
|
files\" to index it)",
|
|
|
|
|
entry.path().display()
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
if ctx.ignore.matches_component(&name) {
|
2026-08-05 18:05:04 -04:00
|
|
|
ctx.pruned.ignored.fetch_add(1, Ordering::Relaxed);
|
2026-08-02 19:04:30 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let path = entry.path();
|
|
|
|
|
if ctx.ignore.matches_path_pattern(&path) {
|
2026-08-05 18:05:04 -04:00
|
|
|
ctx.pruned.ignored.fetch_add(1, Ordering::Relaxed);
|
2026-08-02 19:04:30 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
// `file_type` is the cached `d_type` from the directory read.
|
2026-08-02 19:04:30 -04:00
|
|
|
match entry.file_type() {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Directories hold no `files` row and are not marked present: a
|
|
|
|
|
// name that was a file last run and is a directory now *should*
|
|
|
|
|
// lose its row.
|
2026-08-02 22:21:39 -04:00
|
|
|
Ok(ft) if ft.is_dir() => found.push(Found::Dir(path)),
|
2026-08-02 19:04:30 -04:00
|
|
|
Ok(ft) if ft.is_symlink() => {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Directory and file targets must be gated together, or the
|
|
|
|
|
// two walkers disagree: `filtered_walk` follows neither kind,
|
|
|
|
|
// so a file target followed only here would be indexed by
|
|
|
|
|
// every full run and never updated between them.
|
2026-08-03 03:06:19 -04:00
|
|
|
if !ctx.follow_symlinks {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-08-09 16:25:43 -04:00
|
|
|
// The index stores the target's canonical path, and pushing
|
|
|
|
|
// only canonical directories is what keeps `seen_dirs` able
|
|
|
|
|
// to break cycles. Normalized like the roots, or on Windows
|
|
|
|
|
// the target keeps `canonicalize`'s `\\?\` prefix, under
|
|
|
|
|
// which full-path ignore patterns would never match and
|
|
|
|
|
// `seen_dirs` could not dedup against an overlapping root.
|
2026-08-02 19:04:30 -04:00
|
|
|
if let Ok(target) = path.canonicalize() {
|
2026-08-04 03:27:05 -04:00
|
|
|
let target = PathBuf::from(path_to_db_string(&target));
|
2026-08-02 19:04:30 -04:00
|
|
|
match fs::metadata(&target) {
|
2026-08-03 03:06:19 -04:00
|
|
|
Ok(m) if m.is_dir() => found.push(Found::Dir(target)),
|
2026-08-09 16:25:43 -04:00
|
|
|
// The target's row belongs to its own directory, so
|
|
|
|
|
// it is not marked present here.
|
2026-08-02 22:21:39 -04:00
|
|
|
Ok(_) => found.push(Found::Alias(target)),
|
2026-08-02 19:04:30 -04:00
|
|
|
Err(_) => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-02 22:21:39 -04:00
|
|
|
Ok(_) => {
|
|
|
|
|
present.insert(name.into_owned());
|
2026-08-09 16:25:43 -04:00
|
|
|
// `None` on Unix and on any reparse point: see
|
|
|
|
|
// `entry_cached_metadata`.
|
2026-08-05 18:05:04 -04:00
|
|
|
let cached = crate::platform::entry_cached_metadata(|| entry.metadata().ok());
|
|
|
|
|
files.push(PendingFile { path, cached });
|
2026-08-02 22:21:39 -04:00
|
|
|
}
|
2026-08-09 16:25:43 -04:00
|
|
|
// Type unknown: mark it present so an existing row survives.
|
2026-08-02 22:21:39 -04:00
|
|
|
Err(_) => {
|
|
|
|
|
present.insert(name.into_owned());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !unreadable_entry {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Rebuild each stored path the way `prepare` does, by joining onto
|
|
|
|
|
// the canonical directory, so separators and roots match the
|
|
|
|
|
// `files.path` spelling exactly.
|
|
|
|
|
stale.extend(
|
|
|
|
|
rows.keys()
|
|
|
|
|
.filter(|name| !present.contains(name.as_str()))
|
|
|
|
|
.map(|name| path_to_db_string(&dir.join(name))),
|
|
|
|
|
);
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
// Spread a wide directory across the pool, keeping the tail for ourselves
|
|
|
|
|
// so the entries the read just warmed are handled now.
|
2026-08-02 19:04:30 -04:00
|
|
|
while files.len() > FILES_PER_JOB {
|
|
|
|
|
let chunk = files.split_off(files.len() - FILES_PER_JOB);
|
2026-08-02 22:21:39 -04:00
|
|
|
found.push(Found::Files(chunk, rows.clone()));
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
files
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
/// How a file's stored mtime is to be found.
|
|
|
|
|
enum Known<'a> {
|
|
|
|
|
/// By name within the directory being walked — the ordinary case.
|
|
|
|
|
InDir(&'a DirRows),
|
|
|
|
|
/// Already resolved by exact path, for a symlink target whose row lives
|
|
|
|
|
/// under a different parent.
|
|
|
|
|
Exact(Option<u64>),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 128-bit truncated SHA-256 of a path, for the writer's duplicate-visit set.
|
|
|
|
|
///
|
|
|
|
|
/// Truncated rather than full: 16 bytes is ~4e-26 collision probability at
|
|
|
|
|
/// 7M paths, where 8 bytes would be ~1e-6 — and a collision here silently
|
|
|
|
|
/// drops a real file from the index. Cryptographic rather than fast because
|
|
|
|
|
/// filenames on a shared volume are attacker-supplied, so a cheap hash would
|
|
|
|
|
/// let a chosen pair hide one of the two files.
|
|
|
|
|
pub fn path_digest(path: &str) -> u128 {
|
|
|
|
|
let digest = Sha256::digest(path.as_bytes());
|
|
|
|
|
let mut bytes = [0u8; 16];
|
|
|
|
|
bytes.copy_from_slice(&digest[..16]);
|
|
|
|
|
u128::from_be_bytes(bytes)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// At most one `stat`, then classify; only files that are actually going to be
|
2026-08-02 19:04:30 -04:00
|
|
|
/// written get opened, and small text files are finished outright.
|
2026-08-05 18:05:04 -04:00
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// "At most": on Windows [`PendingFile::cached`] may already hold the answer —
|
|
|
|
|
/// see [`crate::platform::metadata_or_stat`].
|
2026-08-05 18:05:04 -04:00
|
|
|
fn prepare(file: PendingFile, known: Known<'_>, ctx: &Ctx) -> WalkedFile {
|
|
|
|
|
let PendingFile { path, cached } = file;
|
2026-08-02 19:04:30 -04:00
|
|
|
let db_path = path_to_db_string(&path);
|
2026-08-02 22:21:39 -04:00
|
|
|
let digest = path_digest(&db_path);
|
|
|
|
|
let aliased = matches!(known, Known::Exact(_));
|
2026-08-02 19:04:30 -04:00
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
// A name that is not valid UTF-8 cannot be stored in `files.path`. Emitted
|
|
|
|
|
// as `Skip` because the caller reads a missing path as "deleted".
|
2026-08-02 19:04:30 -04:00
|
|
|
if warn_if_unrepresentable(&path) {
|
2026-08-02 22:21:39 -04:00
|
|
|
return WalkedFile::skipped(db_path, digest, aliased);
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
let Ok(meta) = crate::platform::metadata_or_stat(&path, cached) else {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Seen but unreadable: a transient stat failure must not read as
|
|
|
|
|
// "deleted".
|
2026-08-02 22:21:39 -04:00
|
|
|
return WalkedFile::skipped(db_path, digest, aliased);
|
2026-08-02 19:04:30 -04:00
|
|
|
};
|
|
|
|
|
let Some(mtime) = meta
|
|
|
|
|
.modified()
|
|
|
|
|
.ok()
|
|
|
|
|
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
|
|
|
|
|
.map(|d| d.as_secs())
|
|
|
|
|
else {
|
2026-08-02 22:21:39 -04:00
|
|
|
return WalkedFile::skipped(db_path, digest, aliased);
|
2026-08-02 19:04:30 -04:00
|
|
|
};
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
let action = match known {
|
|
|
|
|
Known::InDir(rows) => {
|
|
|
|
|
let name = path
|
|
|
|
|
.file_name()
|
|
|
|
|
.map(|n| n.to_string_lossy().into_owned())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
classify_for_indexing(&name, mtime, rows)
|
|
|
|
|
}
|
|
|
|
|
Known::Exact(stored) => classify_by_mtime(stored, mtime),
|
|
|
|
|
};
|
2026-08-02 19:04:30 -04:00
|
|
|
let record = match action {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Unchanged: never opened, never hashed — the case that must stay at
|
|
|
|
|
// one syscall.
|
2026-08-02 19:04:30 -04:00
|
|
|
FileIndexAction::Skip => None,
|
2026-08-09 16:25:43 -04:00
|
|
|
// `prepare_file_record` gates on `is_file()`, which keeps us from
|
|
|
|
|
// opening a FIFO — that would block forever, uninterruptibly.
|
2026-08-02 19:04:30 -04:00
|
|
|
_ => prepare_file_record(&db_path, &meta, &ctx.config, &ctx.registry),
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
WalkedFile {
|
|
|
|
|
path: db_path,
|
|
|
|
|
action,
|
|
|
|
|
record,
|
|
|
|
|
digest,
|
|
|
|
|
aliased,
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
fn worker(shared: &Shared, ctx: &Ctx, tx: &mpsc::SyncSender<WalkEvent>) {
|
2026-08-02 19:04:30 -04:00
|
|
|
while let Some((job, slot)) = shared.take() {
|
2026-08-03 03:06:19 -04:00
|
|
|
let _busy = shared.stats.enter();
|
2026-08-02 19:04:30 -04:00
|
|
|
if should_abort(&ctx.stop_flag, &ctx.suspend_flag) {
|
|
|
|
|
shared.shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut found = Vec::new();
|
2026-08-02 22:21:39 -04:00
|
|
|
let mut stale = Vec::new();
|
|
|
|
|
let (files, rows) = match job {
|
|
|
|
|
Job::Dir(dir, rows) => {
|
|
|
|
|
let files = read_directory(&dir, &rows, ctx, &mut found, &mut stale);
|
|
|
|
|
(files, rows)
|
|
|
|
|
}
|
|
|
|
|
Job::Files(files, rows) => (files, rows),
|
|
|
|
|
Job::Alias(path, stored) => {
|
|
|
|
|
slot.finish(found);
|
2026-08-05 18:05:04 -04:00
|
|
|
let file = PendingFile::uncached(path);
|
2026-08-04 03:27:05 -04:00
|
|
|
if tx
|
2026-08-05 18:05:04 -04:00
|
|
|
.send(WalkEvent::File(prepare(file, Known::Exact(stored), ctx)))
|
2026-08-04 03:27:05 -04:00
|
|
|
.is_err()
|
|
|
|
|
{
|
2026-08-02 22:21:39 -04:00
|
|
|
shared.shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Hand the subdirectories over before doing our own per-file work, so
|
2026-08-09 16:25:43 -04:00
|
|
|
// the rest of the pool never idles waiting behind one worker.
|
2026-08-02 19:04:30 -04:00
|
|
|
slot.finish(found);
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
if !stale.is_empty() && tx.send(WalkEvent::Stale(stale)).is_err() {
|
|
|
|
|
shared.shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
for file in files {
|
2026-08-02 19:04:30 -04:00
|
|
|
if should_abort(&ctx.stop_flag, &ctx.suspend_flag) {
|
|
|
|
|
shared.shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-08-04 03:27:05 -04:00
|
|
|
if tx
|
2026-08-05 18:05:04 -04:00
|
|
|
.send(WalkEvent::File(prepare(file, Known::InDir(&rows), ctx)))
|
2026-08-04 03:27:05 -04:00
|
|
|
.is_err()
|
|
|
|
|
{
|
2026-08-02 19:04:30 -04:00
|
|
|
// Receiver gone: the run was stopped or failed. Not an error.
|
|
|
|
|
shared.shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
/// Serves the pool's directory-row and symlink-mtime lookups from one
|
|
|
|
|
/// read-only connection.
|
|
|
|
|
///
|
|
|
|
|
/// A failed query is not fatal: the job is abandoned rather than retried, and
|
|
|
|
|
/// the directory it was for simply goes unwalked, which reconciliation reads
|
|
|
|
|
/// as "not seen" and therefore deletes nothing.
|
|
|
|
|
fn prefetcher(shared: &Shared, db_path: &str) {
|
|
|
|
|
let conn = match crate::db::open::open_walk_reader(db_path) {
|
|
|
|
|
Ok(conn) => conn,
|
|
|
|
|
Err(e) => {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Without rows, continuing would treat every file as new and
|
|
|
|
|
// every row as stale.
|
2026-08-02 22:21:39 -04:00
|
|
|
crate::log_warn!("walk reader: {}", e);
|
|
|
|
|
shared.shutdown();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
while let Some(work) = shared.take_prefetch() {
|
|
|
|
|
match work {
|
|
|
|
|
PrefetchWork::Dir(dir) => {
|
|
|
|
|
match crate::db::repo::dir_rows(&conn, &path_to_db_string(&dir)) {
|
|
|
|
|
Ok(rows) => shared.finish_prefetch(Job::Dir(dir, Arc::new(rows))),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
crate::log_warn!("{}", e);
|
|
|
|
|
shared.abandon_prefetch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
PrefetchWork::Alias(path) => {
|
|
|
|
|
match crate::db::repo::mtime_for_path(&conn, &path_to_db_string(&path)) {
|
|
|
|
|
Ok(stored) => shared.finish_prefetch(Job::Alias(path, stored)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
crate::log_warn!("{}", e);
|
|
|
|
|
shared.abandon_prefetch();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
/// A running parallel walk. Iterating it drains finished files; dropping it
|
|
|
|
|
/// stops the workers and joins them.
|
|
|
|
|
pub struct ParallelWalk {
|
2026-08-02 22:21:39 -04:00
|
|
|
rx: Option<mpsc::Receiver<WalkEvent>>,
|
2026-08-05 18:05:04 -04:00
|
|
|
/// One event pulled off the channel by [`ParallelWalk::wait_ready`] and not
|
2026-08-09 16:25:43 -04:00
|
|
|
/// yet handed to [`ParallelWalk::try_next`].
|
2026-08-05 18:05:04 -04:00
|
|
|
pending: Option<WalkEvent>,
|
2026-08-02 19:04:30 -04:00
|
|
|
handles: Vec<JoinHandle<()>>,
|
2026-08-02 22:21:39 -04:00
|
|
|
prefetch: Option<JoinHandle<()>>,
|
2026-08-02 19:04:30 -04:00
|
|
|
shared: Arc<Shared>,
|
|
|
|
|
ctx: Arc<Ctx>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ParallelWalk {
|
|
|
|
|
/// Directories that could not be read. Only final once the iterator has
|
2026-08-09 16:25:43 -04:00
|
|
|
/// ended.
|
2026-08-02 19:04:30 -04:00
|
|
|
pub fn unreadable(&self) -> &UnreadableDirs {
|
|
|
|
|
&self.ctx.unreadable
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// How many entries each filter rejected. Final on the same terms as
|
|
|
|
|
/// [`ParallelWalk::unreadable`].
|
|
|
|
|
pub fn pruned(&self) -> &PruneCounts {
|
|
|
|
|
&self.ctx.pruned
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
/// Every canonical directory the walk queued, in `files.parent` spelling.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// The vanished-directory sweep needs this: a directory deleted wholesale
|
|
|
|
|
/// is never read, so nothing reconciles the rows beneath it.
|
2026-08-02 22:21:39 -04:00
|
|
|
///
|
|
|
|
|
/// Only meaningful once the walk has finished.
|
|
|
|
|
pub fn seen_dirs(&self) -> HashSet<String> {
|
2026-08-09 16:25:43 -04:00
|
|
|
crate::lock_ok(&self.shared.queue)
|
2026-08-02 22:21:39 -04:00
|
|
|
.seen_dirs
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|d| path_to_db_string(d))
|
|
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-09 16:25:43 -04:00
|
|
|
/// A cheap, cloneable handle for reading worker activity.
|
2026-08-03 03:06:19 -04:00
|
|
|
///
|
|
|
|
|
/// Meaningful only while the walk is running: once the workers exit, the
|
2026-08-09 16:25:43 -04:00
|
|
|
/// busy count is permanently zero.
|
2026-08-02 19:04:30 -04:00
|
|
|
pub fn worker_stats(&self) -> WorkerStats {
|
2026-08-03 03:06:19 -04:00
|
|
|
self.shared.stats.clone()
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Join the workers and report whether every one of them finished
|
|
|
|
|
/// cleanly.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// A dead worker and a finished worker look identical from the receiving
|
|
|
|
|
/// end — both close the channel — and treating a panicked walk as complete
|
|
|
|
|
/// would hand stale cleanup a partial file set. Join before deciding
|
|
|
|
|
/// anything about what the walk saw.
|
2026-08-02 19:04:30 -04:00
|
|
|
pub fn finish(&mut self) -> bool {
|
|
|
|
|
// Dropping the receiver first releases any worker parked in `send`.
|
|
|
|
|
self.rx = None;
|
|
|
|
|
let mut clean = true;
|
|
|
|
|
for handle in self.handles.drain(..) {
|
|
|
|
|
if handle.join().is_err() {
|
|
|
|
|
clean = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-02 22:21:39 -04:00
|
|
|
if let Some(handle) = self.prefetch.take() {
|
2026-08-09 16:25:43 -04:00
|
|
|
// `shutdown` releases a prefetcher parked behind PREFETCH_AHEAD;
|
|
|
|
|
// without it this join would block until the queue emptied.
|
2026-08-02 22:21:39 -04:00
|
|
|
self.shared.shutdown();
|
|
|
|
|
if handle.join().is_err() {
|
|
|
|
|
clean = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
clean
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 03:06:19 -04:00
|
|
|
/// Result of a non-blocking pull from a producer pool.
|
|
|
|
|
pub enum TryNext<T> {
|
|
|
|
|
Item(T),
|
|
|
|
|
/// Nothing ready right now; the pass is still running.
|
2026-08-02 19:04:30 -04:00
|
|
|
Empty,
|
2026-08-03 03:06:19 -04:00
|
|
|
/// The pass has ended (all workers exited, for any reason).
|
2026-08-02 19:04:30 -04:00
|
|
|
Finished,
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 03:06:19 -04:00
|
|
|
/// Translate a non-blocking channel pull into [`TryNext`]. `None` is a
|
|
|
|
|
/// receiver the owner already dropped, which reads as finished.
|
|
|
|
|
pub(crate) fn try_recv_next<T>(rx: Option<&mpsc::Receiver<T>>) -> TryNext<T> {
|
|
|
|
|
match rx {
|
|
|
|
|
None => TryNext::Finished,
|
|
|
|
|
Some(rx) => match rx.try_recv() {
|
|
|
|
|
Ok(item) => TryNext::Item(item),
|
|
|
|
|
Err(mpsc::TryRecvError::Empty) => TryNext::Empty,
|
|
|
|
|
Err(mpsc::TryRecvError::Disconnected) => TryNext::Finished,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 18:05:04 -04:00
|
|
|
/// [`try_recv_next`], but willing to wait up to `timeout` for something to
|
|
|
|
|
/// arrive.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// Used instead of a sleep backoff: on Windows the default timer resolution
|
|
|
|
|
/// is 15.6 ms, so a 2 ms sleep actually stalls for 15.6. `recv_timeout` parks
|
|
|
|
|
/// on the channel's own condition variable, so a sender wakes it immediately.
|
2026-08-05 18:05:04 -04:00
|
|
|
pub(crate) fn recv_next_timeout<T>(
|
|
|
|
|
rx: Option<&mpsc::Receiver<T>>,
|
|
|
|
|
timeout: std::time::Duration,
|
|
|
|
|
) -> TryNext<T> {
|
|
|
|
|
match rx {
|
|
|
|
|
None => TryNext::Finished,
|
|
|
|
|
Some(rx) => match rx.recv_timeout(timeout) {
|
|
|
|
|
Ok(item) => TryNext::Item(item),
|
|
|
|
|
Err(mpsc::RecvTimeoutError::Timeout) => TryNext::Empty,
|
|
|
|
|
Err(mpsc::RecvTimeoutError::Disconnected) => TryNext::Finished,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-02 19:04:30 -04:00
|
|
|
impl ParallelWalk {
|
|
|
|
|
/// Non-blocking variant of `next`, for callers multiplexing several
|
|
|
|
|
/// walks (the per-root writer loop).
|
2026-08-03 03:06:19 -04:00
|
|
|
pub fn try_next(&mut self) -> TryNext<WalkEvent> {
|
2026-08-05 18:05:04 -04:00
|
|
|
if let Some(event) = self.pending.take() {
|
|
|
|
|
return TryNext::Item(event);
|
|
|
|
|
}
|
2026-08-03 03:06:19 -04:00
|
|
|
try_recv_next(self.rx.as_ref())
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
2026-08-05 18:05:04 -04:00
|
|
|
|
|
|
|
|
/// Wait up to `timeout` for this walk to produce something, holding
|
|
|
|
|
/// whatever arrives for the next [`ParallelWalk::try_next`].
|
|
|
|
|
///
|
|
|
|
|
/// Returns whether anything is now ready.
|
|
|
|
|
pub fn wait_ready(&mut self, timeout: std::time::Duration) -> bool {
|
|
|
|
|
if self.pending.is_some() {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
match recv_next_timeout(self.rx.as_ref(), timeout) {
|
|
|
|
|
TryNext::Item(event) => {
|
|
|
|
|
self.pending = Some(event);
|
|
|
|
|
true
|
|
|
|
|
}
|
2026-08-09 16:25:43 -04:00
|
|
|
// A finished walk is "ready": there is something to do (notice
|
|
|
|
|
// it ended).
|
2026-08-05 18:05:04 -04:00
|
|
|
TryNext::Finished => true,
|
|
|
|
|
TryNext::Empty => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Iterator for ParallelWalk {
|
2026-08-02 22:21:39 -04:00
|
|
|
type Item = WalkEvent;
|
2026-08-02 19:04:30 -04:00
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
fn next(&mut self) -> Option<WalkEvent> {
|
2026-08-05 18:05:04 -04:00
|
|
|
if let Some(event) = self.pending.take() {
|
|
|
|
|
return Some(event);
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
self.rx.as_ref()?.recv().ok()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for ParallelWalk {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
self.shared.shutdown();
|
|
|
|
|
// No-op if the caller already called `finish`.
|
|
|
|
|
self.finish();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Walk `roots` in parallel, yielding every indexable file exactly once per
|
|
|
|
|
/// canonical path.
|
|
|
|
|
///
|
|
|
|
|
/// `workers` is explicit so callers can honour per-root overrides; use
|
|
|
|
|
/// [`thread_count_for`] for the storage-appropriate default. Clamped to
|
|
|
|
|
/// 1..=64.
|
2026-08-02 22:21:39 -04:00
|
|
|
/// `db_path` is opened read-only by this walk's row prefetcher; the walk
|
|
|
|
|
/// itself never writes.
|
2026-08-02 19:04:30 -04:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub fn walk_indexable_files(
|
|
|
|
|
roots: &[String],
|
|
|
|
|
follow_symlinks: bool,
|
|
|
|
|
include_hidden: bool,
|
|
|
|
|
ignore: IgnoreSet,
|
2026-08-02 22:21:39 -04:00
|
|
|
db_path: &str,
|
2026-08-02 19:04:30 -04:00
|
|
|
config: Config,
|
|
|
|
|
registry: Arc<Registry>,
|
2026-08-03 03:06:19 -04:00
|
|
|
stop_flag: Arc<AtomicBool>,
|
2026-08-02 19:04:30 -04:00
|
|
|
suspend_flag: Arc<AtomicBool>,
|
|
|
|
|
workers: usize,
|
|
|
|
|
) -> ParallelWalk {
|
|
|
|
|
let mut queue = Queue::default();
|
|
|
|
|
let mut unresolvable: Vec<PathBuf> = Vec::new();
|
|
|
|
|
for root in roots {
|
2026-08-09 16:25:43 -04:00
|
|
|
// Canonicalize here so "everything below a root is already canonical"
|
|
|
|
|
// holds however this is called: a non-canonical root would make every
|
|
|
|
|
// file look new and every stored row look stale.
|
2026-08-02 19:04:30 -04:00
|
|
|
//
|
2026-08-09 16:25:43 -04:00
|
|
|
// Roots themselves are never filtered — the user chose them.
|
2026-08-02 19:04:30 -04:00
|
|
|
match fs::canonicalize(root) {
|
|
|
|
|
Ok(dir) => {
|
|
|
|
|
let dir = PathBuf::from(path_to_db_string(&dir));
|
|
|
|
|
if queue.seen_dirs.insert(dir.clone()) {
|
2026-08-02 22:21:39 -04:00
|
|
|
queue.needs_rows.push(dir);
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
crate::log_warn!("cannot resolve indexing root {}: {}", root, e);
|
2026-08-09 16:25:43 -04:00
|
|
|
// An unmounted root yields nothing, indistinguishable from
|
|
|
|
|
// "all its files were deleted"; recorded so stale cleanup
|
|
|
|
|
// leaves it alone.
|
2026-08-02 19:04:30 -04:00
|
|
|
unresolvable.push(PathBuf::from(root));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let threads = workers.clamp(1, 64);
|
|
|
|
|
let shared = Arc::new(Shared {
|
|
|
|
|
queue: Mutex::new(queue),
|
|
|
|
|
idle: Condvar::new(),
|
2026-08-03 03:06:19 -04:00
|
|
|
stats: WorkerStats::new(threads),
|
2026-08-02 19:04:30 -04:00
|
|
|
});
|
|
|
|
|
let ctx = Arc::new(Ctx {
|
|
|
|
|
follow_symlinks,
|
|
|
|
|
include_hidden,
|
|
|
|
|
ignore,
|
2026-08-05 18:05:04 -04:00
|
|
|
pruned: PruneCounts::default(),
|
2026-08-02 19:04:30 -04:00
|
|
|
config,
|
|
|
|
|
registry,
|
|
|
|
|
unreadable: UnreadableDirs::default(),
|
|
|
|
|
stop_flag,
|
|
|
|
|
suspend_flag,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for root in unresolvable {
|
|
|
|
|
ctx.unreadable.record(root);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (tx, rx) = mpsc::sync_channel(CHANNEL_CAP);
|
|
|
|
|
let handles = (0..threads)
|
|
|
|
|
.map(|_| {
|
|
|
|
|
let (shared, ctx, tx) = (shared.clone(), ctx.clone(), tx.clone());
|
2026-08-05 19:17:11 -04:00
|
|
|
crate::platform::spawn_worker("qs-walk", move || {
|
2026-08-03 03:06:19 -04:00
|
|
|
crate::platform::set_background_priority();
|
|
|
|
|
worker(&shared, &ctx, &tx)
|
|
|
|
|
})
|
2026-08-02 19:04:30 -04:00
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
// The workers must hold the only senders, or `recv` never reports the end
|
2026-08-09 16:25:43 -04:00
|
|
|
// of the walk.
|
2026-08-02 19:04:30 -04:00
|
|
|
drop(tx);
|
|
|
|
|
|
2026-08-02 22:21:39 -04:00
|
|
|
let prefetch = {
|
|
|
|
|
let (shared, db_path) = (shared.clone(), db_path.to_string());
|
2026-08-05 19:17:11 -04:00
|
|
|
crate::platform::spawn_worker("qs-prefetch", move || {
|
2026-08-03 03:06:19 -04:00
|
|
|
crate::platform::set_background_priority();
|
|
|
|
|
prefetcher(&shared, &db_path)
|
|
|
|
|
})
|
2026-08-02 22:21:39 -04:00
|
|
|
};
|
|
|
|
|
|
2026-08-04 03:27:05 -04:00
|
|
|
ParallelWalk {
|
|
|
|
|
rx: Some(rx),
|
2026-08-05 18:05:04 -04:00
|
|
|
pending: None,
|
2026-08-04 03:27:05 -04:00
|
|
|
handles,
|
|
|
|
|
prefetch: Some(prefetch),
|
|
|
|
|
shared,
|
|
|
|
|
ctx,
|
|
|
|
|
}
|
2026-08-02 19:04:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pick a worker count for these roots.
|
|
|
|
|
///
|
2026-08-09 16:25:43 -04:00
|
|
|
/// A network share wants far more threads than cores — each worker spends its
|
|
|
|
|
/// time blocked on a round trip — and with a mix of roots the higher count
|
|
|
|
|
/// wins.
|
2026-08-02 19:04:30 -04:00
|
|
|
pub fn thread_count_for(roots: &[String]) -> usize {
|
|
|
|
|
let network = roots
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|r| crate::platform::is_network_path(Path::new(r)));
|
|
|
|
|
if network {
|
|
|
|
|
NETWORK_THREADS
|
|
|
|
|
} else {
|
|
|
|
|
LOCAL_THREADS
|
|
|
|
|
}
|
|
|
|
|
}
|