1use crate::colony::{Colony, ColonySnapshot};
8use crate::metrics::{ColonyMetrics, compute_from_snapshots};
9use phago_core::types::Tick;
10use serde::Serialize;
11use std::time::Instant;
12
13#[derive(Debug, Clone, Serialize)]
15pub struct BenchmarkRun {
16 pub name: String,
17 pub ticks: u64,
18 pub snapshots: Vec<ColonySnapshot>,
19 pub metrics_timeline: Vec<(Tick, ColonyMetrics)>,
20 pub wall_time_ms: u64,
21}
22
23pub struct BenchmarkSuite {
25 pub runs: Vec<BenchmarkRun>,
26}
27
28pub struct BenchmarkConfig {
30 pub name: String,
31 pub ticks: u64,
32 pub snapshot_interval: u64,
34 pub metrics_interval: u64,
36}
37
38impl Default for BenchmarkConfig {
39 fn default() -> Self {
40 Self {
41 name: "default".to_string(),
42 ticks: 200,
43 snapshot_interval: 10,
44 metrics_interval: 50,
45 }
46 }
47}
48
49impl BenchmarkConfig {
50 pub fn new(name: &str, ticks: u64) -> Self {
51 Self {
52 name: name.to_string(),
53 ticks,
54 ..Default::default()
55 }
56 }
57
58 pub fn with_snapshot_interval(mut self, interval: u64) -> Self {
59 self.snapshot_interval = interval;
60 self
61 }
62
63 pub fn with_metrics_interval(mut self, interval: u64) -> Self {
64 self.metrics_interval = interval;
65 self
66 }
67}
68
69pub fn run_benchmark(colony: &mut Colony, config: &BenchmarkConfig) -> BenchmarkRun {
74 let mut snapshots = Vec::new();
75 let mut metrics_timeline = Vec::new();
76
77 snapshots.push(colony.snapshot());
79
80 let start = Instant::now();
81
82 for tick_num in 1..=config.ticks {
83 colony.tick();
84
85 if tick_num % config.snapshot_interval == 0 {
86 snapshots.push(colony.snapshot());
87 }
88
89 if tick_num % config.metrics_interval == 0 {
90 let metrics = compute_from_snapshots(colony, &snapshots);
91 metrics_timeline.push((tick_num, metrics));
92 }
93 }
94
95 let wall_time = start.elapsed();
96
97 let final_metrics = compute_from_snapshots(colony, &snapshots);
99 metrics_timeline.push((config.ticks, final_metrics));
100
101 BenchmarkRun {
102 name: config.name.clone(),
103 ticks: config.ticks,
104 snapshots,
105 metrics_timeline,
106 wall_time_ms: wall_time.as_millis() as u64,
107 }
108}
109
110impl BenchmarkSuite {
111 pub fn new() -> Self {
112 Self { runs: Vec::new() }
113 }
114
115 pub fn add_run(&mut self, run: BenchmarkRun) {
116 self.runs.push(run);
117 }
118
119 pub fn compare(&self) -> ComparisonTable {
121 let rows: Vec<ComparisonRow> = self
122 .runs
123 .iter()
124 .map(|run| {
125 let final_metrics = run
126 .metrics_timeline
127 .last()
128 .map(|(_, m)| m.clone());
129
130 ComparisonRow {
131 name: run.name.clone(),
132 ticks: run.ticks,
133 wall_time_ms: run.wall_time_ms,
134 graph_nodes: final_metrics
135 .as_ref()
136 .map(|m| m.graph_richness.node_count)
137 .unwrap_or(0),
138 graph_edges: final_metrics
139 .as_ref()
140 .map(|m| m.graph_richness.edge_count)
141 .unwrap_or(0),
142 density: final_metrics
143 .as_ref()
144 .map(|m| m.graph_richness.density)
145 .unwrap_or(0.0),
146 clustering: final_metrics
147 .as_ref()
148 .map(|m| m.graph_richness.clustering_coefficient)
149 .unwrap_or(0.0),
150 avg_degree: final_metrics
151 .as_ref()
152 .map(|m| m.graph_richness.avg_degree)
153 .unwrap_or(0.0),
154 shared_term_ratio: final_metrics
155 .as_ref()
156 .map(|m| m.transfer.shared_term_ratio)
157 .unwrap_or(0.0),
158 gini: final_metrics
159 .as_ref()
160 .map(|m| m.vocabulary_spread.gini_coefficient)
161 .unwrap_or(0.0),
162 }
163 })
164 .collect();
165
166 ComparisonTable { rows }
167 }
168
169 pub fn to_csv(&self) -> String {
171 let table = self.compare();
172 let mut csv = String::new();
173 csv.push_str("name,ticks,wall_time_ms,nodes,edges,density,clustering,avg_degree,shared_term_ratio,gini\n");
174 for row in &table.rows {
175 csv.push_str(&format!(
176 "{},{},{},{},{},{:.4},{:.4},{:.2},{:.4},{:.4}\n",
177 row.name,
178 row.ticks,
179 row.wall_time_ms,
180 row.graph_nodes,
181 row.graph_edges,
182 row.density,
183 row.clustering,
184 row.avg_degree,
185 row.shared_term_ratio,
186 row.gini,
187 ));
188 }
189 csv
190 }
191}
192
193impl Default for BenchmarkSuite {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199#[derive(Debug, Clone, Serialize)]
201pub struct ComparisonTable {
202 pub rows: Vec<ComparisonRow>,
203}
204
205#[derive(Debug, Clone, Serialize)]
207pub struct ComparisonRow {
208 pub name: String,
209 pub ticks: u64,
210 pub wall_time_ms: u64,
211 pub graph_nodes: usize,
212 pub graph_edges: usize,
213 pub density: f64,
214 pub clustering: f64,
215 pub avg_degree: f64,
216 pub shared_term_ratio: f64,
217 pub gini: f64,
218}
219
220impl ComparisonTable {
221 pub fn print(&self) {
223 println!("┌{:─<20}┬{:─<7}┬{:─<10}┬{:─<7}┬{:─<7}┬{:─<8}┬{:─<10}┬{:─<9}┐",
224 "", "", "", "", "", "", "", "");
225 println!("│{:<20}│{:>7}│{:>10}│{:>7}│{:>7}│{:>8}│{:>10}│{:>9}│",
226 " Run", " Nodes", " Edges", " Dense", " Clust", " AvgDeg", " Shared%", " Gini");
227 println!("├{:─<20}┼{:─<7}┼{:─<10}┼{:─<7}┼{:─<7}┼{:─<8}┼{:─<10}┼{:─<9}┤",
228 "", "", "", "", "", "", "", "");
229 for row in &self.rows {
230 println!("│{:<20}│{:>7}│{:>10}│{:>7.3}│{:>7.3}│{:>8.2}│{:>9.1}%│{:>9.3}│",
231 row.name,
232 row.graph_nodes,
233 row.graph_edges,
234 row.density,
235 row.clustering,
236 row.avg_degree,
237 row.shared_term_ratio * 100.0,
238 row.gini,
239 );
240 }
241 println!("└{:─<20}┴{:─<7}┴{:─<10}┴{:─<7}┴{:─<7}┴{:─<8}┴{:─<10}┴{:─<9}┘",
242 "", "", "", "", "", "", "", "");
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::corpus::Corpus;
250 use phago_agents::digester::Digester;
251 use phago_core::types::Position;
252
253 #[test]
254 fn benchmark_run_produces_data() {
255 let mut colony = Colony::new();
256 let corpus = Corpus::inline_corpus();
258 corpus.ingest_into(&mut colony);
259 colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(80)));
260
261 let config = BenchmarkConfig::new("test", 20)
262 .with_snapshot_interval(5)
263 .with_metrics_interval(10);
264 let run = run_benchmark(&mut colony, &config);
265
266 assert_eq!(run.ticks, 20);
267 assert!(!run.snapshots.is_empty());
268 assert!(!run.metrics_timeline.is_empty());
269 assert!(run.wall_time_ms < 10_000); }
271
272 #[test]
273 fn suite_comparison_works() {
274 let mut suite = BenchmarkSuite::new();
275
276 let mut colony1 = Colony::new();
277 colony1.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(80)));
278 let run1 = run_benchmark(&mut colony1, &BenchmarkConfig::new("empty", 10));
279 suite.add_run(run1);
280
281 let csv = suite.to_csv();
282 assert!(csv.contains("empty"));
283 }
284}