Skip to main content

ralph_core/diagnostics/
performance.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fs::{File, OpenOptions};
4use std::io::{BufWriter, Write};
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct PerformanceEntry {
9    pub timestamp: DateTime<Utc>,
10    pub iteration: u32,
11    pub hat: String,
12    pub metric: PerformanceMetric,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum PerformanceMetric {
18    IterationDuration { duration_ms: u64 },
19    AgentLatency { duration_ms: u64 },
20    TokenCount { input: usize, output: usize },
21}
22
23pub struct PerformanceLogger {
24    writer: BufWriter<File>,
25}
26
27impl PerformanceLogger {
28    pub fn new(session_dir: &Path) -> std::io::Result<Self> {
29        let log_file = session_dir.join("performance.jsonl");
30        let file = OpenOptions::new()
31            .create(true)
32            .append(true)
33            .open(log_file)?;
34        let writer = BufWriter::new(file);
35        Ok(Self { writer })
36    }
37
38    pub fn log(
39        &mut self,
40        iteration: u32,
41        hat: &str,
42        metric: PerformanceMetric,
43    ) -> std::io::Result<()> {
44        let entry = PerformanceEntry {
45            timestamp: Utc::now(),
46            iteration,
47            hat: hat.to_string(),
48            metric,
49        };
50
51        serde_json::to_writer(&mut self.writer, &entry)?;
52        writeln!(&mut self.writer)?;
53        self.writer.flush()?;
54        Ok(())
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use std::io::BufRead;
62    use tempfile::TempDir;
63
64    #[test]
65    fn test_performance_logger_creates_file() {
66        let temp = TempDir::new().unwrap();
67        let _logger = PerformanceLogger::new(temp.path()).unwrap();
68
69        let log_file = temp.path().join("performance.jsonl");
70        assert!(log_file.exists(), "performance.jsonl should be created");
71    }
72
73    #[test]
74    fn test_all_metric_types_serialize() {
75        let temp = TempDir::new().unwrap();
76        let mut logger = PerformanceLogger::new(temp.path()).unwrap();
77
78        // Log all metric types
79        logger
80            .log(
81                1,
82                "ralph",
83                PerformanceMetric::IterationDuration { duration_ms: 1500 },
84            )
85            .unwrap();
86        logger
87            .log(
88                1,
89                "builder",
90                PerformanceMetric::AgentLatency { duration_ms: 800 },
91            )
92            .unwrap();
93        logger
94            .log(
95                1,
96                "builder",
97                PerformanceMetric::TokenCount {
98                    input: 1000,
99                    output: 500,
100                },
101            )
102            .unwrap();
103
104        // Read back and verify
105        drop(logger);
106        let log_file = temp.path().join("performance.jsonl");
107        let file = File::open(log_file).unwrap();
108        let reader = std::io::BufReader::new(file);
109        let lines: Vec<_> = reader.lines().collect::<Result<_, _>>().unwrap();
110
111        assert_eq!(lines.len(), 3, "Should have 3 log entries");
112
113        // Verify each line is valid JSON
114        let entry1: PerformanceEntry = serde_json::from_str(&lines[0]).unwrap();
115        assert_eq!(entry1.iteration, 1);
116        assert_eq!(entry1.hat, "ralph");
117        assert!(matches!(
118            entry1.metric,
119            PerformanceMetric::IterationDuration { duration_ms: 1500 }
120        ));
121
122        let entry2: PerformanceEntry = serde_json::from_str(&lines[1]).unwrap();
123        assert_eq!(entry2.iteration, 1);
124        assert_eq!(entry2.hat, "builder");
125        assert!(matches!(
126            entry2.metric,
127            PerformanceMetric::AgentLatency { duration_ms: 800 }
128        ));
129
130        let entry3: PerformanceEntry = serde_json::from_str(&lines[2]).unwrap();
131        assert_eq!(entry3.iteration, 1);
132        assert_eq!(entry3.hat, "builder");
133        assert!(matches!(
134            entry3.metric,
135            PerformanceMetric::TokenCount {
136                input: 1000,
137                output: 500
138            }
139        ));
140    }
141}