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
68                .phase
69                .len()
70                .saturating_add(step.step_type.len())
71                .saturating_add(step.timestamp.len())
72                .saturating_add(step.agent.as_ref().map_or(0, |s| s.len()))
73                .saturating_add(
74                    step.checkpoint_saved_at
75                        .as_ref()
76                        .map_or(0, std::string::String::len),
77                )
78                .saturating_add(
79                    step.git_commit_oid
80                        .as_ref()
81                        .map_or(0, std::string::String::len),
82                )
83                .saturating_add(
84                    step.prompt_used
85                        .as_ref()
86                        .map_or(0, std::string::String::len),
87                )
88                .saturating_add(modified_files_detail_size)
89                .saturating_add(issues_summary_size);
90
91            let outcome_size = match &step.outcome {
92                StepOutcome::Success {
93                    output,
94                    files_modified,
95                    ..
96                } => output.as_ref().map_or(0, |s| s.len()).saturating_add(
97                    files_modified.as_ref().map_or(0, |files| {
98                        files.iter().map(std::string::String::len).sum::<usize>()
99                    }),
100                ),
101                StepOutcome::Failure { error, signals, .. } => {
102                    error
103                        .len()
104                        .saturating_add(signals.as_ref().map_or(0, |sigs| {
105                            sigs.iter().map(std::string::String::len).sum::<usize>()
106                        }))
107                }
108                StepOutcome::Partial {
109                    completed,
110                    remaining,
111                    ..
112                } => completed.len().saturating_add(remaining.len()),
113                StepOutcome::Skipped { reason } => reason.len(),
114            };
115
116            base_size.saturating_add(outcome_size)
117        })
118        .sum()
119}