1use crate::RepoConfig;
2use eyre::Result;
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, process::Command, thread};
5
6#[derive(Debug, Deserialize, Serialize)]
8pub struct HyperfineResult {
9 pub command: String,
10 pub mean: f64,
11 pub stddev: Option<f64>,
12 pub median: f64,
13 pub user: f64,
14 pub system: f64,
15 pub min: f64,
16 pub max: f64,
17 pub times: Vec<f64>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub exit_codes: Option<Vec<i32>>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub parameters: Option<HashMap<String, serde_json::Value>>,
22}
23
24#[derive(Debug, Deserialize, Serialize)]
26pub struct HyperfineOutput {
27 pub results: Vec<HyperfineResult>,
28}
29
30#[derive(Debug, Default)]
32pub struct BenchmarkResults {
33 pub data: HashMap<String, HashMap<String, HashMap<String, HyperfineResult>>>,
35 pub baseline_version: Option<String>,
37 pub version_details: HashMap<String, String>,
39}
40
41impl BenchmarkResults {
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn set_baseline_version(&mut self, version: String) {
47 self.baseline_version = Some(version);
48 }
49
50 pub fn add_result(
51 &mut self,
52 benchmark: &str,
53 version: &str,
54 repo: &str,
55 result: HyperfineResult,
56 ) {
57 self.data
58 .entry(benchmark.to_string())
59 .or_default()
60 .entry(version.to_string())
61 .or_default()
62 .insert(repo.to_string(), result);
63 }
64
65 pub fn add_version_details(&mut self, version: &str, details: String) {
66 self.version_details.insert(version.to_string(), details);
67 }
68
69 pub fn generate_markdown(&self, versions: &[String], repos: &[RepoConfig]) -> String {
70 let mut output = String::new();
71
72 output.push_str("# Foundry Benchmark Results\n\n");
74 output.push_str(&format!(
75 "**Date**: {}\n\n",
76 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
77 ));
78
79 output.push_str("## Summary\n\n");
81 let mut repos_with_results = std::collections::HashSet::new();
83 for version_data in self.data.values() {
84 for repo_data in version_data.values() {
85 for repo_name in repo_data.keys() {
86 repos_with_results.insert(repo_name.clone());
87 }
88 }
89 }
90 output.push_str(&format!(
91 "Benchmarked {} Foundry versions across {} repositories.\n\n",
92 versions.len(),
93 repos_with_results.len()
94 ));
95
96 output.push_str("### Repositories Tested\n\n");
98 for (i, repo) in repos.iter().enumerate() {
99 output.push_str(&format!(
100 "{}. [{}/{}](https://github.com/{}/{})\n",
101 i + 1,
102 repo.org,
103 repo.repo,
104 repo.org,
105 repo.repo
106 ));
107 }
108 output.push('\n');
109
110 output.push_str("### Foundry Versions\n\n");
112 for version in versions {
113 if let Some(details) = self.version_details.get(version) {
114 output.push_str(&format!("- **{version}**: {}\n", details.trim()));
115 } else {
116 output.push_str(&format!("- {version}\n"));
117 }
118 }
119 output.push('\n');
120
121 for (benchmark_name, version_data) in &self.data {
123 output.push_str(&self.generate_benchmark_table(
124 benchmark_name,
125 version_data,
126 versions,
127 repos,
128 ));
129 }
130
131 output.push_str("## System Information\n\n");
133 output.push_str(&format!("- **OS**: {}\n", std::env::consts::OS));
134 output.push_str(&format!(
135 "- **CPU**: {}\n",
136 thread::available_parallelism().map_or(1, |n| n.get())
137 ));
138 output.push_str(&format!(
139 "- **Rustc**: {}\n",
140 get_rustc_version().unwrap_or_else(|_| "unknown".to_string())
141 ));
142
143 output
144 }
145
146 fn generate_benchmark_table(
150 &self,
151 benchmark_name: &str,
152 version_data: &HashMap<String, HashMap<String, HyperfineResult>>,
153 versions: &[String],
154 repos: &[RepoConfig],
155 ) -> String {
156 let mut output = String::new();
157
158 output.push_str(&format!("## {}\n\n", format_benchmark_name(benchmark_name)));
160
161 output.push_str("| Repository |");
163 for version in versions {
164 output.push_str(&format!(" {version} |"));
165 }
166 output.push('\n');
167
168 output.push_str("|------------|");
170 for _ in versions {
171 output.push_str("----------|");
172 }
173 output.push('\n');
174
175 output.push_str(&generate_table_rows(version_data, versions, repos));
177 output.push('\n');
178
179 output
180 }
181}
182
183fn generate_table_rows(
188 version_data: &HashMap<String, HashMap<String, HyperfineResult>>,
189 versions: &[String],
190 repos: &[RepoConfig],
191) -> String {
192 let mut output = String::new();
193
194 for repo in repos {
195 output.push_str(&format!("| {} |", repo.name));
196
197 for version in versions {
198 let cell_content = get_benchmark_cell_content(version_data, version, &repo.name);
199 output.push_str(&format!(" {cell_content} |"));
200 }
201
202 output.push('\n');
203 }
204
205 output
206}
207
208fn get_benchmark_cell_content(
215 version_data: &HashMap<String, HashMap<String, HyperfineResult>>,
216 version: &str,
217 repo_name: &str,
218) -> String {
219 if let Some(repo_data) = version_data.get(version) &&
221 let Some(result) = repo_data.get(repo_name)
223 {
224 return format_duration_seconds(result.mean);
225 }
226
227 "N/A".to_string()
228}
229
230pub fn format_benchmark_name(name: &str) -> String {
231 match name {
232 "forge_test" => "Forge Test",
233 "forge_build_no_cache" => "Forge Build (No Cache)",
234 "forge_build_with_cache" => "Forge Build (With Cache)",
235 "forge_fuzz_test" => "Forge Fuzz Test",
236 "forge_coverage" => "Forge Coverage",
237 "forge_isolate_test" => "Forge Test (Isolated)",
238 _ => name,
239 }
240 .to_string()
241}
242
243pub fn format_duration_seconds(seconds: f64) -> String {
244 if seconds < 0.001 {
245 format!("{:.2} ms", seconds * 1000.0)
246 } else if seconds < 1.0 {
247 format!("{seconds:.3} s")
248 } else if seconds < 60.0 {
249 format!("{seconds:.2} s")
250 } else {
251 let minutes = (seconds / 60.0).floor();
252 let remaining_seconds = seconds % 60.0;
253 format!("{minutes:.0}m {remaining_seconds:.1}s")
254 }
255}
256
257pub fn get_rustc_version() -> Result<String> {
258 let output = Command::new("rustc").arg("--version").output()?;
259
260 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
261}