Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions example-codspeed/go-runner.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"profile_folder": "/tmp",
"relative_package_path": "codspeed-go"
}
9 changes: 9 additions & 0 deletions example/very/nested/module/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package example

import "testing"

func BenchmarkExample(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = 42
}
}
34 changes: 33 additions & 1 deletion go-runner/src/builder/templater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,50 @@ use crate::builder::{BenchmarkPackage, GoBenchmark};
use crate::utils;
use crate::{builder::patcher, prelude::*};

#[derive(Debug, Serialize)]
struct GoRunnerMetadata {
profile_folder: String,
relative_package_path: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct TemplateData {
benchmarks: Vec<GoBenchmark>,
module_name: String,
}

pub fn run(package: &BenchmarkPackage) -> anyhow::Result<(TempDir, PathBuf)> {
pub fn run<P: AsRef<Path>>(
package: &BenchmarkPackage,
profile_dir: P,
) -> anyhow::Result<(TempDir, PathBuf)> {
// 1. Copy the whole module to a build directory
let target_dir = TempDir::new()?;
std::fs::create_dir_all(&target_dir).context("Failed to create target directory")?;
utils::copy_dir_recursively(&package.module.dir, &target_dir)?;

// Create a new go-runner.metadata file in the root of the project
//
// The package path will be prepended to the URI. The benchmark will
// find the path relative to the root of the `target_dir`.
//
// This is needed because we could execute a Go project that is a sub-folder
// within a Git repository, then we won't copy the .git folder. Therefore, we
// have to resolve the .git relative path in go-runner and then combine it.
let relative_package_path = utils::get_git_relative_path(&package.dir)
.to_string_lossy()
.into();
debug!("Relative package path: {relative_package_path}");

let metadata = GoRunnerMetadata {
profile_folder: profile_dir.as_ref().to_string_lossy().into(),
relative_package_path,
};
fs::write(
target_dir.path().join("go-runner.metadata"),
serde_json::to_string_pretty(&metadata)?,
)
.context("Failed to write go-runner.metadata file")?;

// Get files that need to be renamed first
let files = package
.test_go_files
Expand Down
8 changes: 1 addition & 7 deletions go-runner/src/integration_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use itertools::Itertools;
use rstest::rstest;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tempfile::TempDir;

use crate::results::walltime_results::WalltimeResults;
Expand Down Expand Up @@ -66,18 +65,13 @@ fn test_build_and_run(#[case] project_name: &str) {
.join("testdata/projects")
.join(project_name);

// Mutex to prevent concurrent tests from interfering with CODSPEED_PROFILE_FOLDER env var
static ENV_MUTEX: Mutex<()> = Mutex::new(());
let _env_guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());

let temp_dir = TempDir::new().unwrap();
let profile_dir = temp_dir.path().join("profile");
unsafe { std::env::set_var("CODSPEED_PROFILE_FOLDER", &profile_dir) };
let cli = crate::cli::Cli {
benchtime: "1x".into(),
..Default::default()
};
if let Err(error) = crate::run_benchmarks(project_dir.as_path(), &cli) {
if let Err(error) = crate::run_benchmarks(&profile_dir, project_dir.as_path(), &cli) {
panic!("Benchmarks couldn't run: {error}");
}

Expand Down
50 changes: 15 additions & 35 deletions go-runner/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use crate::{builder::BenchmarkPackage, prelude::*};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use std::{collections::HashMap, path::Path};

pub mod builder;
pub mod cli;
Expand All @@ -15,35 +12,30 @@ pub(crate) mod utils;
mod integration_tests;

/// Builds and runs the specified Go project benchmarks, writing results to the .codspeed folder.
pub fn run_benchmarks(project_dir: &Path, cli: &crate::cli::Cli) -> anyhow::Result<()> {
let profile_dir = std::env::var("CODSPEED_PROFILE_FOLDER")
.context("CODSPEED_PROFILE_FOLDER env var not set")?;
pub fn run_benchmarks<P: AsRef<Path>>(
profile_dir: P,
project_dir: &Path,
cli: &crate::cli::Cli,
) -> anyhow::Result<()> {
std::fs::remove_dir_all(&profile_dir).ok();

// 1. Build phase - Benchmark and package discovery
let packages = BenchmarkPackage::from_project(project_dir, &cli.packages)?;
info!("Discovered {} packages", packages.len());

let mut bench_name_to_path = HashMap::new();
let total_benchmarks: usize = packages.iter().map(|p| p.benchmarks.len()).sum();
info!("Total benchmarks discovered: {total_benchmarks}");

for package in &packages {
for benchmark in &package.benchmarks {
// Create absolute path and immediately convert to git-relative path
let abs_path = package.module.dir.join(&benchmark.file_path);
let git_relative_path = crate::utils::get_git_relative_path(&abs_path);
bench_name_to_path.insert(benchmark.name.clone(), git_relative_path);
info!("Found {:30} in {:?}", benchmark.name, benchmark.file_path);
}
}

let total_benchmarks: usize = packages.iter().map(|p| p.benchmarks.len()).sum();
info!("Total benchmarks discovered: {total_benchmarks}");
for (name, path) in &bench_name_to_path {
info!("Found {name:30} in {path:?}");
}

// 2. Generate codspeed runners, build binaries, and execute them
for package in &packages {
info!("Generating custom runner for package: {}", package.name);
let (_target_dir, runner_path) = builder::templater::run(package)?;
let (_target_dir, runner_path) = builder::templater::run(package, &profile_dir)?;

info!("Building binary for package: {}", package.name);
let binary_path = builder::build_binary(&runner_path)?;
Expand All @@ -55,35 +47,23 @@ pub fn run_benchmarks(project_dir: &Path, cli: &crate::cli::Cli) -> anyhow::Resu
}

// 3. Collect the results
collect_walltime_results(bench_name_to_path)?;
collect_walltime_results(profile_dir.as_ref())?;

Ok(())
}

// TODO: This should be merged with codspeed-rust/codspeed/walltime_results.rs
fn collect_walltime_results(bench_name_to_path: HashMap<String, PathBuf>) -> anyhow::Result<()> {
let profile_dir = std::env::var("CODSPEED_PROFILE_FOLDER")
.context("CODSPEED_PROFILE_FOLDER env var not set")?;
let profile_dir = PathBuf::from(&profile_dir);
let raw_results = results::raw_result::RawResult::parse_folder(&profile_dir)?;
fn collect_walltime_results(profile_dir: &Path) -> anyhow::Result<()> {
let raw_results = results::raw_result::RawResult::parse_folder(profile_dir)?;
info!("Parsed {} raw results", raw_results.len());

let mut benchmarks_by_pid: HashMap<u32, Vec<results::walltime_results::WalltimeBenchmark>> =
HashMap::new();
for raw in raw_results {
// We only parse the `func Benchmark*` name which is the first part of the URI
let func_name = raw
.benchmark_name
.split("::")
.next()
.unwrap_or(&raw.benchmark_name);
let file_path = bench_name_to_path
.get(func_name)
.map(|p| p.to_string_lossy().to_string());
benchmarks_by_pid
.entry(raw.pid)
.or_default()
.push(raw.into_walltime_benchmark(file_path));
.push(raw.into_walltime_benchmark());
}

for (pid, walltime_benchmarks) in benchmarks_by_pid {
Expand Down
3 changes: 2 additions & 1 deletion go-runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ fn main() -> anyhow::Result<()> {
.init();

let cli = Cli::parse();
codspeed_go_runner::run_benchmarks(Path::new("."), &cli)?;
let profile_dir = std::env::var("CODSPEED_PROFILE_FOLDER").unwrap_or("/tmp".into());
codspeed_go_runner::run_benchmarks(profile_dir, Path::new("."), &cli)?;

Ok(())
}
41 changes: 13 additions & 28 deletions go-runner/src/results/raw_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::results::walltime_results::WalltimeBenchmark;
// WARN: Keep in sync with Golang "testing" fork (benchmark.go)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawResult {
pub benchmark_name: String,
pub name: String,
pub uri: String,
pub pid: u32,
pub codspeed_time_per_round_ns: Vec<u64>,

Expand All @@ -32,12 +33,7 @@ impl RawResult {
.collect())
}

pub fn into_walltime_benchmark(self, file_path: Option<String>) -> WalltimeBenchmark {
let name = self.benchmark_name;

let file = file_path.as_deref().unwrap_or("unknown");
let uri = format!("{file}::{name}");

pub fn into_walltime_benchmark(self) -> WalltimeBenchmark {
let times_per_round_ns = self
.codspeed_time_per_round_ns
.iter()
Expand All @@ -52,7 +48,13 @@ impl RawResult {
.collect()
};

WalltimeBenchmark::from_runtime_data(name, uri, iters_per_round, times_per_round_ns, None)
WalltimeBenchmark::from_runtime_data(
self.name,
self.uri,
iters_per_round,
times_per_round_ns,
None,
)
}
}

Expand All @@ -63,33 +65,16 @@ mod tests {
#[test]
fn test_raw_result_deserialization() {
let json_data = r#"{
"benchmark_name": "BenchmarkFibonacci20-16",
"name": "BenchmarkFibonacci20-16",
"uri": "pkg/foo/fib_test.go::BenchmarkFibonacci20-16",
"pid": 777767,
"codspeed_time_per_round_ns": [1000, 2000, 3000]
}"#;
let result: RawResult = serde_json::from_str(json_data).unwrap();

assert_eq!(result.benchmark_name, "BenchmarkFibonacci20-16");
assert_eq!(result.name, "BenchmarkFibonacci20-16");
assert_eq!(result.pid, 777767);
assert_eq!(result.codspeed_time_per_round_ns.len(), 3);
assert_eq!(result.codspeed_iters_per_round.len(), 0); // Default: 1 per round
}

#[test]
fn test_into_walltime_benchmark_with_file_path() {
let raw_result = RawResult {
benchmark_name: "BenchmarkFibonacci20-16".to_string(),
pid: 777767,
codspeed_time_per_round_ns: vec![1000, 2000, 3000],
codspeed_iters_per_round: vec![],
};

// Test with file path - should not panic and create successfully
let _walltime_bench = raw_result
.clone()
.into_walltime_benchmark(Some("pkg/foo/fib_test.go".to_string()));

// Test without file path (should default to TODO) - should not panic and create successfully
let _walltime_bench_no_path = raw_result.into_walltime_benchmark(None);
}
}
Loading
Loading