From 5cd363ed8f52ba936f89a3bb89de77424764b898 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Fri, 10 Apr 2026 18:54:23 +0200 Subject: [PATCH 01/13] feat: add aarch64-apple-darwin to the release targets --- Cargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4d4c210e..a7d57992 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,6 +129,7 @@ lto = "thin" strip = true [package.metadata.dist] -targets = ["aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] +targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] +binaries.aarch64-apple-darwin = ["codspeed"] binaries.aarch64-unknown-linux-musl = ["codspeed"] binaries.x86_64-unknown-linux-musl = ["codspeed"] From 2cc2f037b8f9b532b98c2b975e04b290eee75da5 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Mon, 13 Apr 2026 14:27:39 +0200 Subject: [PATCH 02/13] chore(exec-harness): rerun exec harness build if instrument hooks sources change --- crates/exec-harness/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/exec-harness/build.rs b/crates/exec-harness/build.rs index 49c7cded..bf65ef7e 100644 --- a/crates/exec-harness/build.rs +++ b/crates/exec-harness/build.rs @@ -62,6 +62,7 @@ fn main() { core_c: instrument_hooks_dir.join("dist/core.c"), includes_dir: instrument_hooks_dir.join("includes"), }; + println!("cargo:rerun-if-changed={}", paths.core_c.display()); paths.check_sources_exist(); build_shared_library(&paths, &preload_constants); } From 22668e25f19390c3bc34e5248f24c1f22a472a23 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Mon, 13 Apr 2026 14:28:15 +0200 Subject: [PATCH 03/13] fix: fix instropected_go's behavior on macos On macos a symlink breaks the existing test creating an infinite loop where `instropected_go` detects itself as the real `go` executable. Fixed by changing the behavior to what instrospected_node does. --- src/executor/helpers/introspected_golang/go.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/executor/helpers/introspected_golang/go.sh b/src/executor/helpers/introspected_golang/go.sh index 6aba393d..5601d136 100755 --- a/src/executor/helpers/introspected_golang/go.sh +++ b/src/executor/helpers/introspected_golang/go.sh @@ -18,8 +18,10 @@ if [ "${CODSPEED_RUNNER_MODE:-}" != "walltime" ]; then exit 1 fi -# Find the real go binary, so that we don't end up in infinite recursion -REAL_GO=$(which -a go | grep -v "$(realpath "$0")" | head -1) +# Find the real go binary by removing our directory from PATH (same approach as node.sh) +ORIGINAL_PATH=$(echo "$PATH" | tr ":" "\n" | grep -v "codspeed_introspected_go" | tr "\n" ":") +REAL_GO=$(env PATH="$ORIGINAL_PATH" which go 2>/dev/null || true) +debug_log "Real go path: $REAL_GO" if [ -z "$REAL_GO" ]; then echo "ERROR: Could not find real go binary" >&2 exit 1 From dcff241ee47b1050ccb8f1217a3a1efed89beda3 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Mon, 13 Apr 2026 14:30:42 +0200 Subject: [PATCH 04/13] chore: bump instrument-hooks submodule to include stubs improvement Fixes compilation warnings in exec-harness's build script. --- crates/instrument-hooks-bindings/instrument-hooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/instrument-hooks-bindings/instrument-hooks b/crates/instrument-hooks-bindings/instrument-hooks index 89fb72a0..4376be3e 160000 --- a/crates/instrument-hooks-bindings/instrument-hooks +++ b/crates/instrument-hooks-bindings/instrument-hooks @@ -1 +1 @@ -Subproject commit 89fb72a076ec71c9eca6eee9bca98bada4b4dfb4 +Subproject commit 4376be3e1e2d0e0c82d690bcb8e54bde98b7805c From 9f7b50a611468924ec5c7e13c9e0016bb73af71b Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Mon, 13 Apr 2026 15:00:51 +0200 Subject: [PATCH 05/13] feat: bypass systemd-run usage on macos --- src/executor/wall_time/executor.rs | 27 ++++-------------- src/executor/wall_time/isolation.rs | 43 +++++++++++++++++++++++++++++ src/executor/wall_time/mod.rs | 1 + 3 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 src/executor/wall_time/isolation.rs diff --git a/src/executor/wall_time/executor.rs b/src/executor/wall_time/executor.rs index c317f266..9103204f 100644 --- a/src/executor/wall_time/executor.rs +++ b/src/executor/wall_time/executor.rs @@ -1,12 +1,11 @@ use super::helpers::validate_walltime_results; +use super::isolation::wrap_with_isolation; use super::perf::PerfRunner; use crate::executor::Executor; use crate::executor::ExecutorConfig; use crate::executor::ToolStatus; use crate::executor::helpers::command::CommandBuilder; -use crate::executor::helpers::env::{ - build_path_env, get_base_injected_env, is_codspeed_debug_enabled, -}; +use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; use crate::executor::helpers::run_with_env::wrap_with_env; @@ -105,28 +104,12 @@ impl WallTimeExecutor { bench_cmd.arg(script_file.path()); let (mut bench_cmd, env_file) = wrap_with_env(bench_cmd, &extra_env)?; - let mut cmd_builder = CommandBuilder::new("systemd-run"); if let Some(cwd) = &config.working_directory { let abs_cwd = canonicalize(cwd)?; - cmd_builder.current_dir(abs_cwd); + bench_cmd.current_dir(abs_cwd); } - if !is_codspeed_debug_enabled() { - cmd_builder.arg("--quiet"); - } - // Remarks: - // - We're using --scope so that perf is able to capture the events of the benchmark process. - // - We can't user `--user` here because we need to run in `codspeed.slice`, otherwise we'd run in - // user.slice` (which is isolated). We can use `--gid` and `--uid` to run the command as the current user. - // - We must use `bash` here instead of `sh` since `source` isn't available when symlinked to `dash`. - // - We have to pass the environment variables because `--scope` only inherits the system and not the user environment variables. - cmd_builder.arg("--slice=codspeed.slice"); - cmd_builder.arg("--scope"); - cmd_builder.arg("--same-dir"); - cmd_builder.arg(format!("--uid={}", nix::unistd::Uid::current().as_raw())); - cmd_builder.arg(format!("--gid={}", nix::unistd::Gid::current().as_raw())); - cmd_builder.args(["--"]); - - bench_cmd.wrap_with(cmd_builder); + + let bench_cmd = wrap_with_isolation(bench_cmd)?; Ok((env_file, script_file, bench_cmd)) } diff --git a/src/executor/wall_time/isolation.rs b/src/executor/wall_time/isolation.rs new file mode 100644 index 00000000..37c41ee9 --- /dev/null +++ b/src/executor/wall_time/isolation.rs @@ -0,0 +1,43 @@ +use crate::executor::helpers::command::CommandBuilder; +use crate::prelude::*; + +/// Run the benchmark command in an isolated process scope. +/// +/// On Linux, the command is wrapped with `systemd-run --scope` so it runs inside the +/// `codspeed.slice` cgroup (required for perf to capture the full process tree). +/// +/// Remarks: +/// - We're using `--scope` so that perf is able to capture the events of the benchmark process. +/// - We can't use `--user` here because we need to run in `codspeed.slice`, otherwise we'd run in +/// `user.slice` (which is isolated). We use `--uid` and `--gid` to keep running as the current +/// user. +/// - `--scope` only inherits the system environment, so the caller is expected to have already +/// forwarded the relevant variables (via `wrap_with_env`). +/// - The caller is expected to have already set the working directory on `bench_cmd`; it will be +/// propagated to `systemd-run` via [`CommandBuilder::wrap_with`], and `--same-dir` makes the +/// spawned scope inherit it. +#[cfg(target_os = "linux")] +pub fn wrap_with_isolation(mut bench_cmd: CommandBuilder) -> Result { + use crate::executor::helpers::env::is_codspeed_debug_enabled; + + let mut cmd_builder = CommandBuilder::new("systemd-run"); + if !is_codspeed_debug_enabled() { + cmd_builder.arg("--quiet"); + } + cmd_builder.arg("--slice=codspeed.slice"); + cmd_builder.arg("--scope"); + cmd_builder.arg("--same-dir"); + cmd_builder.arg(format!("--uid={}", nix::unistd::Uid::current().as_raw())); + cmd_builder.arg(format!("--gid={}", nix::unistd::Gid::current().as_raw())); + cmd_builder.args(["--"]); + + bench_cmd.wrap_with(cmd_builder); + Ok(bench_cmd) +} + +/// Dummy implementation on non-Linux platforms: the benchmark command is returned as-is. +// TODO(COD-2513): implement an equivalent process-isolation mechanism on macOS +#[cfg(not(target_os = "linux"))] +pub fn wrap_with_isolation(bench_cmd: CommandBuilder) -> Result { + Ok(bench_cmd) +} diff --git a/src/executor/wall_time/mod.rs b/src/executor/wall_time/mod.rs index b790b6ac..ecd1ce79 100644 --- a/src/executor/wall_time/mod.rs +++ b/src/executor/wall_time/mod.rs @@ -1,3 +1,4 @@ pub mod executor; pub mod helpers; +pub mod isolation; pub mod perf; From 62bd7b337b9afd6c8e1c7080a96cc26e62ed51a4 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Mon, 13 Apr 2026 15:41:56 +0200 Subject: [PATCH 06/13] feat(system): represent the host OS as a typed Os enum with per-executor support gates Replaces the loose `os` + `os_version` strings on `SystemInfo` with an `Os` enum. Adds `Executor::support_level(&Os)` so each executor declares its own OS matrix: `run_executor` bails on `Unsupported`, skips `setup()` on `RequiresManualSetup`, and runs it on `FullySupported`. --- src/cli/setup.rs | 95 ++++++++++++++------ src/cli/status.rs | 7 +- src/executor/helpers/apt.rs | 4 +- src/executor/memory/executor.rs | 14 ++- src/executor/mod.rs | 39 ++++++-- src/executor/valgrind/executor.rs | 23 +++-- src/executor/valgrind/setup.rs | 80 ++++++++++++----- src/executor/wall_time/executor.rs | 18 +++- src/system/check.rs | 50 +---------- src/system/info.rs | 19 ++-- src/system/mod.rs | 2 + src/system/os.rs | 140 +++++++++++++++++++++++++++++ src/upload/interfaces.rs | 4 +- src/upload/upload_metadata.rs | 8 +- 14 files changed, 368 insertions(+), 135 deletions(-) create mode 100644 src/system/os.rs diff --git a/src/cli/setup.rs b/src/cli/setup.rs index 19559dbe..db1d8a9d 100644 --- a/src/cli/setup.rs +++ b/src/cli/setup.rs @@ -1,4 +1,4 @@ -use crate::executor::{ToolInstallStatus, get_all_executors}; +use crate::executor::{ExecutorSupport, ToolInstallStatus, get_all_executors}; use crate::prelude::*; use crate::system::SystemInfo; use clap::{Args, Subcommand}; @@ -22,10 +22,7 @@ enum SetupCommands { pub async fn run(args: SetupArgs, setup_cache_dir: Option<&Path>) -> Result<()> { match args.command { None => setup(setup_cache_dir).await, - Some(SetupCommands::Status) => { - status(); - Ok(()) - } + Some(SetupCommands::Status) => status(), } } @@ -34,41 +31,81 @@ async fn setup(setup_cache_dir: Option<&Path>) -> Result<()> { let executors = get_all_executors(); start_group!("Setting up the environment for all executors"); for executor in executors { - info!( - "Setting up the environment for the executor: {}", - executor.name() - ); - executor.setup(&system_info, setup_cache_dir).await?; + match executor.support_level(&system_info) { + ExecutorSupport::Unsupported => { + info!( + "Skipping setup for the {} executor: not supported on {}", + executor.name(), + system_info.os + ); + } + ExecutorSupport::RequiresManualInstallation => { + info!( + "Skipping automatic setup for the {} executor on {}; install required tooling manually.", + executor.name(), + system_info.os + ); + } + ExecutorSupport::FullySupported => { + info!( + "Setting up the environment for the executor: {}", + executor.name() + ); + executor.setup(&system_info, setup_cache_dir).await?; + } + } } info!("Environment setup completed"); end_group!(); Ok(()) } -pub fn status() { +pub fn status() -> Result<()> { + let system_info = SystemInfo::new()?; info!("{}", style("Tools").bold()); for executor in get_all_executors() { - let tool_status = executor.tool_status(); - match &tool_status.status { - ToolInstallStatus::Installed { version } => { - info!(" {} {} ({})", check_mark(), tool_status.tool_name, version); - } - ToolInstallStatus::IncorrectVersion { version, message } => { - info!( - " {} {} ({}, {})", - warn_mark(), - tool_status.tool_name, - version, - message - ); - } - ToolInstallStatus::NotInstalled => { + // Don't probe for tooling that can't be used on this OS anyway. + if executor.support_level(&system_info) == ExecutorSupport::Unsupported { + continue; + } + match executor.tool_status() { + Some(tool_status) => match &tool_status.status { + ToolInstallStatus::Installed { version } => { + info!( + " {} {} executor: {} ({})", + check_mark(), + executor.name(), + tool_status.tool_name, + version + ); + } + ToolInstallStatus::IncorrectVersion { version, message } => { + info!( + " {} {} executor: {} ({}, {})", + warn_mark(), + executor.name(), + tool_status.tool_name, + version, + message + ); + } + ToolInstallStatus::NotInstalled => { + info!( + " {} {} executor: {} (not installed)", + cross_mark(), + executor.name(), + tool_status.tool_name + ); + } + }, + None => { info!( - " {} {} (not installed)", - cross_mark(), - tool_status.tool_name + " {} {} executor: No tool to install", + check_mark(), + executor.name() ); } } } + Ok(()) } diff --git a/src/cli/status.rs b/src/cli/status.rs index b8fe3f90..13a5b1b7 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -22,17 +22,14 @@ pub async fn run(api_client: &CodSpeedAPIClient) -> Result<()> { info!(""); // Setup/tools status - super::setup::status(); + super::setup::status()?; info!(""); // System info info!("{}", style("System").bold()); info!(" codspeed {VERSION}"); let system_info = SystemInfo::new()?; - info!( - " {} {} ({})", - system_info.os, system_info.os_version, system_info.arch - ); + info!(" {} ({})", system_info.os, system_info.arch); info!( " {} ({}C / {}GB)", system_info.cpu_brand, system_info.cpu_cores, system_info.total_memory_gb diff --git a/src/executor/helpers/apt.rs b/src/executor/helpers/apt.rs index 00fb1cfc..8d430bdc 100644 --- a/src/executor/helpers/apt.rs +++ b/src/executor/helpers/apt.rs @@ -1,13 +1,13 @@ use super::run_with_sudo::run_with_sudo; use crate::prelude::*; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; use std::path::Path; use std::process::Command; const METADATA_FILENAME: &str = "./tmp/codspeed-cache-metadata.txt"; fn is_system_compatible(system_info: &SystemInfo) -> bool { - system_info.os == "ubuntu" || system_info.os == "debian" + matches!(system_info.os, SupportedOs::Linux(ref distro) if distro.is_supported()) } /// Installs packages with caching support. diff --git a/src/executor/memory/executor.rs b/src/executor/memory/executor.rs index d5c8320e..03f56aaa 100644 --- a/src/executor/memory/executor.rs +++ b/src/executor/memory/executor.rs @@ -1,4 +1,5 @@ use crate::executor::ExecutorName; +use crate::executor::ExecutorSupport; use crate::executor::ToolStatus; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::get_base_injected_env; @@ -11,7 +12,7 @@ use crate::executor::{ExecutionContext, Executor}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; use crate::runner_mode::RunnerMode; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; use async_trait::async_trait; use ipc_channel::ipc; use memtrack::MemtrackIpcClient; @@ -72,8 +73,15 @@ impl Executor for MemoryExecutor { ExecutorName::Memory } - fn tool_status(&self) -> ToolStatus { - get_memtrack_status() + fn tool_status(&self) -> Option { + Some(get_memtrack_status()) + } + + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { + match &system_info.os { + SupportedOs::Linux(_) => ExecutorSupport::FullySupported, + SupportedOs::Macos { .. } => ExecutorSupport::Unsupported, + } } async fn setup( diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 3cee058e..353a1669 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -73,12 +73,28 @@ pub enum ToolInstallStatus { NotInstalled, } +/// How well a given executor runs on a given [`SupportedOs`]. +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub enum ExecutorSupport { + /// The executor cannot run on this OS at all — `run_executor` hard-bails. + Unsupported, + /// The executor runs on this OS, but the user is responsible for installing the required tooling themselves. + RequiresManualInstallation, + /// The executor runs on this OS and `setup()` knows how to auto-install tooling. + FullySupported, +} + #[async_trait(?Send)] pub trait Executor { fn name(&self) -> ExecutorName; /// Report the installation status of the tool(s) this executor depends on. - fn tool_status(&self) -> ToolStatus; + fn tool_status(&self) -> Option; + + /// Declare how well this executor runs on the host system. Drives whether `setup()` is invoked + /// (only when [`ExecutorSupport::FullySupported`]) and whether we bail out of running the + /// executor at all (on [`ExecutorSupport::Unsupported`]). + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport; async fn setup( &self, @@ -107,11 +123,24 @@ pub async fn run_executor( execution_context: &ExecutionContext, setup_cache_dir: Option<&Path>, ) -> Result<()> { - if !execution_context.config.skip_setup { - executor - .setup(&orchestrator.system_info, setup_cache_dir) - .await?; + match executor.support_level(&orchestrator.system_info) { + ExecutorSupport::Unsupported => { + bail!( + "The {} executor is not supported on {}", + executor.name(), + orchestrator.system_info.os + ); + } + ExecutorSupport::RequiresManualInstallation | ExecutorSupport::FullySupported => { + if !execution_context.config.skip_setup { + executor + .setup(&orchestrator.system_info, setup_cache_dir) + .await?; + } + } + } + if !execution_context.config.skip_setup { // TODO: refactor and move directly in the Instruments struct as a `setup` method if execution_context.config.instruments.is_mongodb_enabled() { install_mongodb_tracer().await?; diff --git a/src/executor/valgrind/executor.rs b/src/executor/valgrind/executor.rs index c81c1046..cfdbe88a 100644 --- a/src/executor/valgrind/executor.rs +++ b/src/executor/valgrind/executor.rs @@ -3,12 +3,12 @@ use std::path::Path; use crate::executor::Executor; use crate::executor::ToolStatus; -use crate::executor::{ExecutionContext, ExecutorName}; +use crate::executor::{ExecutionContext, ExecutorName, ExecutorSupport}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; -use super::setup::{get_valgrind_status, install_valgrind}; +use super::setup::{get_codspeed_valgrind_filename, get_valgrind_status, install_valgrind}; use super::{helpers::perf_maps::harvest_perf_maps, helpers::venv_compat, measure}; pub struct ValgrindExecutor; @@ -19,8 +19,21 @@ impl Executor for ValgrindExecutor { ExecutorName::Valgrind } - fn tool_status(&self) -> ToolStatus { - get_valgrind_status() + fn tool_status(&self) -> Option { + Some(get_valgrind_status()) + } + + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { + match &system_info.os { + SupportedOs::Linux(_) => { + if get_codspeed_valgrind_filename(system_info).is_ok() { + ExecutorSupport::FullySupported + } else { + ExecutorSupport::RequiresManualInstallation + } + } + SupportedOs::Macos { .. } => ExecutorSupport::Unsupported, + } } async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 0d04c892..391673c5 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -2,7 +2,7 @@ use crate::cli::run::helpers::download_file; use crate::executor::helpers::apt; use crate::executor::{ToolInstallStatus, ToolStatus}; use crate::prelude::*; -use crate::system::SystemInfo; +use crate::system::{LinuxDistribution, SupportedOs, SystemInfo}; use crate::{ VALGRIND_CODSPEED_DEB_VERSION, VALGRIND_CODSPEED_VERSION, VALGRIND_CODSPEED_VERSION_STRING, }; @@ -10,23 +10,37 @@ use semver::Version; use std::{env, path::Path, process::Command}; use url::Url; -fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result { - let (version, architecture) = match ( - system_info.os.as_str(), - system_info.os_version.as_str(), - system_info.arch.as_str(), - ) { - ("ubuntu", "22.04", "x86_64") | ("debian", "12", "x86_64") => ("22.04", "amd64"), - ("ubuntu", "24.04", "x86_64") => ("24.04", "amd64"), - ("ubuntu", "22.04", "aarch64") | ("debian", "12", "aarch64") => ("22.04", "arm64"), - ("ubuntu", "24.04", "aarch64") => ("24.04", "arm64"), +pub(super) fn get_codspeed_valgrind_filename(system_info: &SystemInfo) -> Result { + let SupportedOs::Linux(distro) = &system_info.os else { + bail!("Unsupported system"); + }; + + let (deb_ubuntu_version, architecture) = match (distro, system_info.arch.as_str()) { + (LinuxDistribution::Ubuntu { version }, "x86_64") + | (LinuxDistribution::Debian { version }, "x86_64") + if version == "22.04" || version == "12" => + { + ("22.04", "amd64") + } + (LinuxDistribution::Ubuntu { version }, "x86_64") if version == "24.04" => { + ("24.04", "amd64") + } + (LinuxDistribution::Ubuntu { version }, "aarch64") + | (LinuxDistribution::Debian { version }, "aarch64") + if version == "22.04" || version == "12" => + { + ("22.04", "arm64") + } + (LinuxDistribution::Ubuntu { version }, "aarch64") if version == "24.04" => { + ("24.04", "arm64") + } _ => bail!("Unsupported system"), }; Ok(format!( "valgrind_{}_ubuntu-{}_{}.deb", VALGRIND_CODSPEED_DEB_VERSION.as_str(), - version, + deb_ubuntu_version, architecture )) } @@ -175,8 +189,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_ubuntu() { let system_info = SystemInfo { - os: "ubuntu".to_string(), - os_version: "22.04".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "22.04".into(), + }), arch: "x86_64".to_string(), ..SystemInfo::test() }; @@ -189,8 +204,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_ubuntu_24() { let system_info = SystemInfo { - os: "ubuntu".to_string(), - os_version: "24.04".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "24.04".into(), + }), arch: "x86_64".to_string(), ..SystemInfo::test() }; @@ -203,8 +219,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_debian() { let system_info = SystemInfo { - os: "debian".to_string(), - os_version: "12".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Debian { + version: "12".into(), + }), arch: "x86_64".to_string(), ..SystemInfo::test() }; @@ -217,8 +234,9 @@ mod tests { #[test] fn test_system_info_to_codspeed_valgrind_version_ubuntu_arm() { let system_info = SystemInfo { - os: "ubuntu".to_string(), - os_version: "22.04".to_string(), + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "22.04".into(), + }), arch: "aarch64".to_string(), ..SystemInfo::test() }; @@ -228,6 +246,28 @@ mod tests { ); } + #[test] + fn test_codspeed_valgrind_filename_unsupported_os() { + let system_info = SystemInfo { + os: SupportedOs::Macos { + version: "14.0".into(), + }, + ..SystemInfo::test() + }; + assert!(get_codspeed_valgrind_filename(&system_info).is_err()); + } + + #[test] + fn test_codspeed_valgrind_filename_unsupported_distro() { + let system_info = SystemInfo { + os: SupportedOs::Linux(LinuxDistribution::Ubuntu { + version: "20.04".into(), + }), + ..SystemInfo::test() + }; + assert!(get_codspeed_valgrind_filename(&system_info).is_err()); + } + #[test] fn test_parse_valgrind_codspeed_version_with_prefix() { let version = parse_valgrind_codspeed_version("valgrind-3.25.1.codspeed").unwrap(); diff --git a/src/executor/wall_time/executor.rs b/src/executor/wall_time/executor.rs index 9103204f..4da11899 100644 --- a/src/executor/wall_time/executor.rs +++ b/src/executor/wall_time/executor.rs @@ -10,11 +10,11 @@ use crate::executor::helpers::get_bench_command::get_bench_command; use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; use crate::executor::helpers::run_with_env::wrap_with_env; use crate::executor::helpers::run_with_sudo::wrap_with_sudo; -use crate::executor::{ExecutionContext, ExecutorName}; +use crate::executor::{ExecutionContext, ExecutorName, ExecutorSupport}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; use crate::runner_mode::RunnerMode; -use crate::system::SystemInfo; +use crate::system::{SupportedOs, SystemInfo}; use async_trait::async_trait; use std::fs::canonicalize; use std::io::Write; @@ -121,8 +121,18 @@ impl Executor for WallTimeExecutor { ExecutorName::WallTime } - fn tool_status(&self) -> ToolStatus { - super::perf::setup::get_perf_status() + fn tool_status(&self) -> Option { + self.perf + .as_ref() + .map(|_| super::perf::setup::get_perf_status()) + } + + fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { + match &system_info.os { + SupportedOs::Linux(distro) if distro.is_supported() => ExecutorSupport::FullySupported, + SupportedOs::Macos { .. } => ExecutorSupport::FullySupported, + SupportedOs::Linux(_) => ExecutorSupport::RequiresManualInstallation, + } } async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { diff --git a/src/system/check.rs b/src/system/check.rs index 9aa35b0d..d291606c 100644 --- a/src/system/check.rs +++ b/src/system/check.rs @@ -1,56 +1,8 @@ -use std::collections::HashSet; -use std::sync::LazyLock; - use crate::prelude::*; use super::SystemInfo; -static SUPPORTED_SYSTEMS: LazyLock> = - LazyLock::new(|| { - HashSet::from([ - ("ubuntu", "22.04", "x86_64"), - ("ubuntu", "24.04", "x86_64"), - ("ubuntu", "22.04", "aarch64"), - ("ubuntu", "24.04", "aarch64"), - ("debian", "12", "x86_64"), - ("debian", "12", "aarch64"), - ]) - }); - -/// Checks if the provided system info is supported -/// -/// Supported systems: -/// - Ubuntu 20.04 x86_64 -/// - Ubuntu 22.04 x86_64 and aarch64 -/// - Debian 11 x86_64 -/// - Debian 12 x86_64 pub fn check_system(system_info: &SystemInfo) -> Result<()> { debug!("System info: {system_info:#?}"); - - let system_tuple = ( - system_info.os.as_str(), - system_info.os_version.as_str(), - system_info.arch.as_str(), - ); - - if SUPPORTED_SYSTEMS.contains(&system_tuple) { - return Ok(()); - } - - match system_info.arch.as_str() { - "x86_64" | "aarch64" => { - warn!( - "Unofficially supported system: {} {}. Continuing with best effort support.", - system_info.os, system_info.os_version - ); - return Ok(()); - } - _ => {} - } - - bail!( - "Unsupported system: {} {}", - system_info.os, - system_info.os_version - ); + Ok(()) } diff --git a/src/system/info.rs b/src/system/info.rs index d62fec01..51f1bde4 100644 --- a/src/system/info.rs +++ b/src/system/info.rs @@ -1,9 +1,10 @@ use std::process::Command; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; use crate::prelude::*; +use crate::system::os::SupportedOs; fn get_user() -> Result { let user_output = Command::new("whoami") @@ -17,11 +18,12 @@ fn get_user() -> Result { Ok(output_str.trim().to_string()) } -#[derive(Eq, PartialEq, Hash, Serialize, Deserialize, Debug, Clone)] +#[derive(Eq, PartialEq, Hash, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SystemInfo { - pub os: String, - pub os_version: String, + /// Flattened to the `os` and `osVersion` fields on the wire via [`SupportedOs`]'s serde impl. + #[serde(flatten)] + pub os: SupportedOs, pub arch: String, pub host: String, pub user: String, @@ -37,8 +39,9 @@ pub struct SystemInfo { impl SystemInfo { pub fn test() -> Self { SystemInfo { - os: "ubuntu".to_string(), - os_version: "20.04".to_string(), + os: SupportedOs::Linux(crate::system::LinuxDistribution::Ubuntu { + version: "20.04".into(), + }), arch: "x86_64".to_string(), host: "host".to_string(), user: "user".to_string(), @@ -96,8 +99,7 @@ fn get_cpu_flags() -> Vec { impl SystemInfo { pub fn new() -> Result { - let os = System::distribution_id(); - let os_version = System::os_version().ok_or(anyhow!("Failed to get OS version"))?; + let os = SupportedOs::from_os(std::env::consts::OS)?; let arch = System::cpu_arch(); let user = get_user()?; let host = System::host_name().ok_or(anyhow!("Failed to get host name"))?; @@ -133,7 +135,6 @@ impl SystemInfo { Ok(SystemInfo { os, - os_version, arch, host, user, diff --git a/src/system/mod.rs b/src/system/mod.rs index df68d6a9..219cd831 100644 --- a/src/system/mod.rs +++ b/src/system/mod.rs @@ -1,5 +1,7 @@ mod check; mod info; +mod os; pub use check::check_system; pub use info::SystemInfo; +pub use os::{LinuxDistribution, SupportedOs}; diff --git a/src/system/os.rs b/src/system/os.rs new file mode 100644 index 00000000..b83cb22b --- /dev/null +++ b/src/system/os.rs @@ -0,0 +1,140 @@ +use std::fmt::{self, Display}; + +use serde::{Deserialize, Serialize}; +use sysinfo::System; + +use crate::prelude::*; +/// Typed representation of the host operating system. +/// +/// Only operating systems that CodSpeed can run on are represented here. +/// Construction via [`SupportedOs::from_current_system`] bails on unsupported platforms +#[derive(Eq, PartialEq, Hash, Debug, Clone, Serialize)] +#[serde(into = "SupportedOsSerde")] +pub enum SupportedOs { + Linux(LinuxDistribution), + Macos { version: String }, +} + +impl SupportedOs { + /// Build a [`SupportedOs`] from the given OS family string. + /// Expects `std::env::consts::OS` as input + /// + /// For Linux, the distribution is identified via `sysinfo::System::distribution_id()`. + /// The OS version is read from `sysinfo::System::os_version()`. + pub fn from_os(os: &str) -> Result { + let os_version = System::os_version().ok_or(anyhow!("Failed to get OS version"))?; + match os { + "linux" => { + let os_id = System::distribution_id(); + Ok(Self::Linux(LinuxDistribution::from_id(&os_id, &os_version))) + } + "macos" => Ok(Self::Macos { + version: os_version, + }), + unsupported => bail!("Unsupported operating system: {unsupported}"), + } + } + + /// The distro/OS id as it appears on the wire (matches `sysinfo::System::distribution_id()`). + pub fn id(&self) -> &str { + match self { + Self::Linux(distro) => distro.id(), + Self::Macos { .. } => "macos", + } + } + + pub fn version(&self) -> &str { + match self { + Self::Linux(distro) => distro.version(), + Self::Macos { version } => version, + } + } +} + +impl Display for SupportedOs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.id(), self.version()) + } +} + +/// Flat `{os, osVersion}` shape we emit on the wire as part of `SystemInfo`. +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SupportedOsSerde { + os: String, + os_version: String, +} + +impl From for SupportedOsSerde { + fn from(os: SupportedOs) -> Self { + SupportedOsSerde { + os: os.id().to_string(), + os_version: os.version().to_string(), + } + } +} + +/// Linux distribution, identified by the `sysinfo` distribution id. +#[derive(Eq, PartialEq, Hash, Debug, Clone)] +pub enum LinuxDistribution { + Ubuntu { version: String }, + Debian { version: String }, + Other { name: String, version: String }, +} + +impl LinuxDistribution { + /// Build a [`LinuxDistribution`] from the raw `(os_id, version)` strings reported by `sysinfo`. + fn from_id(os_id: &str, version: &str) -> Self { + match os_id { + "ubuntu" => Self::Ubuntu { + version: version.to_string(), + }, + "debian" => Self::Debian { + version: version.to_string(), + }, + _ => Self::Other { + name: os_id.to_string(), + version: version.to_string(), + }, + } + } + + /// The distro id as it appears on the wire (matches `sysinfo::System::distribution_id()`). + pub fn id(&self) -> &str { + match self { + Self::Ubuntu { .. } => "ubuntu", + Self::Debian { .. } => "debian", + Self::Other { name, .. } => name, + } + } + + pub fn version(&self) -> &str { + match self { + Self::Ubuntu { version } | Self::Debian { version } | Self::Other { version, .. } => { + version + } + } + } + + /// Whether this distribution has first-class support (auto-install via apt, prebuilt .debs, etc.). + pub fn is_supported(&self) -> bool { + matches!(self, Self::Ubuntu { .. } | Self::Debian { .. }) + } +} + +impl Display for LinuxDistribution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.id(), self.version()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_os_bails_on_unsupported() { + let err = SupportedOs::from_os("windows").unwrap_err(); + assert_eq!(err.to_string(), "Unsupported operating system: windows"); + } +} diff --git a/src/upload/interfaces.rs b/src/upload/interfaces.rs index c6dc2a8d..e07c4525 100644 --- a/src/upload/interfaces.rs +++ b/src/upload/interfaces.rs @@ -7,7 +7,7 @@ use crate::system::SystemInfo; pub const LATEST_UPLOAD_METADATA_VERSION: u32 = 10; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct UploadMetadata { pub repository_provider: RepositoryProvider, @@ -24,7 +24,7 @@ pub struct UploadMetadata { pub run_environment_metadata: RunEnvironmentMetadata, } -#[derive(Deserialize, Serialize, Debug)] +#[derive(Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Runner { pub name: String, diff --git a/src/upload/upload_metadata.rs b/src/upload/upload_metadata.rs index 68cf51d3..fd64758e 100644 --- a/src/upload/upload_metadata.rs +++ b/src/upload/upload_metadata.rs @@ -96,8 +96,12 @@ mod tests { instruments: vec![], executor: ExecutorName::Valgrind, system_info: SystemInfo { - os: "nixos".to_string(), - os_version: "25.11".to_string(), + os: crate::system::SupportedOs::Linux( + crate::system::LinuxDistribution::Other { + name: "nixos".into(), + version: "25.11".into(), + }, + ), arch: "x86_64".to_string(), host: "badlands".to_string(), user: "guillaume".to_string(), From 2fdce4e2561bae0982d33ab4dc8e2f513052aaaa Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Mon, 13 Apr 2026 16:07:46 +0200 Subject: [PATCH 07/13] chore: skip tests that rely on linux behavior These tests panic on macOS, and for now are not relevant since we do not support profiling on macOS. When we do support it, we should have os-specific tests --- src/executor/shared/fifo.rs | 2 +- src/executor/tests.rs | 2 ++ src/executor/wall_time/perf/debug_info.rs | 4 +++- src/executor/wall_time/perf/module_symbols.rs | 2 +- src/executor/wall_time/perf/unwind_data.rs | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/executor/shared/fifo.rs b/src/executor/shared/fifo.rs index 39b9533b..577f7bed 100644 --- a/src/executor/shared/fifo.rs +++ b/src/executor/shared/fifo.rs @@ -275,7 +275,7 @@ impl RunnerFifo { } } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; use std::time::Duration; diff --git a/src/executor/tests.rs b/src/executor/tests.rs index db982741..fe38e3a5 100644 --- a/src/executor/tests.rs +++ b/src/executor/tests.rs @@ -146,6 +146,7 @@ async fn acquire_bpf_instrumentation_lock() -> SemaphorePermit<'static> { semaphore.acquire().await.unwrap() } +#[cfg(target_os = "linux")] mod valgrind { use super::*; use crate::executor::valgrind::executor::ValgrindExecutor; @@ -377,6 +378,7 @@ fi } } +#[cfg(target_os = "linux")] #[test_with::env(GITHUB_ACTIONS)] mod memory { use super::*; diff --git a/src/executor/wall_time/perf/debug_info.rs b/src/executor/wall_time/perf/debug_info.rs index 5e252bb3..5109044b 100644 --- a/src/executor/wall_time/perf/debug_info.rs +++ b/src/executor/wall_time/perf/debug_info.rs @@ -127,7 +127,9 @@ pub fn debug_info_by_path( .collect() } -#[cfg(test)] +// These tests parse Linux ELF binaries from `testdata/`; gate them on Linux so they're not +// attempted (and don't abort the test process) when running on macOS. +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; diff --git a/src/executor/wall_time/perf/module_symbols.rs b/src/executor/wall_time/perf/module_symbols.rs index c9d8fb1f..240a2602 100644 --- a/src/executor/wall_time/perf/module_symbols.rs +++ b/src/executor/wall_time/perf/module_symbols.rs @@ -177,7 +177,7 @@ impl ModuleSymbols { } } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; diff --git a/src/executor/wall_time/perf/unwind_data.rs b/src/executor/wall_time/perf/unwind_data.rs index 3b538309..ae39e69d 100644 --- a/src/executor/wall_time/perf/unwind_data.rs +++ b/src/executor/wall_time/perf/unwind_data.rs @@ -87,7 +87,7 @@ pub fn unwind_data_from_elf( Ok((v3, mapping)) } -#[cfg(test)] +#[cfg(all(test, target_os = "linux"))] mod tests { use super::*; From bd98f373aa68ab1b7768cf839c2e9a07c511d2a9 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 16 Apr 2026 09:46:16 +0200 Subject: [PATCH 08/13] chore: add a small powershell script to sync from a shared VM folder Had a lot of issues building directly within the shared folder, this script can be thrown away if it's no longer relevant. --- scripts/sync-to-local.ps1 | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 scripts/sync-to-local.ps1 diff --git a/scripts/sync-to-local.ps1 b/scripts/sync-to-local.ps1 new file mode 100644 index 00000000..58699ca9 --- /dev/null +++ b/scripts/sync-to-local.ps1 @@ -0,0 +1,16 @@ +# Syncs the repo from the shared folder (Z:\) to a local working copy. +# +# Usage: powershell -ExecutionPolicy Bypass -File Z:\scripts\sync-to-local.ps1 + +$ErrorActionPreference = "Stop" + +$syncs = @( + @{ SRC = "Z:\codspeed"; DST = "C:\Users\vboxuser\codspeed" }, + @{ SRC = "Z:\codspeed-rust"; DST = "C:\Users\vboxuser\codspeed-rust" } +) + +foreach ($sync in $syncs) { + # /MIR = mirror (copy + delete extras), /XD = exclude dirs, /MT = multi-threaded + robocopy $sync.SRC $sync.DST /MIR /MT /XD .git target target_win node_modules /NFL /NDL /NJH /NJS /NC /NS + Write-Host "Synced $($sync.SRC) -> $($sync.DST)" +} From 33499604670fbe3f946463a17f209db2fc9aa2cb Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Tue, 14 Apr 2026 16:44:15 +0200 Subject: [PATCH 09/13] chore: define pid_t in runner-shared instead of relying on libc's type `pid_t` is not available on msvc in the libc crate. --- Cargo.lock | 1 - crates/runner-shared/Cargo.toml | 1 - crates/runner-shared/src/artifacts/memtrack.rs | 3 ++- crates/runner-shared/src/artifacts/mod.rs | 3 ++- crates/runner-shared/src/lib.rs | 4 ++++ crates/runner-shared/src/metadata.rs | 3 ++- src/executor/helpers/harvest_perf_maps_for_pids.rs | 2 +- src/executor/valgrind/helpers/perf_maps.rs | 2 +- src/executor/wall_time/perf/jit_dump.rs | 2 +- src/executor/wall_time/perf/parse_perf_file.rs | 2 +- src/executor/wall_time/perf/save_artifacts.rs | 2 +- src/runner_mode/shell_session.rs | 2 +- 12 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 42aa1a05..de97e760 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3540,7 +3540,6 @@ dependencies = [ "bincode", "codspeed-divan-compat", "itertools 0.14.0", - "libc", "log", "rand 0.8.5", "rmp", diff --git a/crates/runner-shared/Cargo.toml b/crates/runner-shared/Cargo.toml index 6ca3db3d..07379351 100644 --- a/crates/runner-shared/Cargo.toml +++ b/crates/runner-shared/Cargo.toml @@ -13,7 +13,6 @@ itertools = { workspace = true } log = { workspace = true } rmp = "0.8.14" rmp-serde = "1.3.0" -libc = { workspace = true } zstd = "0.13" [dev-dependencies] diff --git a/crates/runner-shared/src/artifacts/memtrack.rs b/crates/runner-shared/src/artifacts/memtrack.rs index 64c90e82..15cf2d09 100644 --- a/crates/runner-shared/src/artifacts/memtrack.rs +++ b/crates/runner-shared/src/artifacts/memtrack.rs @@ -1,5 +1,6 @@ -use libc::pid_t; use serde::{Deserialize, Serialize}; + +use crate::pid_t; use std::io::{BufReader, BufWriter, Read, Write}; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/runner-shared/src/artifacts/mod.rs b/crates/runner-shared/src/artifacts/mod.rs index 6df5ff35..6611d150 100644 --- a/crates/runner-shared/src/artifacts/mod.rs +++ b/crates/runner-shared/src/artifacts/mod.rs @@ -1,7 +1,8 @@ use std::io::BufReader; -use libc::pid_t; use log::debug; + +use crate::pid_t; use serde::{Deserialize, Serialize}; mod execution_timestamps; diff --git a/crates/runner-shared/src/lib.rs b/crates/runner-shared/src/lib.rs index 61e804de..0f771b71 100644 --- a/crates/runner-shared/src/lib.rs +++ b/crates/runner-shared/src/lib.rs @@ -6,3 +6,7 @@ pub mod module_symbols; pub mod perf_event; pub mod unwind_data; pub mod walltime_results; + +/// Process ID type, equivalent to `libc::pid_t` on Unix. +#[allow(non_camel_case_types)] +pub type pid_t = i32; diff --git a/crates/runner-shared/src/metadata.rs b/crates/runner-shared/src/metadata.rs index b5c7d46c..d9b18bd2 100644 --- a/crates/runner-shared/src/metadata.rs +++ b/crates/runner-shared/src/metadata.rs @@ -1,6 +1,7 @@ use anyhow::Context; -use libc::pid_t; use serde::{Deserialize, Serialize}; + +use crate::pid_t; use std::collections::HashMap; use std::io::BufWriter; use std::path::Path; diff --git a/src/executor/helpers/harvest_perf_maps_for_pids.rs b/src/executor/helpers/harvest_perf_maps_for_pids.rs index 4ffda881..76b48539 100644 --- a/src/executor/helpers/harvest_perf_maps_for_pids.rs +++ b/src/executor/helpers/harvest_perf_maps_for_pids.rs @@ -5,7 +5,7 @@ use tokio::fs; pub async fn harvest_perf_maps_for_pids( profile_folder: &Path, - pids: &HashSet, + pids: &HashSet, ) -> Result<()> { let perf_maps = pids .iter() diff --git a/src/executor/valgrind/helpers/perf_maps.rs b/src/executor/valgrind/helpers/perf_maps.rs index 583ca2f9..5260e50a 100644 --- a/src/executor/valgrind/helpers/perf_maps.rs +++ b/src/executor/valgrind/helpers/perf_maps.rs @@ -7,7 +7,7 @@ use std::path::Path; /// Extracts a PID from a profile output file path. /// /// Supports both Callgrind (`.out`) and Tracegrind (`.tgtrace`) file formats. -fn extract_pid_from_profile_file(path: &Path) -> Option { +fn extract_pid_from_profile_file(path: &Path) -> Option { let ext = path.extension()?.to_str()?; match ext { "out" | "tgtrace" => path.file_stem()?.to_str()?.parse().ok(), diff --git a/src/executor/wall_time/perf/jit_dump.rs b/src/executor/wall_time/perf/jit_dump.rs index f306de08..50f10213 100644 --- a/src/executor/wall_time/perf/jit_dump.rs +++ b/src/executor/wall_time/perf/jit_dump.rs @@ -118,7 +118,7 @@ impl JitDump { /// Unwind data is generated as a list pub async fn save_symbols_and_harvest_unwind_data_for_pids( profile_folder: &Path, - pids: &HashSet, + pids: &HashSet, ) -> Result>> { let mut jit_unwind_data_by_path = HashMap::new(); diff --git a/src/executor/wall_time/perf/parse_perf_file.rs b/src/executor/wall_time/perf/parse_perf_file.rs index 9f5b0fd8..932cbff0 100644 --- a/src/executor/wall_time/perf/parse_perf_file.rs +++ b/src/executor/wall_time/perf/parse_perf_file.rs @@ -1,11 +1,11 @@ use super::module_symbols::ModuleSymbols; use super::unwind_data::unwind_data_from_elf; use crate::prelude::*; -use libc::pid_t; use linux_perf_data::PerfFileReader; use linux_perf_data::PerfFileRecord; use linux_perf_data::linux_perf_event_reader::EventRecord; use linux_perf_data::linux_perf_event_reader::RecordType; +use runner_shared::pid_t; use runner_shared::unwind_data::ProcessUnwindData; use runner_shared::unwind_data::UnwindData; use std::collections::HashMap; diff --git a/src/executor/wall_time/perf/save_artifacts.rs b/src/executor/wall_time/perf/save_artifacts.rs index e5b1d572..7ef0720c 100644 --- a/src/executor/wall_time/perf/save_artifacts.rs +++ b/src/executor/wall_time/perf/save_artifacts.rs @@ -3,10 +3,10 @@ use crate::executor::wall_time::perf::debug_info::debug_info_by_path; use crate::executor::wall_time::perf::naming; use crate::executor::wall_time::perf::parse_perf_file::LoadedModule; use crate::prelude::*; -use libc::pid_t; use rayon::prelude::*; use runner_shared::debug_info::{MappedProcessDebugInfo, ModuleDebugInfo}; use runner_shared::module_symbols::MappedProcessModuleSymbols; +use runner_shared::pid_t; use runner_shared::unwind_data::{MappedProcessUnwindData, ProcessUnwindData, UnwindData}; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/src/runner_mode/shell_session.rs b/src/runner_mode/shell_session.rs index 46586563..a371f7b9 100644 --- a/src/runner_mode/shell_session.rs +++ b/src/runner_mode/shell_session.rs @@ -1,6 +1,6 @@ use super::RunnerMode; use crate::prelude::*; -use libc::pid_t; +use runner_shared::pid_t; use std::path::Path; use std::path::PathBuf; use std::sync::OnceLock; From 6c9a196bc71ccd48fb1048ba9b24f99d3628b55e Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 16 Apr 2026 10:17:02 +0200 Subject: [PATCH 10/13] chore: fix compilation of mock impl of instrument hooks This aligns the function signature with the actual implementation. --- crates/instrument-hooks-bindings/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/instrument-hooks-bindings/src/lib.rs b/crates/instrument-hooks-bindings/src/lib.rs index c4a12aaa..1a95a4da 100644 --- a/crates/instrument-hooks-bindings/src/lib.rs +++ b/crates/instrument-hooks-bindings/src/lib.rs @@ -163,7 +163,7 @@ mod other_impl { pub struct InstrumentHooks; impl InstrumentHooks { - pub fn instance() -> &'static Self { + pub fn instance(_integration: &str, _version: &str) -> &'static Self { static INSTANCE: InstrumentHooks = InstrumentHooks; &INSTANCE } From e80b0eb0d18ed32bf298159735f052137ca05506 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 16 Apr 2026 10:28:34 +0200 Subject: [PATCH 11/13] feat: use dirs to find config dir rather than re-inventing the wheel --- Cargo.lock | 1 + Cargo.toml | 7 ++++++- src/config.rs | 9 ++------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de97e760..4d7e5c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,6 +691,7 @@ dependencies = [ "clap", "console", "debugid", + "dirs", "exec-harness", "futures", "gimli", diff --git a/Cargo.toml b/Cargo.toml index a7d57992..fee0a574 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,6 +73,7 @@ shell-words = "1.1.0" rmp-serde = "1.3.0" uuid = { version = "1.21.0", features = ["v4"] } which = "8.0.2" +dirs = "6.0.0" [target.'cfg(target_os = "linux")'.dependencies] procfs = "0.17.0" @@ -129,7 +130,11 @@ lto = "thin" strip = true [package.metadata.dist] -targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl"] +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-musl", + "x86_64-unknown-linux-musl", +] binaries.aarch64-apple-darwin = ["codspeed"] binaries.aarch64-unknown-linux-musl = ["codspeed"] binaries.x86_64-unknown-linux-musl = ["codspeed"] diff --git a/src/config.rs b/src/config.rs index 1e3fdf94..2816aad2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::{env, fs, path::PathBuf}; +use std::{fs, path::PathBuf}; use crate::prelude::*; use nestify::nest; @@ -28,12 +28,7 @@ nest! { /// If config_name is None, returns ~/.config/codspeed/config.yaml (default) /// If config_name is Some, returns ~/.config/codspeed/{config_name}.yaml fn get_configuration_file_path(config_name: Option<&str>) -> PathBuf { - let config_dir = env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| { - let home = env::var("HOME").expect("HOME env variable not set"); - PathBuf::from(home).join(".config") - }); + let config_dir = dirs::config_dir().expect("Could not determine config directory"); let config_dir = config_dir.join("codspeed"); match config_name { From 64905a505db3fe8664370756fc3a325afda4c18a Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 16 Apr 2026 10:57:09 +0200 Subject: [PATCH 12/13] feat: scope unix specific code to allow windows build --- src/executor/helpers/env.rs | 29 +++++---- src/executor/helpers/mod.rs | 3 + src/executor/mod.rs | 53 +++++++++++----- src/executor/shared/mod.rs | 1 + src/executor/wall_time/executor.rs | 43 ++++--------- src/executor/wall_time/mod.rs | 2 + src/executor/wall_time/platform/default.rs | 42 +++++++++++++ src/executor/wall_time/platform/mod.rs | 9 +++ src/executor/wall_time/platform/unix.rs | 70 ++++++++++++++++++++++ 9 files changed, 195 insertions(+), 57 deletions(-) create mode 100644 src/executor/wall_time/platform/default.rs create mode 100644 src/executor/wall_time/platform/mod.rs create mode 100644 src/executor/wall_time/platform/unix.rs diff --git a/src/executor/helpers/env.rs b/src/executor/helpers/env.rs index 09b7c0f3..4ebfbabb 100644 --- a/src/executor/helpers/env.rs +++ b/src/executor/helpers/env.rs @@ -1,4 +1,5 @@ use crate::executor::ExecutorConfig; +#[cfg(unix)] use crate::executor::helpers::{introspected_golang, introspected_nodejs}; use crate::prelude::*; use crate::runner_mode::RunnerMode; @@ -82,17 +83,25 @@ pub fn build_path_env(enable_introspection: bool) -> Result { return Ok(path_env); } - let node_path = introspected_nodejs::setup() - .map_err(|e| anyhow!("failed to setup NodeJS introspection. {e}"))?; - let go_path = introspected_golang::setup() - .map_err(|e| anyhow!("failed to setup Go introspection. {e}"))?; + #[cfg(unix)] + { + let node_path = introspected_nodejs::setup() + .map_err(|e| anyhow!("failed to setup NodeJS introspection. {e}"))?; + let go_path = introspected_golang::setup() + .map_err(|e| anyhow!("failed to setup Go introspection. {e}"))?; - Ok(format!( - "{}:{}:{}", - node_path.to_string_lossy(), - go_path.to_string_lossy(), - path_env, - )) + Ok(format!( + "{}:{}:{}", + node_path.to_string_lossy(), + go_path.to_string_lossy(), + path_env, + )) + } + #[cfg(not(unix))] + { + // Language introspection is not yet supported on non-Unix platforms + unimplemented!(); + } } pub fn is_codspeed_debug_enabled() -> bool { diff --git a/src/executor/helpers/mod.rs b/src/executor/helpers/mod.rs index 3e433c7c..3c06f768 100644 --- a/src/executor/helpers/mod.rs +++ b/src/executor/helpers/mod.rs @@ -3,8 +3,11 @@ pub mod command; pub mod detect_executable; pub mod env; pub mod get_bench_command; +#[cfg(unix)] pub mod harvest_perf_maps_for_pids; +#[cfg(unix)] pub mod introspected_golang; +#[cfg(unix)] pub mod introspected_nodejs; pub mod profile_folder; pub mod run_command_with_log_pipe; diff --git a/src/executor/mod.rs b/src/executor/mod.rs index 353a1669..7ff2076b 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -4,11 +4,13 @@ pub mod config; mod execution_context; mod helpers; mod interfaces; +#[cfg(unix)] mod memory; pub mod orchestrator; mod shared; #[cfg(test)] mod tests; +#[cfg(unix)] mod valgrind; mod wall_time; @@ -22,9 +24,7 @@ pub use execution_context::ExecutionContext; pub use interfaces::ExecutorName; pub use orchestrator::Orchestrator; -use memory::executor::MemoryExecutor; use std::path::Path; -use valgrind::executor::ValgrindExecutor; use wall_time::executor::WallTimeExecutor; impl Display for RunnerMode { @@ -41,23 +41,48 @@ impl Display for RunnerMode { pub const EXECUTOR_TARGET: &str = "executor"; -pub fn get_executor_from_mode(mode: &RunnerMode) -> Box { - match mode { - #[allow(deprecated)] - RunnerMode::Instrumentation | RunnerMode::Simulation => Box::new(ValgrindExecutor), - RunnerMode::Walltime => Box::new(WallTimeExecutor::new()), - RunnerMode::Memory => Box::new(MemoryExecutor), +#[cfg(unix)] +mod platform { + use super::*; + use memory::executor::MemoryExecutor; + use valgrind::executor::ValgrindExecutor; + + pub fn get_executor_from_mode(mode: &RunnerMode) -> Box { + match mode { + #[allow(deprecated)] + RunnerMode::Instrumentation | RunnerMode::Simulation => Box::new(ValgrindExecutor), + RunnerMode::Walltime => Box::new(WallTimeExecutor::new()), + RunnerMode::Memory => Box::new(MemoryExecutor), + } + } + + pub fn get_all_executors() -> Vec> { + vec![ + Box::new(ValgrindExecutor), + Box::new(WallTimeExecutor::new()), + Box::new(MemoryExecutor), + ] } } -pub fn get_all_executors() -> Vec> { - vec![ - Box::new(ValgrindExecutor), - Box::new(WallTimeExecutor::new()), - Box::new(MemoryExecutor), - ] +#[cfg(not(unix))] +mod platform { + use super::*; + + pub fn get_executor_from_mode(mode: &RunnerMode) -> Box { + match mode { + RunnerMode::Walltime => Box::new(WallTimeExecutor::new()), + _ => panic!("{mode} mode is not supported on this platform"), + } + } + + pub fn get_all_executors() -> Vec> { + vec![Box::new(WallTimeExecutor::new())] + } } +pub use platform::{get_all_executors, get_executor_from_mode}; + /// Installation status of a tool required by an executor. pub struct ToolStatus { pub tool_name: String, diff --git a/src/executor/shared/mod.rs b/src/executor/shared/mod.rs index 2badf406..9e1bab42 100644 --- a/src/executor/shared/mod.rs +++ b/src/executor/shared/mod.rs @@ -1 +1,2 @@ +#[cfg(unix)] pub mod fifo; diff --git a/src/executor/wall_time/executor.rs b/src/executor/wall_time/executor.rs index 4da11899..66f5e6ff 100644 --- a/src/executor/wall_time/executor.rs +++ b/src/executor/wall_time/executor.rs @@ -1,15 +1,13 @@ use super::helpers::validate_walltime_results; use super::isolation::wrap_with_isolation; -use super::perf::PerfRunner; +use super::platform::WalltimePlatform; use crate::executor::Executor; use crate::executor::ExecutorConfig; use crate::executor::ToolStatus; use crate::executor::helpers::command::CommandBuilder; use crate::executor::helpers::env::{build_path_env, get_base_injected_env}; use crate::executor::helpers::get_bench_command::get_bench_command; -use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; use crate::executor::helpers::run_with_env::wrap_with_env; -use crate::executor::helpers::run_with_sudo::wrap_with_sudo; use crate::executor::{ExecutionContext, ExecutorName, ExecutorSupport}; use crate::instruments::mongo_tracer::MongoTracer; use crate::prelude::*; @@ -72,13 +70,13 @@ impl Drop for HookScriptsGuard { } pub struct WallTimeExecutor { - perf: Option, + platform: WalltimePlatform, } impl WallTimeExecutor { pub fn new() -> Self { Self { - perf: cfg!(target_os = "linux").then(PerfRunner::new), + platform: WalltimePlatform::new(), } } @@ -122,9 +120,7 @@ impl Executor for WallTimeExecutor { } fn tool_status(&self) -> Option { - self.perf - .as_ref() - .map(|_| super::perf::setup::get_perf_status()) + self.platform.tool_status() } fn support_level(&self, system_info: &SystemInfo) -> ExecutorSupport { @@ -136,11 +132,7 @@ impl Executor for WallTimeExecutor { } async fn setup(&self, system_info: &SystemInfo, setup_cache_dir: Option<&Path>) -> Result<()> { - if self.perf.is_some() { - return PerfRunner::setup_environment(system_info, setup_cache_dir).await; - } - - Ok(()) + self.platform.setup(system_info, setup_cache_dir).await } async fn run( @@ -153,20 +145,10 @@ impl Executor for WallTimeExecutor { let (_env_file, _script_file, cmd_builder) = WallTimeExecutor::walltime_bench_cmd(&execution_context.config, execution_context)?; - if let Some(perf) = &self.perf - && execution_context.config.enable_perf - { - perf.run( - cmd_builder, - &execution_context.config, - &execution_context.profile_folder, - ) + + self.platform + .run_bench_cmd(cmd_builder, execution_context) .await - } else { - let cmd = wrap_with_sudo(cmd_builder)?.build(); - debug!("cmd: {cmd:?}"); - run_command_with_log_pipe(cmd).await - } }; let status = status.map_err(|e| anyhow!("failed to execute the benchmark process. {e}"))?; @@ -182,12 +164,7 @@ impl Executor for WallTimeExecutor { async fn teardown(&self, execution_context: &ExecutionContext) -> Result<()> { debug!("Copying files to the profile folder"); - if let Some(perf) = &self.perf - && execution_context.config.enable_perf - { - perf.save_files_to(&execution_context.profile_folder) - .await?; - } + self.platform.teardown(execution_context).await?; validate_walltime_results( &execution_context.profile_folder, @@ -198,7 +175,7 @@ impl Executor for WallTimeExecutor { } } -#[cfg(test)] +#[cfg(all(test, unix))] mod tests { use tempfile::NamedTempFile; diff --git a/src/executor/wall_time/mod.rs b/src/executor/wall_time/mod.rs index ecd1ce79..ddaf7849 100644 --- a/src/executor/wall_time/mod.rs +++ b/src/executor/wall_time/mod.rs @@ -1,4 +1,6 @@ pub mod executor; pub mod helpers; pub mod isolation; +#[cfg(unix)] pub mod perf; +mod platform; diff --git a/src/executor/wall_time/platform/default.rs b/src/executor/wall_time/platform/default.rs new file mode 100644 index 00000000..0e630216 --- /dev/null +++ b/src/executor/wall_time/platform/default.rs @@ -0,0 +1,42 @@ +use crate::executor::ExecutionContext; +use crate::executor::ToolStatus; +use crate::executor::helpers::command::CommandBuilder; +use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; +use crate::prelude::*; +use crate::system::SystemInfo; +use std::path::Path; +use std::process::ExitStatus; + +pub struct WalltimePlatform; + +impl WalltimePlatform { + pub fn new() -> Self { + Self + } + + pub fn tool_status(&self) -> Option { + None + } + + pub async fn setup( + &self, + _system_info: &SystemInfo, + _setup_cache_dir: Option<&Path>, + ) -> Result<()> { + Ok(()) + } + + pub async fn run_bench_cmd( + &self, + cmd_builder: CommandBuilder, + _execution_context: &ExecutionContext, + ) -> Result { + let cmd = cmd_builder.build(); + debug!("cmd: {cmd:?}"); + run_command_with_log_pipe(cmd).await + } + + pub async fn teardown(&self, _execution_context: &ExecutionContext) -> Result<()> { + Ok(()) + } +} diff --git a/src/executor/wall_time/platform/mod.rs b/src/executor/wall_time/platform/mod.rs new file mode 100644 index 00000000..13f1b594 --- /dev/null +++ b/src/executor/wall_time/platform/mod.rs @@ -0,0 +1,9 @@ +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::*; + +#[cfg(not(unix))] +mod default; +#[cfg(not(unix))] +pub use default::*; diff --git a/src/executor/wall_time/platform/unix.rs b/src/executor/wall_time/platform/unix.rs new file mode 100644 index 00000000..b45dcbb4 --- /dev/null +++ b/src/executor/wall_time/platform/unix.rs @@ -0,0 +1,70 @@ +use super::super::perf::PerfRunner; +use crate::executor::ExecutionContext; +use crate::executor::ToolStatus; +use crate::executor::helpers::command::CommandBuilder; +use crate::executor::helpers::run_command_with_log_pipe::run_command_with_log_pipe; +use crate::executor::helpers::run_with_sudo::wrap_with_sudo; +use crate::prelude::*; +use crate::system::SystemInfo; +use std::path::Path; +use std::process::ExitStatus; + +pub struct WalltimePlatform { + perf: Option, +} + +impl WalltimePlatform { + pub fn new() -> Self { + Self { + perf: cfg!(target_os = "linux").then(PerfRunner::new), + } + } + + pub fn tool_status(&self) -> Option { + self.perf + .as_ref() + .map(|_| super::super::perf::setup::get_perf_status()) + } + + pub async fn setup( + &self, + system_info: &SystemInfo, + setup_cache_dir: Option<&Path>, + ) -> Result<()> { + if self.perf.is_some() { + return PerfRunner::setup_environment(system_info, setup_cache_dir).await; + } + Ok(()) + } + + pub async fn run_bench_cmd( + &self, + cmd_builder: CommandBuilder, + execution_context: &ExecutionContext, + ) -> Result { + if let Some(perf) = &self.perf + && execution_context.config.enable_perf + { + perf.run( + cmd_builder, + &execution_context.config, + &execution_context.profile_folder, + ) + .await + } else { + let cmd = wrap_with_sudo(cmd_builder)?.build(); + debug!("cmd: {cmd:?}"); + run_command_with_log_pipe(cmd).await + } + } + + pub async fn teardown(&self, execution_context: &ExecutionContext) -> Result<()> { + if let Some(perf) = &self.perf + && execution_context.config.enable_perf + { + perf.save_files_to(&execution_context.profile_folder) + .await?; + } + Ok(()) + } +} From a2f1fbb4d81b5ef07292cc85446e9a3beb0e6904 Mon Sep 17 00:00:00 2001 From: Guillaume Lagrange Date: Thu, 16 Apr 2026 10:58:45 +0200 Subject: [PATCH 13/13] ci: check that the runner builds on windows Note: the runner is still absolutely not functional, we just have this test to avoid adding more debt passively before making it functional. --- .github/workflows/ci.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27f62812..847e371e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,21 @@ jobs: - name: Clean up run: sudo chown -R $USER:$USER . ~/.cargo + windows-build: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + submodules: true + - name: "Install rust-toolchain.toml" + run: rustup toolchain install + - name: Add Windows GNU target + run: rustup target add x86_64-pc-windows-gnu + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + - name: Check codspeed-runner builds on Windows + # Instrument hooks fails to build with msvc target, so we use the gnu target for this check for now + run: cargo check -p codspeed-runner --target x86_64-pc-windows-gnu + benchmarks: runs-on: ubuntu-latest steps: