grok-build-upstream-mirror/crates/codegen/xai-codebase-graph/tests/incremental_memory.rs

136 lines
4.4 KiB
Rust
Raw Normal View History

//! Isolated RSS test for incremental reindexing.
//!
//! This test lives in its own integration-test file (and therefore its own
//! Bazel `rust_test` target / process) so that its whole-process RSS samples
//! are not polluted by the other allocation-heavy tests in
//! `memory_integration.rs` (e.g. `test_fresh_build_rss`,
//! `test_build_batch_peak_rss_is_bounded`, `test_compact_reduces_rss_vs_uncompacted`).
//!
//! Background: `libtest` runs tests in a single binary concurrently across
//! `num_cpus` threads, and VmRSS is measured per-*process*. When this test
//! ran inside `memory_integration.rs` it observed allocator churn from the
//! other tests on the same process, intermittently pushing the measured
//! "incremental growth" delta over the 20 MB budget on aarch64 fastbuild CI
//! (`run_1_of_2` and `run_2_of_2` both failed at ~31 MB).
//!
//! Keep this file to a single test. If you need to add another RSS-sensitive
//! test, give it its own file too rather than reintroducing the
//! noisy-neighbor problem.
use std::fs;
use std::path::Path;
use tempfile::tempdir;
use xai_codebase_graph::{FileEvent, IndexManager, IndexManagerConfig};
/// Read current process RSS in bytes. Supports Linux and macOS.
/// Returns `None` on unsupported platforms.
fn rss_bytes() -> Option<usize> {
#[cfg(target_os = "linux")]
{
let status = std::fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
if let Some(val) = line.strip_prefix("VmRSS:") {
let kb: usize = val.trim().trim_end_matches(" kB").trim().parse().ok()?;
return Some(kb * 1024);
}
}
None
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
let output = Command::new("ps")
.args(["-o", "rss=", "-p", &std::process::id().to_string()])
.output()
.ok()?;
let kb: usize = String::from_utf8_lossy(&output.stdout)
.trim()
.parse()
.ok()?;
Some(kb * 1024)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}
fn rss_mb() -> Option<f64> {
rss_bytes().map(|b| b as f64 / (1024.0 * 1024.0))
}
fn fmt_rss(rss: Option<f64>) -> String {
rss.map_or("N/A".to_string(), |v| format!("{:.1}MB", v))
}
/// Create N Rust source files in `dir`, each with `defs_per_file` function defs.
fn create_rust_files(dir: &Path, count: usize, defs_per_file: usize) {
for i in 0..count {
let mut content = String::new();
for d in 0..defs_per_file {
content.push_str(&format!("fn func_{}_{}() {{}}\n", i, d));
}
fs::write(dir.join(format!("file_{}.rs", i)), &content).unwrap();
}
}
#[test]
fn test_bulk_incremental_indexing_memory() {
let dir = tempdir().unwrap();
let root = dir.path();
create_rust_files(root, 500, 10);
let rss_before = rss_mb();
let config = IndexManagerConfig::new(root.to_path_buf())
.without_cache_load()
.without_cache_save();
let handle = IndexManager::spawn(config);
let stats = handle.get_stats().unwrap();
let rss_after_build = rss_mb();
println!(
"Initial build: {} files, {} defs, {} refs",
stats.files, stats.definitions, stats.references
);
println!(
"RSS: {} before → {} after build",
fmt_rss(rss_before),
fmt_rss(rss_after_build)
);
assert_eq!(stats.files, 500);
assert!(stats.definitions >= 5000);
for i in 0..100 {
let path = root.join(format!("file_{}.rs", i));
fs::write(&path, "fn modified() {}\nfn also_modified() {}\n").unwrap();
handle.send_event(FileEvent::modified(path)).unwrap();
}
let stats_after = handle.get_stats().unwrap();
let rss_after_incremental = rss_mb();
println!(
"After 100 incremental reindexes: {} files, {} defs",
stats_after.files, stats_after.definitions
);
println!("RSS after incremental: {}", fmt_rss(rss_after_incremental));
// Incremental reindexing should not grow memory significantly.
if let (Some(after_inc), Some(after_build)) = (rss_after_incremental, rss_after_build) {
let growth = after_inc - after_build;
assert!(
growth < 20.0,
"Incremental reindex grew RSS by {:.1}MB (expected <20MB)",
growth
);
}
handle.shutdown().unwrap();
}