1use serde_json::{json, Value};
6
7use crate::usage::Usage;
8
9#[derive(Debug, Clone)]
10pub struct RoundRecord {
11 pub round: u32,
12 pub summary: String,
14 pub outcome: String,
18 pub value: Option<f64>,
19 pub delta: Option<f64>,
20 pub improvement: Option<f64>,
23 pub policy_area: String,
24 pub direction: String,
25 pub kept: bool,
26}
27
28#[derive(Debug, Clone, Default)]
29pub struct Report {
30 pub objective: String,
31 pub goal: String,
32 pub metric_name: String,
33 pub start_value: Option<f64>,
34 pub best_value: Option<f64>,
35 pub best_round: Option<u32>,
36 pub rounds: Vec<RoundRecord>,
37 pub usage: Usage,
39}
40
41fn fmt_opt(v: Option<f64>) -> String {
42 match v {
43 Some(x) => format!("{x:.3}"),
44 None => "-".to_string(),
45 }
46}
47
48fn pad_left(s: &str, width: usize) -> String {
50 let n = s.chars().count();
51 if n >= width {
52 s.to_string()
53 } else {
54 format!("{}{s}", " ".repeat(width - n))
55 }
56}
57
58fn pad_right(s: &str, width: usize) -> String {
60 let n = s.chars().count();
61 if n >= width {
62 s.to_string()
63 } else {
64 format!("{s}{}", " ".repeat(width - n))
65 }
66}
67
68impl Report {
69 pub fn render_markdown(&self) -> String {
70 let mut s = String::new();
71 s.push_str("# scx_forge_agent optimization report\n\n");
72 s.push_str(&format!(
73 "- Objective: **{} {}**\n",
74 self.goal, self.metric_name
75 ));
76 s.push_str(&format!(
77 "- Starting scheduler value: {}\n",
78 fmt_opt(self.start_value)
79 ));
80 s.push_str(&format!(
81 "- Best scheduler value: {}{}\n",
82 fmt_opt(self.best_value),
83 match self.best_round {
84 Some(r) => format!(" (round {r})"),
85 None => String::new(),
86 }
87 ));
88 if let (Some(start), Some(best)) = (self.start_value, self.best_value) {
89 if start != 0.0 {
90 let pct = if self.goal == "minimize" {
91 (start - best) / start * 100.0
92 } else {
93 (best - start) / start * 100.0
94 };
95 s.push_str(&format!("- Improvement over start: {pct:.2}%\n"));
96 }
97 }
98 s.push('\n');
99
100 let cells: Vec<[String; 5]> = self
104 .rounds
105 .iter()
106 .map(|r| {
107 [
108 r.round.to_string(),
109 r.outcome.clone(),
110 fmt_opt(r.value),
111 fmt_opt(r.delta),
112 r.summary.replace('|', "\\|").replace('\n', " "),
113 ]
114 })
115 .collect();
116
117 let width = |col: usize, header: &str| -> usize {
118 cells
119 .iter()
120 .map(|c| c[col].chars().count())
121 .chain(std::iter::once(header.chars().count()))
122 .max()
123 .unwrap_or(0)
124 };
125 let (wr, wo, wv, wd) = (
126 width(0, "Round"),
127 width(1, "Outcome"),
128 width(2, "Value"),
129 width(3, "Δ"),
130 );
131
132 s.push_str(&format!(
134 "| {} | {} | {} | {} | Change |\n",
135 pad_left("Round", wr),
136 pad_right("Outcome", wo),
137 pad_left("Value", wv),
138 pad_left("Δ", wd),
139 ));
140 s.push_str(&format!(
141 "|{}:|{}|{}:|{}:|--------|\n",
142 "-".repeat(wr + 1),
143 "-".repeat(wo + 2),
144 "-".repeat(wv + 1),
145 "-".repeat(wd + 1),
146 ));
147 for c in &cells {
148 s.push_str(&format!(
149 "| {} | {} | {} | {} | {} |\n",
150 pad_left(&c[0], wr),
151 pad_right(&c[1], wo),
152 pad_left(&c[2], wv),
153 pad_left(&c[3], wd),
154 c[4],
155 ));
156 }
157 s
158 }
159
160 pub fn to_json(&self) -> Value {
161 json!({
162 "objective": self.objective,
163 "goal": self.goal,
164 "metric_name": self.metric_name,
165 "start_value": self.start_value,
166 "best_value": self.best_value,
167 "best_round": self.best_round,
168 "rounds": self.rounds.iter().map(|r| json!({
169 "round": r.round,
170 "outcome": r.outcome,
171 "value": r.value,
172 "delta": r.delta,
173 "improvement": r.improvement,
174 "policy_area": r.policy_area,
175 "direction": r.direction,
176 "kept": r.kept,
177 "summary": r.summary,
178 })).collect::<Vec<_>>(),
179 "usage": self.usage.to_json(),
180 })
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn rec(
189 round: u32,
190 outcome: &str,
191 value: Option<f64>,
192 delta: Option<f64>,
193 s: &str,
194 ) -> RoundRecord {
195 RoundRecord {
196 round,
197 summary: s.to_string(),
198 outcome: outcome.to_string(),
199 value,
200 delta,
201 improvement: delta.map(|d| -d),
202 policy_area: "slice".into(),
203 direction: "test_direction".into(),
204 kept: outcome == "kept",
205 }
206 }
207
208 #[test]
209 fn table_columns_are_aligned() {
210 let rep = Report {
211 objective: "minimize p99".into(),
212 goal: "minimize".into(),
213 metric_name: "p99_wakeup_latency_us".into(),
214 start_value: Some(5224.0),
215 best_value: Some(5208.0),
216 best_round: Some(3),
217 rounds: vec![
218 rec(1, "no-edit", None, None, "<tool_call>"),
219 rec(
220 2,
221 "reverted",
222 Some(5224.0),
223 Some(0.0),
224 "Reduced SLEEP_VLAG_LIMIT_NS from 20ms to 5ms.",
225 ),
226 rec(
227 3,
228 "kept",
229 Some(5208.0),
230 Some(-16.0),
231 "Reduced SLEEP_VLAG_LIMIT_NS from 20ms to 10ms.",
232 ),
233 ],
234 usage: Usage::default(),
235 };
236 let out = rep.render_markdown();
237 let table: Vec<&str> = out.lines().filter(|l| l.starts_with('|')).collect();
238 assert_eq!(table.len(), 5);
240 let pipes = |l: &str| -> Vec<usize> {
243 l.chars()
244 .enumerate()
245 .filter(|(_, c)| *c == '|')
246 .map(|(i, _)| i)
247 .collect()
248 };
249 let header_pipes = pipes(table[0]);
250 for line in &table {
252 let p = pipes(line);
253 assert!(p.len() >= 5, "row has too few columns: {line}");
254 assert_eq!(&p[..5], &header_pipes[..5], "misaligned row: {line}");
255 }
256 }
257}