ralph_core/diagnostics/
performance.rs1use 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 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 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 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}