use super::workspace_index::WorkspaceIndex;
use std::fmt;
#[derive(Clone, Debug, Default)]
pub struct MemorySnapshot {
pub file_count: usize,
pub symbol_count: usize,
pub files_bytes: usize,
pub symbols_bytes: usize,
pub global_refs_bytes: usize,
pub document_store_bytes: usize,
}
impl MemorySnapshot {
pub fn capture(index: &WorkspaceIndex) -> Self {
index.memory_snapshot()
}
pub fn total_estimated_bytes(&self) -> usize {
self.files_bytes + self.symbols_bytes + self.global_refs_bytes + self.document_store_bytes
}
pub fn bytes_per_symbol(&self) -> usize {
if self.symbol_count == 0 {
return 0;
}
self.total_estimated_bytes() / self.symbol_count
}
pub fn bytes_per_file(&self) -> usize {
if self.file_count == 0 {
return 0;
}
self.total_estimated_bytes() / self.file_count
}
pub fn to_report_string(&self) -> String {
let total = self.total_estimated_bytes();
format!(
"MemorySnapshot {{ files: {} ({} B), symbols map: {} ({} B), \
global_refs: {} B, doc_store: {} B, total: {} B, \
{} symbols, {} B/symbol }}",
self.file_count,
self.files_bytes,
self.symbol_count,
self.symbols_bytes,
self.global_refs_bytes,
self.document_store_bytes,
total,
self.symbol_count,
self.bytes_per_symbol(),
)
}
}
impl fmt::Display for MemorySnapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_report_string())
}
}
#[derive(Debug, Default)]
pub struct ScaleReport {
checkpoints: Vec<(usize, MemorySnapshot)>,
}
impl ScaleReport {
pub fn new() -> Self {
Self { checkpoints: Vec::new() }
}
pub fn add_checkpoint(&mut self, file_count: usize, snap: MemorySnapshot) {
self.checkpoints.push((file_count, snap));
}
pub fn checkpoints(&self) -> &[(usize, MemorySnapshot)] {
&self.checkpoints
}
pub fn scaling_factor(&self) -> Option<f64> {
if self.checkpoints.len() < 2 {
return None;
}
let first = self.checkpoints.first()?;
let last = self.checkpoints.last()?;
let mem_first = first.1.total_estimated_bytes();
let mem_last = last.1.total_estimated_bytes();
let file_first = first.0;
let file_last = last.0;
if mem_first == 0 || file_first == 0 || file_last == 0 {
return None;
}
let expected_linear = mem_first as f64 * (file_last as f64 / file_first as f64);
Some(mem_last as f64 / expected_linear)
}
}
impl fmt::Display for ScaleReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{:<10} {:>12} {:>12} {:>12} {:>14}",
"files", "total_B", "symbols", "B/sym", "B/file"
)?;
writeln!(f, "{}", "-".repeat(64))?;
for (file_count, snap) in &self.checkpoints {
writeln!(
f,
"{:<10} {:>12} {:>12} {:>12} {:>14}",
file_count,
snap.total_estimated_bytes(),
snap.symbol_count,
snap.bytes_per_symbol(),
snap.bytes_per_file(),
)?;
}
if let Some(factor) = self.scaling_factor() {
writeln!(f, "\nScaling factor vs linear: {:.2}x", factor)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{MemorySnapshot, ScaleReport};
#[test]
fn memory_snapshot_derived_metrics_handle_zero_counts() -> Result<(), Box<dyn std::error::Error>>
{
let snap = MemorySnapshot::default();
assert_eq!(snap.total_estimated_bytes(), 0);
assert_eq!(snap.bytes_per_symbol(), 0);
assert_eq!(snap.bytes_per_file(), 0);
Ok(())
}
#[test]
fn memory_snapshot_report_includes_aggregated_values() -> Result<(), Box<dyn std::error::Error>>
{
let snap = MemorySnapshot {
file_count: 2,
symbol_count: 4,
files_bytes: 100,
symbols_bytes: 20,
global_refs_bytes: 60,
document_store_bytes: 20,
};
assert_eq!(snap.total_estimated_bytes(), 200);
assert_eq!(snap.bytes_per_symbol(), 50);
assert_eq!(snap.bytes_per_file(), 100);
let report = snap.to_report_string();
assert!(report.contains("total: 200 B"));
assert!(report.contains("4 symbols"));
assert!(report.contains("50 B/symbol"));
Ok(())
}
#[test]
fn scale_report_scaling_factor_tracks_linear_vs_growth()
-> Result<(), Box<dyn std::error::Error>> {
let mut linear = ScaleReport::new();
linear.add_checkpoint(10, MemorySnapshot { files_bytes: 100, ..MemorySnapshot::default() });
linear.add_checkpoint(20, MemorySnapshot { files_bytes: 200, ..MemorySnapshot::default() });
let linear_factor =
linear.scaling_factor().ok_or("expected linear scaling factor to exist")?;
assert!((linear_factor - 1.0).abs() < 1e-9, "expected ~1.0, got {linear_factor}");
let mut super_linear = ScaleReport::new();
super_linear
.add_checkpoint(10, MemorySnapshot { files_bytes: 100, ..MemorySnapshot::default() });
super_linear
.add_checkpoint(20, MemorySnapshot { files_bytes: 300, ..MemorySnapshot::default() });
let super_linear_factor =
super_linear.scaling_factor().ok_or("expected super-linear scaling factor to exist")?;
assert!(
(super_linear_factor - 1.5).abs() < 1e-9,
"expected ~1.5, got {super_linear_factor}"
);
Ok(())
}
#[test]
fn scale_report_scaling_factor_returns_none_for_insufficient_data()
-> Result<(), Box<dyn std::error::Error>> {
let mut report = ScaleReport::new();
assert_eq!(report.scaling_factor(), None);
report.add_checkpoint(0, MemorySnapshot { files_bytes: 100, ..MemorySnapshot::default() });
report.add_checkpoint(20, MemorySnapshot { files_bytes: 200, ..MemorySnapshot::default() });
assert_eq!(report.scaling_factor(), None);
Ok(())
}
#[test]
fn scale_report_display_contains_table_and_scaling_factor()
-> Result<(), Box<dyn std::error::Error>> {
let mut report = ScaleReport::new();
report.add_checkpoint(
10,
MemorySnapshot {
file_count: 10,
symbol_count: 10,
files_bytes: 100,
..MemorySnapshot::default()
},
);
report.add_checkpoint(
20,
MemorySnapshot {
file_count: 20,
symbol_count: 20,
files_bytes: 200,
..MemorySnapshot::default()
},
);
let display = format!("{report}");
assert!(display.contains("files"));
assert!(display.contains("total_B"));
assert!(display.contains("Scaling factor vs linear: 1.00x"));
Ok(())
}
}