#![cfg(feature = "daemon")]
#[path = "common/mod.rs"]
mod common;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use codenexus::daemon::{Daemon, DaemonEvent, EventObserver, DEFAULT_DEBOUNCE_MS};
use codenexus::index::IndexFacade;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion, Throughput};
use tempfile::TempDir;
use common::generate_large_repo;
const PROJECT_NAME: &str = "daemon_bench";
const FILE_COUNT: usize = 1_000;
const LANGUAGES: &[&str] = &["rs", "c", "f90", "py", "ts"];
const DEBOUNCE_MS: u64 = DEFAULT_DEBOUNCE_MS;
const THROUGHPUT_EVENT_COUNT: usize = 50;
const OBSERVER_TIMEOUT: Duration = Duration::from_secs(30);
const WATCHER_INIT_DELAY: Duration = Duration::from_millis(500);
struct CountingIndexObserver {
facade: IndexFacade,
project_name: String,
watch_path: PathBuf,
index_count: Arc<AtomicUsize>,
event_count: Arc<AtomicUsize>,
}
impl EventObserver for CountingIndexObserver {
fn on_events(&mut self, events: &[DaemonEvent]) {
self.event_count.fetch_add(events.len(), Ordering::SeqCst);
let _ = self
.facade
.index_incremental(&self.watch_path, &self.project_name, false);
self.index_count.fetch_add(1, Ordering::SeqCst);
}
}
struct DaemonBenchState {
dir: TempDir,
index_count: Arc<AtomicUsize>,
event_count: Arc<AtomicUsize>,
stop_handle: Arc<AtomicBool>,
handle: std::thread::JoinHandle<()>,
}
fn setup_daemon() -> DaemonBenchState {
let dir = generate_large_repo(FILE_COUNT);
let db_path = dir.path().join("bench.db");
let facade = IndexFacade::new(&db_path).expect("IndexFacade::new");
facade
.index(dir.path(), PROJECT_NAME, false)
.expect("warm-up index");
let index_count = Arc::new(AtomicUsize::new(0));
let event_count = Arc::new(AtomicUsize::new(0));
let observer = CountingIndexObserver {
facade,
project_name: PROJECT_NAME.to_string(),
watch_path: dir.path().to_path_buf(),
index_count: Arc::clone(&index_count),
event_count: Arc::clone(&event_count),
};
let mut daemon = Daemon::new(dir.path(), PROJECT_NAME, DEBOUNCE_MS, &db_path);
daemon.add_observer(Box::new(observer));
let stop_handle = daemon.stop_handle();
let handle = std::thread::spawn(move || {
let _ = daemon.run();
});
std::thread::sleep(WATCHER_INIT_DELAY);
DaemonBenchState {
dir,
index_count,
event_count,
stop_handle,
handle,
}
}
fn teardown_daemon(state: DaemonBenchState) {
state.stop_handle.store(true, Ordering::SeqCst);
let _ = state.handle.join();
}
fn modify_file(dir: &Path, i: usize) {
let ext = LANGUAGES[i % LANGUAGES.len()];
let path = dir.join(format!("file_{i}.{ext}"));
let original = std::fs::read_to_string(&path).expect("read fixture for modify");
let suffix = match ext {
"f90" => format!("\n! bench modify {i}\n"),
_ => format!("\n// bench modify {i}\n"),
};
std::fs::write(&path, format!("{original}{suffix}")).expect("write modified fixture");
}
fn bench_debounce_response_latency(c: &mut Criterion) {
let mut group = c.benchmark_group("daemon");
group.sample_size(10);
group.measurement_time(Duration::from_secs(300));
group.bench_function("debounce_response_latency", |b| {
b.iter_batched(
setup_daemon,
|state| {
let initial = state.index_count.load(Ordering::SeqCst);
modify_file(state.dir.path(), 0);
let deadline = Instant::now() + OBSERVER_TIMEOUT;
while state.index_count.load(Ordering::SeqCst) <= initial {
if Instant::now() > deadline {
panic!("daemon did not process modify event within 30 s");
}
std::thread::sleep(Duration::from_millis(50));
}
teardown_daemon(state);
},
BatchSize::PerIteration,
);
});
group.finish();
}
fn bench_indexing_event_queue_throughput(c: &mut Criterion) {
let mut group = c.benchmark_group("daemon");
group.throughput(Throughput::Elements(THROUGHPUT_EVENT_COUNT as u64));
group.sample_size(10);
group.measurement_time(Duration::from_secs(300));
group.bench_function("indexing_event_queue_throughput", |b| {
b.iter_batched(
setup_daemon,
|state| {
let initial_events = state.event_count.load(Ordering::SeqCst);
for i in 0..THROUGHPUT_EVENT_COUNT {
modify_file(state.dir.path(), i);
}
let deadline = Instant::now() + OBSERVER_TIMEOUT;
while state.event_count.load(Ordering::SeqCst) - initial_events
< THROUGHPUT_EVENT_COUNT
{
if Instant::now() > deadline {
panic!(
"daemon did not process {THROUGHPUT_EVENT_COUNT} events within 30 s"
);
}
std::thread::sleep(Duration::from_millis(50));
}
let processed_events = state.event_count.load(Ordering::SeqCst) - initial_events;
assert!(
processed_events >= THROUGHPUT_EVENT_COUNT,
"event loss: expected >={THROUGHPUT_EVENT_COUNT} events, got {processed_events}"
);
teardown_daemon(state);
black_box(processed_events);
},
BatchSize::PerIteration,
);
});
group.finish();
}
criterion_group!(
benches,
bench_debounce_response_latency,
bench_indexing_event_queue_throughput,
);
criterion_main!(benches);