use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tracing::{error, info};
use crate::FileSync;
use crate::background_watcher::BackgroundWatcher;
use crate::bigram_filter::BigramOverlay;
use crate::bigram_filter::build_bigram_index;
use crate::error::Error;
use crate::file_picker::{self, FFFMode, warmup_mmaps};
use crate::shared::{SharedFilePicker, SharedFrecency};
use crate::types::ContentCacheBudget;
#[derive(Clone, Default)]
pub(crate) struct ScanSignals {
pub(crate) scanning: Arc<AtomicBool>,
pub(crate) watcher_ready: Arc<AtomicBool>,
pub(crate) cancelled: Arc<AtomicBool>,
pub(crate) post_scan_busy: Arc<AtomicBool>,
pub(crate) rescan_pending: Arc<AtomicBool>,
}
#[derive(Clone, Copy, Default)]
pub(crate) struct ScanConfig {
pub(crate) warmup: bool,
pub(crate) content_indexing: bool,
pub(crate) watch: bool,
pub(crate) auto_cache_budget: bool,
pub(crate) install_watcher: bool,
}
pub(crate) struct ScanJob {
shared_picker: SharedFilePicker,
shared_frecency: SharedFrecency,
base_path: PathBuf,
mode: FFFMode,
signals: ScanSignals,
config: ScanConfig,
scanned_files_counter: Arc<AtomicUsize>,
}
impl ScanJob {
pub fn new(
shared_picker: &SharedFilePicker,
shared_frecency: &SharedFrecency,
install_watcher: bool,
) -> Result<Option<Self>, Error> {
let guard = shared_picker.read()?;
let picker = guard.as_ref().ok_or(Error::FilePickerMissing)?;
if picker.is_scan_active() {
return Ok(None);
}
let signals = picker.scan_signals();
if signals.post_scan_busy.load(Ordering::Acquire) {
return Ok(None);
}
Ok(Some(Self {
shared_picker: shared_picker.clone(),
shared_frecency: shared_frecency.clone(),
base_path: picker.base_path().to_path_buf(),
mode: picker.mode(),
signals,
scanned_files_counter: picker.scanned_files_counter(),
config: ScanConfig {
warmup: picker.has_mmap_cache(),
content_indexing: picker.has_content_indexing(),
watch: picker.has_watcher(),
auto_cache_budget: !picker.has_explicit_cache_budget(),
install_watcher,
},
}))
}
pub fn new_initial(
shared_picker: SharedFilePicker,
shared_frecency: SharedFrecency,
base_path: PathBuf,
mode: FFFMode,
signals: ScanSignals,
scanned_files_counter: Arc<AtomicUsize>,
config: ScanConfig,
) -> Self {
Self {
shared_picker,
shared_frecency,
base_path,
mode,
signals,
scanned_files_counter,
config,
}
}
pub fn spawn(self) -> std::thread::JoinHandle<()> {
self.signals.scanning.store(true, Ordering::Release);
std::thread::Builder::new()
.name("fff-scan".into())
.spawn(move || self.run())
.expect("failed to spawn fff-scan thread")
}
fn run(self) {
let Self {
shared_picker,
shared_frecency,
base_path,
mode,
signals,
scanned_files_counter,
config,
} = self;
let _scanning = ScanningGuard::new(&signals, config.install_watcher);
scanned_files_counter.store(0, Ordering::Relaxed);
let git_workdir = FileSync::discover_git_workdir(&base_path);
let status_handle = git_workdir.clone().map(FileSync::spawn_git_status);
let sync = match FileSync::walk_filesystem(
&base_path,
git_workdir,
&scanned_files_counter,
&shared_frecency,
mode,
) {
Ok(sync) => sync,
Err(e) => {
error!(?e, "scan walk failed");
return;
}
};
if signals.cancelled.load(Ordering::Acquire) {
info!("walk completed but picker was replaced, discarding results");
return;
}
let git_workdir = sync.git_workdir.clone();
if let Ok(mut guard) = shared_picker.write()
&& let Some(picker) = guard.as_mut()
{
picker.commit_new_sync(sync);
} else {
error!("failed to install scan results into picker");
return;
}
signals.scanning.store(false, Ordering::Relaxed);
if !config.install_watcher && !signals.cancelled.load(Ordering::Acquire) {
resubscribe_to_new_picker(&shared_picker);
}
if !signals.cancelled.load(Ordering::Acquire)
&& let Some(status_handle) = status_handle
{
file_picker::apply_git_status_and_frecency(
&shared_picker,
&shared_frecency,
status_handle,
mode,
);
}
if config.install_watcher && config.watch && !signals.cancelled.load(Ordering::Acquire) {
let shared_picker: &SharedFilePicker = &shared_picker;
let shared_frecency: &SharedFrecency = &shared_frecency;
let base_path: &std::path::Path = &base_path;
match BackgroundWatcher::new(
base_path.to_path_buf(),
git_workdir,
shared_picker.clone(),
shared_frecency.clone(),
mode,
) {
Ok(watcher) => {
if let Ok(mut guard) = shared_picker.write()
&& let Some(picker) = guard.as_mut()
{
picker.background_watcher = Some(watcher);
}
}
Err(e) => error!(?e, "failed to initialize background watcher"),
};
}
if (config.warmup || config.content_indexing) && !signals.cancelled.load(Ordering::Acquire)
{
run_post_scan(&shared_picker, &base_path, &signals, &config);
}
if !signals.cancelled.load(Ordering::Acquire)
&& signals.rescan_pending.swap(false, Ordering::AcqRel)
{
match Self::new(&shared_picker, &shared_frecency, false) {
Ok(Some(follow_up)) => {
info!("Rescheduling deferred rescan after current scan finished");
follow_up.spawn();
}
Ok(None) => {
signals.rescan_pending.store(true, Ordering::Release);
}
Err(e) => {
error!(?e, "Failed to reschedule deferred rescan");
}
}
}
}
}
struct ScanningGuard<'a> {
signals: &'a ScanSignals,
release_watcher_ready_on_drop: bool,
}
impl<'a> ScanningGuard<'a> {
fn new(signals: &'a ScanSignals, release_watcher_ready_on_drop: bool) -> Self {
signals.scanning.store(true, Ordering::Relaxed);
Self {
signals,
release_watcher_ready_on_drop,
}
}
}
impl Drop for ScanningGuard<'_> {
fn drop(&mut self) {
self.signals.scanning.store(false, Ordering::Relaxed);
if self.release_watcher_ready_on_drop {
self.signals.watcher_ready.store(true, Ordering::Release);
}
}
}
fn run_post_scan(
shared_picker: &SharedFilePicker,
base_path: &std::path::Path,
signals: &ScanSignals,
config: &ScanConfig,
) {
let phase_start = std::time::Instant::now();
if config.auto_cache_budget
&& !signals.cancelled.load(Ordering::Acquire)
&& let Ok(mut guard) = shared_picker.write()
&& let Some(picker) = guard.as_mut()
&& !picker.has_explicit_cache_budget()
{
let (files, _, _) = picker.sync_data_snapshot();
picker.set_cache_budget(ContentCacheBudget::new_for_repo(files.len()));
}
let Some((files, indexable_count, budget, arena, _busy_guard)) = shared_picker
.read()
.ok()
.and_then(|guard| guard.as_ref().map(|p| snapshot_sync_data(p, signals)))
else {
return;
};
if config.warmup && !signals.cancelled.load(Ordering::Acquire) {
let t = std::time::Instant::now();
warmup_mmaps(files, &budget, base_path, arena);
info!(
"Warmup completed in {:.2}s (cached {} files, {} bytes)",
t.elapsed().as_secs_f64(),
budget.cached_count.load(Ordering::Relaxed),
budget.cached_bytes.load(Ordering::Relaxed),
);
}
if config.content_indexing && !signals.cancelled.load(Ordering::Acquire) {
let indexable_files = &files[..indexable_count.min(files.len())];
let (index, content_binary) =
build_bigram_index(indexable_files, &budget, base_path, arena);
if let Ok(mut guard) = shared_picker.write()
&& let Some(picker) = guard.as_mut()
{
for &idx in &content_binary {
if let Some(file) = picker.get_file_mut(idx) {
file.set_binary(true);
}
}
picker.set_bigram_index(index, BigramOverlay::new(indexable_count));
}
}
info!(
"Post-scan phase total: {:.2}s (warmup={}, content_indexing={})",
phase_start.elapsed().as_secs_f64(),
config.warmup,
config.content_indexing,
);
}
struct PostScanBusyGuard<'a>(&'a AtomicBool);
impl Drop for PostScanBusyGuard<'_> {
fn drop(&mut self) {
self.0.store(false, Ordering::Release);
}
}
#[tracing::instrument(skip_all)]
fn resubscribe_to_new_picker(shared_picker: &SharedFilePicker) {
let Ok(guard) = shared_picker.read() else {
return;
};
let Some(picker) = guard.as_ref() else {
return;
};
let Some(watcher) = picker.background_watcher.as_ref() else {
return;
};
watcher.request_watch_dir(picker.base_path().to_path_buf());
picker.for_each_dir(|dir: &std::path::Path| {
watcher.request_watch_dir(dir.to_path_buf());
std::ops::ControlFlow::Continue(())
});
}
fn snapshot_sync_data<'a>(
picker: &crate::file_picker::FilePicker,
signals: &'a ScanSignals,
) -> (
&'static [crate::types::FileItem],
usize,
Arc<ContentCacheBudget>,
crate::simd_path::ArenaPtr,
PostScanBusyGuard<'a>,
) {
signals.post_scan_busy.store(true, Ordering::Release);
let busy = PostScanBusyGuard(&signals.post_scan_busy);
let (files, indexable_count, arena) = picker.sync_data_snapshot();
let ptr = files.as_ptr();
let len = files.len();
let static_files: &'static [crate::types::FileItem] =
unsafe { std::slice::from_raw_parts(ptr, len) };
(
static_files,
indexable_count,
picker.cache_budget_arc(),
arena,
busy,
)
}