use crate::checkpoint::execution_history::ExecutionStep;
#[must_use]
pub fn estimate_execution_step_heap_bytes_core_fields(step: &ExecutionStep) -> usize {
step.phase.len()
+ step.step_type.len()
+ step.timestamp.len()
+ step.agent.as_ref().map_or(0, |s| s.len())
}
pub fn estimate_execution_history_heap_bytes_core_fields(steps: &[ExecutionStep]) -> usize {
steps
.iter()
.map(estimate_execution_step_heap_bytes_core_fields)
.sum()
}
#[derive(Debug, Clone)]
pub struct ExecutionHistoryBaseline {
pub entry_count: usize,
pub heap_size_bytes: usize,
pub serialized_size_bytes: usize,
pub tolerance: f64,
}
impl ExecutionHistoryBaseline {
pub const ENTRIES_1000: Self = Self {
entry_count: 1000,
heap_size_bytes: 60_000, serialized_size_bytes: 400_000, tolerance: 1.2, };
pub fn check_heap_size(&self, measured: usize) -> Result<(), String> {
let max_allowed = tolerance_ceiling(self.heap_size_bytes, self.tolerance);
if measured > max_allowed {
Err(format!(
"Heap size {} bytes exceeds baseline {} bytes (tolerance: {}x)",
measured, max_allowed, self.tolerance
))
} else {
Ok(())
}
}
pub fn check_serialized_size(&self, measured: usize) -> Result<(), String> {
let max_allowed = tolerance_ceiling(self.serialized_size_bytes, self.tolerance);
if measured > max_allowed {
Err(format!(
"Serialized size {} bytes exceeds baseline {} bytes (tolerance: {}x)",
measured, max_allowed, self.tolerance
))
} else {
Ok(())
}
}
}
fn tolerance_ceiling(baseline: usize, tolerance: f64) -> usize {
if !tolerance.is_finite() {
return usize::MAX;
}
let baseline_f = baseline.to_string().parse::<f64>().unwrap_or(f64::MAX);
let scaled = baseline_f * tolerance;
if !scaled.is_finite() {
return usize::MAX;
}
let ceil = scaled.ceil();
if ceil <= 0.0 {
return 0;
}
let max_f = usize::MAX.to_string().parse::<f64>().unwrap_or(f64::MAX);
if ceil >= max_f {
return usize::MAX;
}
let ceil_str = format!("{ceil:.0}");
ceil_str.parse::<usize>().unwrap_or(usize::MAX)
}
#[derive(Debug, Clone)]
pub struct CheckpointSerializationBaseline {
pub entry_count: usize,
pub serialize_us: u64,
pub deserialize_us: u64,
pub tolerance: f64,
}
impl CheckpointSerializationBaseline {
pub const ENTRIES_1000: Self = Self {
entry_count: 1000,
serialize_us: 5_000, deserialize_us: 5_000, tolerance: 2.0, };
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checkpoint::execution_history::{ExecutionStep, StepOutcome};
#[test]
fn test_execution_history_baseline_magnitude_is_reasonable() {
let baseline = ExecutionHistoryBaseline::ENTRIES_1000;
assert!(baseline.heap_size_bytes > 40_000);
assert!(baseline.heap_size_bytes < 100_000);
}
#[test]
fn test_baseline_check_within_tolerance() {
let baseline = ExecutionHistoryBaseline::ENTRIES_1000;
assert!(baseline.check_heap_size(70_000).is_ok());
assert!(baseline.check_heap_size(80_000).is_err());
}
#[test]
fn test_execution_history_heap_estimator_counts_only_core_fields() {
let mut step = ExecutionStep::new(
"Development",
1,
"agent_invoked",
StepOutcome::success(Some("output".to_string()), vec!["file.rs".to_string()]),
)
.with_agent("test-agent")
.with_duration(5);
step.timestamp.reserve_exact(64);
assert!(
step.timestamp.capacity() > step.timestamp.len(),
"test invariant: timestamp capacity should exceed length"
);
let expected = step.phase.len()
+ step.step_type.len()
+ step.timestamp.len()
+ step.agent.as_ref().map_or(0, |s| s.len());
assert_eq!(
estimate_execution_step_heap_bytes_core_fields(&step),
expected
);
}
#[test]
fn test_memory_optimization_regression() {
use crate::checkpoint::StringPool;
let pool = StringPool::new();
let (step, pool) = ExecutionStep::new_with_pool(
"Development",
1,
"agent_invoked",
StepOutcome::success(Some("output".to_string()), vec!["file.rs".to_string()]),
pool,
);
let (step, _pool) = step.with_agent_pooled("test-agent", pool);
let step = step.with_duration(5);
let heap_size = estimate_execution_step_heap_bytes_core_fields(&step);
assert!(
heap_size <= 60,
"Memory regression: {heap_size} bytes per entry exceeds 60 byte target (expected ~40-45 bytes)"
);
}
#[test]
fn test_string_pool_deduplication_regression() {
use crate::checkpoint::StringPool;
use std::sync::Arc;
let pool = StringPool::new();
let (step1, pool) = ExecutionStep::new_with_pool(
"Development",
1,
"dev_run",
StepOutcome::success(None, vec![]),
pool,
);
let (step1, pool) = step1.with_agent_pooled("claude", pool);
let (step2, pool) = ExecutionStep::new_with_pool(
"Development",
2,
"dev_run",
StepOutcome::success(None, vec![]),
pool,
);
let (step2, pool) = step2.with_agent_pooled("claude", pool);
assert!(
Arc::ptr_eq(&step1.phase, &step2.phase),
"String pool regression: phase strings not shared"
);
assert!(
Arc::ptr_eq(step1.agent.as_ref().unwrap(), step2.agent.as_ref().unwrap()),
"String pool regression: agent strings not shared"
);
assert_eq!(
pool.len(),
2,
"String pool regression: expected 2 unique strings, got {}",
pool.len()
);
}
}