Skip to main content

ralph_workflow/monitoring/memory_metrics/
snapshot.rs

1// Memory snapshot types and heap-size estimation.
2
3use serde::{Deserialize, Serialize};
4
5/// Memory usage snapshot at a point in time.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct MemorySnapshot {
8    /// Pipeline iteration when snapshot was taken
9    pub iteration: u32,
10    /// Execution history length
11    pub execution_history_len: usize,
12    /// Deterministic size proxy for execution history (bytes).
13    ///
14    /// This is not a true allocator-backed heap measurement. It uses string lengths as
15    /// a stable, platform-independent proxy suitable for regression tracking.
16    pub execution_history_heap_bytes: usize,
17    /// Checkpoint saved count
18    pub checkpoint_count: u32,
19    /// Timestamp when snapshot was taken (ISO 8601)
20    pub timestamp: String,
21}
22
23impl MemorySnapshot {
24    /// Create a snapshot from current pipeline state.
25    #[must_use]
26    pub fn from_pipeline_state(state: &crate::reducer::PipelineState) -> Self {
27        let execution_history_heap_bytes = estimate_execution_history_heap_size(state);
28
29        Self {
30            iteration: state.iteration,
31            execution_history_len: state.execution_history_len(),
32            execution_history_heap_bytes,
33            checkpoint_count: state.checkpoint_saved_count,
34            timestamp: chrono::Utc::now().to_rfc3339(),
35        }
36    }
37}
38
39/// Estimate a deterministic "heap bytes" proxy for execution history.
40///
41/// Uses string lengths (and collection element lengths) to produce a stable number that
42/// tracks payload growth without depending on allocator behavior.
43pub(super) fn estimate_execution_history_heap_size(state: &crate::reducer::PipelineState) -> usize {
44    use crate::checkpoint::execution_history::StepOutcome;
45
46    state
47        .execution_history()
48        .iter()
49        .map(|step| {
50            let modified_files_detail_size = step.modified_files_detail.as_ref().map_or(0, |d| {
51                let sum_list = |xs: &Option<Box<[String]>>| {
52                    xs.as_ref()
53                        .map_or(0, |v| v.iter().map(std::string::String::len).sum::<usize>())
54                };
55
56                sum_list(&d.added) + sum_list(&d.modified) + sum_list(&d.deleted)
57            });
58
59            let issues_summary_size = step
60                .issues_summary
61                .as_ref()
62                .and_then(|s| s.description.as_ref())
63                .map_or(0, std::string::String::len);
64
65            // Approximate heap allocations: string fields + vec allocations
66            // Use `len()` consistently as a deterministic size proxy.
67            let base_size = step.phase.len()
68                + step.step_type.len()
69                + step.timestamp.len()
70                + step.agent.as_ref().map_or(0, |s| s.len())
71                + step
72                    .checkpoint_saved_at
73                    .as_ref()
74                    .map_or(0, std::string::String::len)
75                + step
76                    .git_commit_oid
77                    .as_ref()
78                    .map_or(0, std::string::String::len)
79                + step
80                    .prompt_used
81                    .as_ref()
82                    .map_or(0, std::string::String::len)
83                + modified_files_detail_size
84                + issues_summary_size;
85
86            let outcome_size = match &step.outcome {
87                StepOutcome::Success {
88                    output,
89                    files_modified,
90                    ..
91                } => {
92                    output.as_ref().map_or(0, |s| s.len())
93                        + files_modified.as_ref().map_or(0, |files| {
94                            files.iter().map(std::string::String::len).sum::<usize>()
95                        })
96                }
97                StepOutcome::Failure { error, signals, .. } => {
98                    error.len()
99                        + signals.as_ref().map_or(0, |sigs| {
100                            sigs.iter().map(std::string::String::len).sum::<usize>()
101                        })
102                }
103                StepOutcome::Partial {
104                    completed,
105                    remaining,
106                    ..
107                } => completed.len() + remaining.len(),
108                StepOutcome::Skipped { reason } => reason.len(),
109            };
110
111            base_size + outcome_size
112        })
113        .sum()
114}