1use crate::results::{HyperfineOutput, HyperfineResult};
4use eyre::{Result, WrapErr};
5use foundry_common::{sh_eprintln, sh_println};
6use foundry_compilers::project_util::TempProject;
7use foundry_test_utils::util::clone_remote;
8use once_cell::sync::Lazy;
9use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
10use std::{
11 env,
12 path::{Path, PathBuf},
13 process::Command,
14 str::FromStr,
15};
16
17pub mod results;
18
19pub const RUNS: u32 = 5;
21
22#[derive(Debug, Clone)]
24pub struct RepoConfig {
25 pub name: String,
26 pub org: String,
27 pub repo: String,
28 pub rev: String,
29}
30
31impl FromStr for RepoConfig {
32 type Err = eyre::Error;
33
34 fn from_str(spec: &str) -> Result<Self> {
35 let parts: Vec<&str> = spec.splitn(2, ':').collect();
37 let repo_path = parts[0];
38 let custom_rev = parts.get(1).copied();
39
40 let path_parts: Vec<&str> = repo_path.split('/').collect();
42 if path_parts.len() != 2 {
43 eyre::bail!("Invalid repo format '{}'. Expected 'org/repo' or 'org/repo:rev'", spec);
44 }
45
46 let org = path_parts[0];
47 let repo = path_parts[1];
48
49 let existing_config = BENCHMARK_REPOS.iter().find(|r| r.org == org && r.repo == repo);
51
52 let config = if let Some(existing) = existing_config {
53 let mut config = existing.clone();
55 if let Some(rev) = custom_rev {
56 config.rev = rev.to_string();
57 }
58 config
59 } else {
60 Self {
63 name: format!("{org}-{repo}"),
64 org: org.to_string(),
65 repo: repo.to_string(),
66 rev: custom_rev.unwrap_or("main").to_string(),
67 }
68 };
69
70 let _ = sh_println!("Parsed repo spec '{spec}' -> {config:?}");
71 Ok(config)
72 }
73}
74
75pub fn default_benchmark_repos() -> Vec<RepoConfig> {
77 vec![
78 RepoConfig {
79 name: "ithacaxyz-account".to_string(),
80 org: "ithacaxyz".to_string(),
81 repo: "account".to_string(),
82 rev: "main".to_string(),
83 },
84 RepoConfig {
85 name: "solady".to_string(),
86 org: "Vectorized".to_string(),
87 repo: "solady".to_string(),
88 rev: "main".to_string(),
89 },
90 ]
91}
92
93pub static BENCHMARK_REPOS: Lazy<Vec<RepoConfig>> = Lazy::new(default_benchmark_repos);
95
96pub static FOUNDRY_VERSIONS: &[&str] = &["stable", "nightly"];
112
113pub struct BenchmarkProject {
115 pub name: String,
116 pub temp_project: TempProject,
117 pub root_path: PathBuf,
118}
119
120impl BenchmarkProject {
121 #[allow(unused_must_use)]
123 pub fn setup(config: &RepoConfig) -> Result<Self> {
124 let temp_project =
125 TempProject::dapptools().wrap_err("Failed to create temporary project")?;
126
127 let root_path = temp_project.root().to_path_buf();
129 let root = root_path.to_str().unwrap();
130
131 for entry in std::fs::read_dir(&root_path)? {
133 let entry = entry?;
134 let path = entry.path();
135 if path.is_dir() {
136 std::fs::remove_dir_all(&path).ok();
137 } else {
138 std::fs::remove_file(&path).ok();
139 }
140 }
141
142 let repo_url = format!("https://github.com/{}/{}.git", config.org, config.repo);
144 clone_remote(&repo_url, root, true);
145
146 if !config.rev.is_empty() && config.rev != "main" && config.rev != "master" {
148 let status = Command::new("git")
149 .current_dir(root)
150 .args(["checkout", &config.rev])
151 .status()
152 .wrap_err("Failed to checkout revision")?;
153
154 if !status.success() {
155 eyre::bail!("Git checkout failed for {}", config.name);
156 }
157 }
158
159 Self::install_npm_dependencies(&root_path)?;
162
163 sh_println!(" ✅ Project {} setup complete at {}", config.name, root);
164 Ok(Self { name: config.name.to_string(), root_path, temp_project })
165 }
166
167 #[allow(unused_must_use)]
169 fn install_npm_dependencies(root: &Path) -> Result<()> {
170 if root.join("package.json").exists() {
171 sh_println!(" 📦 Running npm install...");
172 let status = Command::new("npm")
173 .current_dir(root)
174 .args(["install"])
175 .stdout(std::process::Stdio::inherit())
176 .stderr(std::process::Stdio::inherit())
177 .status()
178 .wrap_err("Failed to run npm install")?;
179
180 if !status.success() {
181 sh_println!(
182 " ⚠️ Warning: npm install failed with exit code: {:?}",
183 status.code()
184 );
185 } else {
186 sh_println!(" ✅ npm install completed successfully");
187 }
188 }
189 Ok(())
190 }
191
192 #[allow(clippy::too_many_arguments)]
213 fn hyperfine(
214 &self,
215 benchmark_name: &str,
216 version: &str,
217 command: &str,
218 runs: u32,
219 setup: Option<&str>,
220 prepare: Option<&str>,
221 conclude: Option<&str>,
222 verbose: bool,
223 ) -> Result<HyperfineResult> {
224 let temp_dir = std::env::temp_dir();
227 let json_dir =
228 temp_dir.join("foundry-bench").join(benchmark_name).join(version).join(&self.name);
229 std::fs::create_dir_all(&json_dir)?;
230
231 let json_path = json_dir.join(format!("{benchmark_name}.json"));
232
233 let mut hyperfine_cmd = Command::new("hyperfine");
235 hyperfine_cmd
236 .current_dir(&self.root_path)
237 .arg("--runs")
238 .arg(runs.to_string())
239 .arg("--export-json")
240 .arg(&json_path)
241 .arg("--shell=bash");
242
243 if let Some(setup_cmd) = setup {
245 hyperfine_cmd.arg("--setup").arg(setup_cmd);
246 }
247
248 if let Some(prepare_cmd) = prepare {
250 hyperfine_cmd.arg("--prepare").arg(prepare_cmd);
251 }
252
253 if let Some(conclude_cmd) = conclude {
255 hyperfine_cmd.arg("--conclude").arg(conclude_cmd);
256 }
257
258 if verbose {
259 hyperfine_cmd.arg("--show-output");
260 hyperfine_cmd.stderr(std::process::Stdio::inherit());
261 hyperfine_cmd.stdout(std::process::Stdio::inherit());
262 }
263
264 hyperfine_cmd.arg(command);
266
267 let status = hyperfine_cmd.status().wrap_err("Failed to run hyperfine")?;
268 if !status.success() {
269 eyre::bail!("Hyperfine failed for command: {}", command);
270 }
271
272 let json_content = std::fs::read_to_string(json_path)?;
274 let output: HyperfineOutput = serde_json::from_str(&json_content)?;
275
276 output.results.into_iter().next().ok_or_else(|| eyre::eyre!("No results from hyperfine"))
278 }
279
280 pub fn bench_forge_test(
282 &self,
283 version: &str,
284 runs: u32,
285 verbose: bool,
286 ) -> Result<HyperfineResult> {
287 self.hyperfine(
289 "forge_test",
290 version,
291 "forge test",
292 runs,
293 Some("forge build"),
294 None,
295 None,
296 verbose,
297 )
298 }
299
300 pub fn bench_forge_build_with_cache(
302 &self,
303 version: &str,
304 runs: u32,
305 verbose: bool,
306 ) -> Result<HyperfineResult> {
307 self.hyperfine(
308 "forge_build_with_cache",
309 version,
310 "FOUNDRY_LINT_LINT_ON_BUILD=false forge build",
311 runs,
312 None,
313 Some("forge build"),
314 None,
315 verbose,
316 )
317 }
318
319 pub fn bench_forge_build_no_cache(
321 &self,
322 version: &str,
323 runs: u32,
324 verbose: bool,
325 ) -> Result<HyperfineResult> {
326 self.hyperfine(
328 "forge_build_no_cache",
329 version,
330 "FOUNDRY_LINT_LINT_ON_BUILD=false forge build",
331 runs,
332 Some("forge clean"),
333 None,
334 Some("forge clean"),
335 verbose,
336 )
337 }
338
339 pub fn bench_forge_fuzz_test(
341 &self,
342 version: &str,
343 runs: u32,
344 verbose: bool,
345 ) -> Result<HyperfineResult> {
346 self.hyperfine(
348 "forge_fuzz_test",
349 version,
350 r#"forge test --match-test "test[^(]*\([^)]+\)""#,
351 runs,
352 Some("forge build"),
353 None,
354 None,
355 verbose,
356 )
357 }
358
359 pub fn bench_forge_coverage(
361 &self,
362 version: &str,
363 runs: u32,
364 verbose: bool,
365 ) -> Result<HyperfineResult> {
366 self.hyperfine(
369 "forge_coverage",
370 version,
371 "forge coverage --ir-minimum",
372 runs,
373 None,
374 None,
375 None,
376 verbose,
377 )
378 }
379
380 pub fn bench_forge_isolate_test(
382 &self,
383 version: &str,
384 runs: u32,
385 verbose: bool,
386 ) -> Result<HyperfineResult> {
387 self.hyperfine(
389 "forge_isolate_test",
390 version,
391 "forge test --isolate",
392 runs,
393 Some("forge build"),
394 None,
395 None,
396 verbose,
397 )
398 }
399
400 pub fn root(&self) -> &Path {
402 &self.root_path
403 }
404
405 pub fn run(
407 &self,
408 benchmark: &str,
409 version: &str,
410 runs: u32,
411 verbose: bool,
412 ) -> Result<HyperfineResult> {
413 match benchmark {
414 "forge_test" => self.bench_forge_test(version, runs, verbose),
415 "forge_build_no_cache" => self.bench_forge_build_no_cache(version, runs, verbose),
416 "forge_build_with_cache" => self.bench_forge_build_with_cache(version, runs, verbose),
417 "forge_fuzz_test" => self.bench_forge_fuzz_test(version, runs, verbose),
418 "forge_coverage" => self.bench_forge_coverage(version, runs, verbose),
419 "forge_isolate_test" => self.bench_forge_isolate_test(version, runs, verbose),
420 _ => eyre::bail!("Unknown benchmark: {}", benchmark),
421 }
422 }
423}
424
425#[allow(unused_must_use)]
427pub fn switch_foundry_version(version: &str) -> Result<()> {
428 let output = Command::new("foundryup")
429 .args(["--use", version])
430 .output()
431 .wrap_err("Failed to run foundryup")?;
432
433 let stderr = String::from_utf8_lossy(&output.stderr);
435 if stderr.contains("command failed") && stderr.contains("forge --version") {
436 eyre::bail!(
437 "Foundry binaries maybe corrupted. Please reinstall by running `foundryup --install <version>`"
438 );
439 }
440
441 if !output.status.success() {
442 sh_eprintln!("foundryup stderr: {stderr}");
443 eyre::bail!("Failed to switch to foundry version: {}", version);
444 }
445
446 sh_println!(" Successfully switched to version: {version}");
447 Ok(())
448}
449
450pub fn get_forge_version() -> Result<String> {
452 let output = Command::new("forge")
453 .args(["--version"])
454 .output()
455 .wrap_err("Failed to get forge version")?;
456
457 if !output.status.success() {
458 eyre::bail!("forge --version failed");
459 }
460
461 let version =
462 String::from_utf8(output.stdout).wrap_err("Invalid UTF-8 in forge version output")?;
463
464 Ok(version.lines().next().unwrap_or("unknown").to_string())
465}
466
467pub fn get_forge_version_details() -> Result<String> {
469 let output = Command::new("forge")
470 .args(["--version"])
471 .output()
472 .wrap_err("Failed to get forge version")?;
473
474 if !output.status.success() {
475 eyre::bail!("forge --version failed");
476 }
477
478 let full_output =
479 String::from_utf8(output.stdout).wrap_err("Invalid UTF-8 in forge version output")?;
480
481 let lines: Vec<&str> = full_output.lines().collect();
483 if lines.len() >= 3 {
484 let version = lines[0].trim();
486 let commit = lines[1].trim().replace("Commit SHA: ", "");
487 let timestamp = lines[2].trim().replace("Build Timestamp: ", "");
488
489 let short_commit = &commit[..7]; let date = timestamp.split('T').next().unwrap_or(×tamp);
492
493 Ok(format!("{version} ({short_commit} {date})"))
494 } else {
495 Ok(lines.first().unwrap_or(&"unknown").to_string())
497 }
498}
499
500pub fn get_benchmark_versions() -> Vec<String> {
508 if let Ok(versions_env) = env::var("FOUNDRY_BENCH_VERSIONS") {
509 versions_env.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
510 } else {
511 FOUNDRY_VERSIONS.iter().map(|&s| s.to_string()).collect()
512 }
513}
514
515pub fn setup_benchmark_repos() -> Vec<(RepoConfig, BenchmarkProject)> {
517 let repos = if let Ok(repos_env) = env::var("FOUNDRY_BENCH_REPOS") {
519 repos_env
522 .split(',')
523 .map(|s| s.trim())
524 .filter(|s| !s.is_empty())
525 .map(|s| s.parse::<RepoConfig>())
526 .collect::<Result<Vec<_>>>()
527 .expect("Failed to parse FOUNDRY_BENCH_REPOS")
528 } else {
529 BENCHMARK_REPOS.clone()
530 };
531
532 repos
533 .par_iter()
534 .map(|repo_config| {
535 let project = BenchmarkProject::setup(repo_config).expect("Failed to setup project");
536 (repo_config.clone(), project)
537 })
538 .collect()
539}