#[path = "common/mod.rs"]
mod common;
use std::path::Path;
#[cfg(feature = "daemon")]
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use codenexus::index::IndexFacade;
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
#[cfg(feature = "daemon")]
use codenexus::daemon::{Daemon, DaemonEvent, EventObserver};
use common::{generate_large_repo, measure_peak_rss};
const PROJECT_NAME: &str = "memory_bench";
const ONE_GB: u64 = 1_073_741_824;
const TWO_HUNDRED_MB: u64 = 209_715_200;
const FIFTY_MB: u64 = 52_428_800;
const LARGE_REPO_FILE_COUNT: usize = 10_000;
const COMPARISON_FILE_COUNT: usize = 1_000;
const SUSTAINED_INCREMENTS: usize = 10;
fn is_ignored_enabled() -> bool {
std::env::var("BENCH_RUN_IGNORED")
.map(|v| v == "1")
.unwrap_or(false)
}
fn modify_rust_file(dir: &Path, i: usize) {
let path = dir.join(format!("file_{i}.rs"));
let original = std::fs::read_to_string(&path).expect("read fixture for modify");
std::fs::write(&path, format!("{original}// bench modify {i}\n"))
.expect("write modified fixture");
}
fn bench_first_index_10k_files_peak(c: &mut Criterion) {
if !is_ignored_enabled() {
return;
}
let mut group = c.benchmark_group("memory");
group.sample_size(10);
group.bench_function("first_index_10k_files_peak", |b| {
b.iter_batched(
|| {
let dir = generate_large_repo(LARGE_REPO_FILE_COUNT);
let db_path = dir.path().join("bench.db");
let facade = IndexFacade::new(&db_path).expect("IndexFacade::new");
(dir, facade)
},
|(dir, facade)| {
let peak = measure_peak_rss(|| {
facade
.index(dir.path(), PROJECT_NAME, false)
.expect("first index");
});
let peak_mb = peak / 1024 / 1024;
eprintln!("first_index_10k_files_peak: peak RSS = {peak} bytes ({peak_mb} MB)");
assert!(
peak <= ONE_GB,
"peak RSS {peak} bytes ({peak_mb} MB) exceeds 1 GB SLO"
);
black_box(peak);
},
BatchSize::PerIteration,
);
});
group.finish();
}
#[cfg(feature = "daemon")]
struct CountingIndexObserver {
facade: IndexFacade,
project_name: String,
watch_path: PathBuf,
index_count: Arc<AtomicUsize>,
}
#[cfg(feature = "daemon")]
impl EventObserver for CountingIndexObserver {
fn on_events(&mut self, _events: &[DaemonEvent]) {
let _ = self
.facade
.index_incremental(&self.watch_path, &self.project_name, false);
self.index_count.fetch_add(1, Ordering::SeqCst);
}
}
#[cfg(feature = "daemon")]
struct DaemonSustainedState {
dir: tempfile::TempDir,
index_count: Arc<AtomicUsize>,
stop_handle: Arc<std::sync::atomic::AtomicBool>,
handle: std::thread::JoinHandle<()>,
}
#[cfg(feature = "daemon")]
fn bench_daemon_sustained_10_increments(c: &mut Criterion) {
if !is_ignored_enabled() {
return;
}
let mut group = c.benchmark_group("memory");
group.sample_size(10);
group.bench_function("daemon_sustained_10_increments", |b| {
b.iter_batched(
|| {
let dir = generate_large_repo(COMPARISON_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 observer = CountingIndexObserver {
facade,
project_name: PROJECT_NAME.to_string(),
watch_path: dir.path().to_path_buf(),
index_count: Arc::clone(&index_count),
};
let mut daemon = Daemon::new(
dir.path(),
PROJECT_NAME,
codenexus::daemon::DEFAULT_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_for_duration(Duration::from_secs(120));
});
DaemonSustainedState {
dir,
index_count,
stop_handle,
handle,
}
},
|state| {
let mut rss_samples: Vec<u64> = Vec::with_capacity(SUSTAINED_INCREMENTS);
let mut sys = sysinfo::System::new();
let pid = sysinfo::Pid::from(std::process::id() as usize);
for i in 0..SUSTAINED_INCREMENTS {
modify_rust_file(state.dir.path(), i);
let expected = i + 1;
let deadline = Instant::now() + Duration::from_secs(30);
while state.index_count.load(Ordering::SeqCst) < expected {
if Instant::now() > deadline {
panic!("daemon did not process increment {expected} within 30 s");
}
std::thread::sleep(Duration::from_millis(100));
}
sys.refresh_process(pid);
let rss = sys.process(pid).map(|p| p.memory()).unwrap_or(0);
let rss_mb = rss / 1024 / 1024;
eprintln!(
"daemon_sustained increment {expected}: RSS = {rss} bytes ({rss_mb} MB)"
);
assert!(
rss <= TWO_HUNDRED_MB,
"RSS {rss} bytes ({rss_mb} MB) exceeds 200 MB SLO at increment {expected}"
);
rss_samples.push(rss);
}
state.stop_handle.store(true, Ordering::SeqCst);
let _ = state.handle.join();
if let (Some(&first), Some(&last)) = (rss_samples.first(), rss_samples.last()) {
let growth = last.saturating_sub(first);
let growth_mb = growth / 1024 / 1024;
eprintln!("daemon_sustained: RSS growth = {growth} bytes ({growth_mb} MB)");
assert!(
growth <= FIFTY_MB,
"RSS growth {growth} bytes ({growth_mb} MB) exceeds 50 MB leak ceiling"
);
}
black_box(rss_samples);
},
BatchSize::PerIteration,
);
});
group.finish();
}
fn bench_ram_first_vs_default_comparison(c: &mut Criterion) {
let mut group = c.benchmark_group("memory");
group.sample_size(10);
group.measurement_time(Duration::from_secs(300));
group.bench_function("ram_first_vs_default_comparison", |b| {
b.iter_batched(
|| {
let dir = generate_large_repo(COMPARISON_FILE_COUNT);
let db_path = dir.path().join("bench.db");
let facade = IndexFacade::new(&db_path).expect("IndexFacade::new");
(dir, facade)
},
|(dir, facade)| {
let default_peak = measure_peak_rss(|| {
facade
.index(dir.path(), PROJECT_NAME, false)
.expect("default index");
});
let ram_peak = measure_peak_rss(|| {
facade
.index_ram_first(dir.path(), PROJECT_NAME, true)
.expect("ram-first index");
});
let delta = default_peak as i64 - ram_peak as i64;
let default_mb = default_peak / 1024 / 1024;
let ram_mb = ram_peak / 1024 / 1024;
let delta_mb = delta / 1024 / 1024;
eprintln!(
"ram_first_vs_default: default={default_peak} bytes ({default_mb} MB), \
ram_first={ram_peak} bytes ({ram_mb} MB), \
delta={delta} bytes ({delta_mb} MB)"
);
black_box((default_peak, ram_peak, delta));
},
BatchSize::PerIteration,
);
});
group.finish();
}
#[cfg(feature = "daemon")]
criterion_group!(
benches,
bench_first_index_10k_files_peak,
bench_daemon_sustained_10_increments,
bench_ram_first_vs_default_comparison,
);
#[cfg(not(feature = "daemon"))]
criterion_group!(
benches,
bench_first_index_10k_files_peak,
bench_ram_first_vs_default_comparison,
);
criterion_main!(benches);