foundry_bench/
results.rs

1use crate::RepoConfig;
2use eyre::Result;
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, process::Command, thread};
5
6/// Hyperfine benchmark result
7#[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/// Hyperfine JSON output format
25#[derive(Debug, Deserialize, Serialize)]
26pub struct HyperfineOutput {
27    pub results: Vec<HyperfineResult>,
28}
29
30/// Aggregated benchmark results
31#[derive(Debug, Default)]
32pub struct BenchmarkResults {
33    /// Map of benchmark_name -> version -> repo -> result
34    pub data: HashMap<String, HashMap<String, HashMap<String, HyperfineResult>>>,
35    /// Track the baseline version for comparison
36    pub baseline_version: Option<String>,
37    /// Map of version name -> full version details
38    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        // Header
73        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        // Summary
80        output.push_str("## Summary\n\n");
81        // Count actual repos that have results
82        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        // Repositories tested
97        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        // Versions tested
111        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        // Results for each benchmark type
122        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        // System info
132        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    /// Generate a complete markdown table for a single benchmark type
147    ///
148    /// This includes the section header, table header, separator, and all rows
149    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        // Section header
159        output.push_str(&format!("## {}\n\n", format_benchmark_name(benchmark_name)));
160
161        // Create table header
162        output.push_str("| Repository |");
163        for version in versions {
164            output.push_str(&format!(" {version} |"));
165        }
166        output.push('\n');
167
168        // Table separator
169        output.push_str("|------------|");
170        for _ in versions {
171            output.push_str("----------|");
172        }
173        output.push('\n');
174
175        // Table rows
176        output.push_str(&generate_table_rows(version_data, versions, repos));
177        output.push('\n');
178
179        output
180    }
181}
182
183/// Generate table rows for benchmark results
184///
185/// This function creates the markdown table rows for each repository,
186/// showing the benchmark results for each version.
187fn 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
208/// Get the content for a single benchmark table cell
209///
210/// Returns the formatted duration or "N/A" if no data is available.
211/// The nested if-let statements handle the following cases:
212/// 1. Check if version data exists
213/// 2. Check if repository data exists for this version
214fn get_benchmark_cell_content(
215    version_data: &HashMap<String, HashMap<String, HyperfineResult>>,
216    version: &str,
217    repo_name: &str,
218) -> String {
219    // Check if we have data for this version
220    if let Some(repo_data) = version_data.get(version) &&
221    // Check if we have data for this repository
222        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}