use std::fmt::Write;
use std::time::Instant;
use crate::benchmarking::rss::{read_current_rss_kb, read_peak_rss_kb};
#[derive(Debug, Clone)]
pub struct MemorySnapshot {
pub label: String,
pub timestamp: Instant,
pub rss_kb: Option<u64>,
pub peak_rss_kb: Option<u64>,
}
#[derive(Debug, Clone)]
pub struct MemoryDelta {
pub from_label: String,
pub to_label: String,
pub rss_delta_kb: Option<i64>,
}
#[derive(Debug, Clone)]
pub struct MemoryReport {
pub snapshots: Vec<MemorySnapshot>,
pub peak_rss_kb: Option<u64>,
pub deltas: Vec<MemoryDelta>,
}
pub struct MemoryTracker {
snapshots: Vec<MemorySnapshot>,
}
impl MemoryTracker {
pub fn new() -> Self {
Self {
snapshots: Vec::new(),
}
}
pub fn snapshot(&mut self, label: &str) {
self.snapshots.push(MemorySnapshot {
label: label.to_string(),
timestamp: Instant::now(),
rss_kb: read_current_rss_kb(),
peak_rss_kb: read_peak_rss_kb(),
});
}
pub fn report(&self) -> MemoryReport {
let deltas = self.compute_deltas();
let peak_rss_kb = self.snapshots.iter().filter_map(|s| s.peak_rss_kb).max();
MemoryReport {
snapshots: self.snapshots.clone(),
peak_rss_kb,
deltas,
}
}
fn compute_deltas(&self) -> Vec<MemoryDelta> {
self.snapshots
.windows(2)
.map(|pair| {
let rss_delta_kb = match (pair[0].rss_kb, pair[1].rss_kb) {
(Some(a), Some(b)) => Some(b as i64 - a as i64),
_ => None,
};
MemoryDelta {
from_label: pair[0].label.clone(),
to_label: pair[1].label.clone(),
rss_delta_kb,
}
})
.collect()
}
}
impl Default for MemoryTracker {
fn default() -> Self {
Self::new()
}
}
impl MemoryReport {
pub fn peak_rss_kb(&self) -> Option<u64> {
self.peak_rss_kb
}
pub fn snapshots(&self) -> &[MemorySnapshot] {
&self.snapshots
}
pub fn deltas(&self) -> &[MemoryDelta] {
&self.deltas
}
pub fn display_report(&self) -> String {
let mut out = String::new();
writeln!(out, "Memory Report").unwrap();
writeln!(out, "{:─<56}", "").unwrap();
writeln!(
out,
" {:<22} {:>10} {:>10} {:>10}",
"Label", "RSS (KB)", "Peak (KB)", "Delta (KB)"
)
.unwrap();
for (i, snapshot) in self.snapshots.iter().enumerate() {
let rss = format_opt_u64(snapshot.rss_kb);
let peak = format_opt_u64(snapshot.peak_rss_kb);
let delta = if i == 0 {
"\u{2014}".to_string() } else {
match &self.deltas.get(i - 1) {
Some(d) => format_opt_delta(d.rss_delta_kb),
None => "N/A".to_string(),
}
};
writeln!(
out,
" {:<22} {:>10} {:>10} {:>10}",
snapshot.label, rss, peak, delta
)
.unwrap();
}
writeln!(out, "{:─<56}", "").unwrap();
match self.peak_rss_kb {
Some(peak) => {
let mb = peak as f64 / 1024.0;
writeln!(out, " Peak RSS: {peak} KB ({mb:.1} MB)").unwrap();
}
None => {
writeln!(out, " Peak RSS: N/A").unwrap();
}
}
out
}
}
fn format_opt_u64(val: Option<u64>) -> String {
match val {
Some(v) => format_with_commas(v),
None => "N/A".to_string(),
}
}
fn format_opt_delta(val: Option<i64>) -> String {
match val {
Some(v) if v >= 0 => format!("+{}", format_with_commas(v as u64)),
Some(v) => format!("-{}", format_with_commas((-v) as u64)),
None => "N/A".to_string(),
}
}
fn format_with_commas(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
result.chars().rev().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_snapshot_construction() {
let snap = MemorySnapshot {
label: "test".to_string(),
timestamp: Instant::now(),
rss_kb: Some(1024),
peak_rss_kb: Some(2048),
};
assert_eq!(snap.label, "test");
assert_eq!(snap.rss_kb, Some(1024));
assert_eq!(snap.peak_rss_kb, Some(2048));
}
#[test]
fn memory_snapshot_none_values() {
let snap = MemorySnapshot {
label: "no_rss".to_string(),
timestamp: Instant::now(),
rss_kb: None,
peak_rss_kb: None,
};
assert_eq!(snap.rss_kb, None);
assert_eq!(snap.peak_rss_kb, None);
}
#[test]
fn memory_delta_construction() {
let delta = MemoryDelta {
from_label: "start".to_string(),
to_label: "end".to_string(),
rss_delta_kb: Some(1000),
};
assert_eq!(delta.from_label, "start");
assert_eq!(delta.to_label, "end");
assert_eq!(delta.rss_delta_kb, Some(1000));
}
#[test]
fn memory_delta_none() {
let delta = MemoryDelta {
from_label: "a".to_string(),
to_label: "b".to_string(),
rss_delta_kb: None,
};
assert_eq!(delta.rss_delta_kb, None);
}
#[test]
fn new_tracker_starts_empty() {
let tracker = MemoryTracker::new();
let report = tracker.report();
assert!(report.snapshots().is_empty());
assert!(report.deltas().is_empty());
assert!(report.peak_rss_kb().is_none());
}
#[test]
fn snapshot_adds_entries() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("first");
tracker.snapshot("second");
let report = tracker.report();
assert_eq!(report.snapshots().len(), 2);
assert_eq!(report.snapshots()[0].label, "first");
assert_eq!(report.snapshots()[1].label, "second");
}
#[test]
fn snapshots_in_correct_order() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("a");
tracker.snapshot("b");
tracker.snapshot("c");
let report = tracker.report();
let labels: Vec<&str> = report
.snapshots()
.iter()
.map(|s| s.label.as_str())
.collect();
assert_eq!(labels, vec!["a", "b", "c"]);
}
#[test]
fn report_computes_deltas_between_consecutive() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("start");
let _data: Vec<u8> = vec![0; 1024 * 1024]; tracker.snapshot("after_alloc");
let report = tracker.report();
assert_eq!(report.deltas().len(), 1);
assert_eq!(report.deltas()[0].from_label, "start");
assert_eq!(report.deltas()[0].to_label, "after_alloc");
}
#[test]
#[cfg(target_os = "linux")]
fn peak_rss_returns_maximum() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("s1");
tracker.snapshot("s2");
let report = tracker.report();
assert!(report.peak_rss_kb().is_some());
let peak = report.peak_rss_kb().unwrap();
for snap in report.snapshots() {
if let Some(p) = snap.peak_rss_kb {
assert!(peak >= p);
}
}
}
#[test]
fn empty_tracker_produces_empty_report() {
let tracker = MemoryTracker::new();
let report = tracker.report();
assert!(report.snapshots().is_empty());
assert!(report.deltas().is_empty());
assert!(report.peak_rss_kb().is_none());
}
#[test]
fn display_report_contains_header() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("test");
let report = tracker.report();
let display = report.display_report();
assert!(display.contains("Memory Report"));
assert!(display.contains("Label"));
assert!(display.contains("RSS (KB)"));
assert!(display.contains("Peak (KB)"));
assert!(display.contains("Delta (KB)"));
}
#[test]
fn display_report_contains_snapshot_labels() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("baseline");
tracker.snapshot("after_load");
let report = tracker.report();
let display = report.display_report();
assert!(display.contains("baseline"));
assert!(display.contains("after_load"));
}
#[test]
fn display_report_shows_peak_footer() {
let mut tracker = MemoryTracker::new();
tracker.snapshot("test");
let report = tracker.report();
let display = report.display_report();
assert!(display.contains("Peak RSS:"));
}
#[test]
fn display_report_handles_none_gracefully() {
let report = MemoryReport {
snapshots: vec![MemorySnapshot {
label: "no_data".to_string(),
timestamp: Instant::now(),
rss_kb: None,
peak_rss_kb: None,
}],
peak_rss_kb: None,
deltas: Vec::new(),
};
let display = report.display_report();
assert!(display.contains("N/A"));
}
#[test]
fn format_with_commas_works() {
assert_eq!(format_with_commas(0), "0");
assert_eq!(format_with_commas(999), "999");
assert_eq!(format_with_commas(1000), "1,000");
assert_eq!(format_with_commas(1000000), "1,000,000");
assert_eq!(format_with_commas(12345), "12,345");
}
#[test]
fn format_opt_delta_positive() {
assert_eq!(format_opt_delta(Some(1000)), "+1,000");
}
#[test]
fn format_opt_delta_negative() {
assert_eq!(format_opt_delta(Some(-500)), "-500");
}
#[test]
fn format_opt_delta_none() {
assert_eq!(format_opt_delta(None), "N/A");
}
}