From 3e2212c95e2d7a57bf7ce52e5c4332d457e33798 Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 13 Oct 2026 17:38:08 +0200 Subject: [PATCH 1/2] feat(cli): add walltime unwinding mode and stack sample size options Add --walltime-unwinding-mode (CODSPEED_WALLTIME_UNWINDING_MODE) and --walltime-stack-sample-size (CODSPEED_WALLTIME_STACK_SAMPLE_SIZE) so the call-stack collection can be tuned for both walltime profilers, perf and samply. --perf-unwinding-mode / CODSPEED_PERF_UNWINDING_MODE stays as a hidden, deprecated alias so existing setups keep working; the new option wins when both are set. perf: the explicit stack size overrides the size inferred from the command (default 8192 bytes). samply: on Linux, forward the options as --unwinding-mode and --stack-size. --- src/cli/exec/mod.rs | 11 ++++- src/cli/run/mod.rs | 17 +++++-- src/cli/shared.rs | 32 +++++++++++-- src/executor/config.rs | 18 +++++--- .../helpers/introspected_golang/go.sh | 4 +- src/executor/wall_time/profiler/perf/mod.rs | 10 +++- src/executor/wall_time/profiler/samply/mod.rs | 46 +++++++++++++------ 7 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/cli/exec/mod.rs b/src/cli/exec/mod.rs index 2302f4839..52ddfa143 100644 --- a/src/cli/exec/mod.rs +++ b/src/cli/exec/mod.rs @@ -80,7 +80,16 @@ fn build_orchestrator_config( targets: vec![target], modes, instruments: Instruments { mongodb: None }, // exec doesn't support MongoDB - perf_unwinding_mode: args.shared.profiler_run_args.perf.perf_unwinding_mode, + walltime_unwinding_mode: args + .shared + .profiler_run_args + .walltime + .resolve_unwinding_mode(), + walltime_stack_sample_size: args + .shared + .profiler_run_args + .walltime + .walltime_stack_sample_size, enable_profiler: args.shared.profiler_run_args.resolve_enable_profiler(), walltime_profiler: args.shared.walltime_profiler, simulation_tool: args.shared.simulation_tool.unwrap_or_default(), diff --git a/src/cli/run/mod.rs b/src/cli/run/mod.rs index e6750c1eb..c97ac08a5 100644 --- a/src/cli/run/mod.rs +++ b/src/cli/run/mod.rs @@ -48,7 +48,7 @@ impl RunArgs { /// Constructs a new `RunArgs` with default values for testing purposes pub fn test() -> Self { use super::experimental::ExperimentalArgs; - use super::{PerfRunArgs, ProfilerRunArgs}; + use super::{ProfilerRunArgs, WalltimeRunArgs}; use crate::RunnerMode; Self { @@ -77,8 +77,10 @@ impl RunArgs { profiler_run_args: ProfilerRunArgs { enable_profiler: false, enable_perf: None, - perf: PerfRunArgs { + walltime: WalltimeRunArgs { + walltime_unwinding_mode: None, perf_unwinding_mode: None, + walltime_stack_sample_size: None, }, }, experimental: ExperimentalArgs { @@ -127,7 +129,16 @@ fn build_orchestrator_config( targets, modes, instruments, - perf_unwinding_mode: args.shared.profiler_run_args.perf.perf_unwinding_mode, + walltime_unwinding_mode: args + .shared + .profiler_run_args + .walltime + .resolve_unwinding_mode(), + walltime_stack_sample_size: args + .shared + .profiler_run_args + .walltime + .walltime_stack_sample_size, enable_profiler: args.shared.profiler_run_args.resolve_enable_profiler(), walltime_profiler: args.shared.walltime_profiler, simulation_tool: args.shared.simulation_tool.unwrap_or_default(), diff --git a/src/cli/shared.rs b/src/cli/shared.rs index 6ea9acd17..fc3058265 100644 --- a/src/cli/shared.rs +++ b/src/cli/shared.rs @@ -228,14 +228,38 @@ pub struct ProfilerRunArgs { pub enable_perf: Option, #[command(flatten)] - pub perf: PerfRunArgs, + pub walltime: WalltimeRunArgs, } #[derive(Args, Debug, Clone)] -pub struct PerfRunArgs { - /// The unwinding mode that should be used with perf to collect the call stack. - #[arg(long, env = "CODSPEED_PERF_UNWINDING_MODE")] +pub struct WalltimeRunArgs { + /// The unwinding mode that the walltime profiler should use to collect the call stack. + #[arg(long, env = "CODSPEED_WALLTIME_UNWINDING_MODE")] + pub walltime_unwinding_mode: Option, + + /// Deprecated alias for --walltime-unwinding-mode / CODSPEED_WALLTIME_UNWINDING_MODE. + #[arg(long, env = "CODSPEED_PERF_UNWINDING_MODE", hide = true)] pub perf_unwinding_mode: Option, + + /// Size in bytes of the user stack copied with each sample for DWARF unwinding. + /// Larger values produce deeper call stacks but bigger profiles. + #[arg(long, env = "CODSPEED_WALLTIME_STACK_SAMPLE_SIZE")] + pub walltime_stack_sample_size: Option, +} + +impl WalltimeRunArgs { + /// Resolves the effective unwinding mode, honoring the deprecated + /// `--perf-unwinding-mode` / `CODSPEED_PERF_UNWINDING_MODE` flag with a warning. + /// The new flag takes precedence when both are set. + pub fn resolve_unwinding_mode(&self) -> Option { + let Some(legacy) = self.perf_unwinding_mode else { + return self.walltime_unwinding_mode; + }; + log::warn!( + "CODSPEED_PERF_UNWINDING_MODE / --perf-unwinding-mode is deprecated; use CODSPEED_WALLTIME_UNWINDING_MODE / --walltime-unwinding-mode instead." + ); + Some(self.walltime_unwinding_mode.unwrap_or(legacy)) + } } impl ProfilerRunArgs { diff --git a/src/executor/config.rs b/src/executor/config.rs index f3ec640c8..6708a001c 100644 --- a/src/executor/config.rs +++ b/src/executor/config.rs @@ -68,8 +68,10 @@ pub struct OrchestratorConfig { pub modes: Vec, pub instruments: Instruments, pub enable_profiler: bool, - /// Stack unwinding mode for perf (if enabled) - pub perf_unwinding_mode: Option, + /// Stack unwinding mode for the walltime profiler (if None, inferred from the command) + pub walltime_unwinding_mode: Option, + /// User stack size in bytes copied per sample for DWARF unwinding (if None, profiler default) + pub walltime_stack_sample_size: Option, /// Profiler override for walltime mode (if None, selected based on the platform) pub walltime_profiler: Option, @@ -120,8 +122,10 @@ pub struct ExecutorConfig { pub instruments: Instruments, pub enable_profiler: bool, - /// Stack unwinding mode for perf (if enabled) - pub perf_unwinding_mode: Option, + /// Stack unwinding mode for the walltime profiler (if None, inferred from the command) + pub walltime_unwinding_mode: Option, + /// User stack size in bytes copied per sample for DWARF unwinding (if None, profiler default) + pub walltime_stack_sample_size: Option, pub simulation_tool: SimulationTool, @@ -218,7 +222,8 @@ impl OrchestratorConfig { command, instruments: self.instruments.clone(), enable_profiler: self.enable_profiler, - perf_unwinding_mode: self.perf_unwinding_mode, + walltime_unwinding_mode: self.walltime_unwinding_mode, + walltime_stack_sample_size: self.walltime_stack_sample_size, simulation_tool: self.simulation_tool, skip_run: self.skip_run, skip_setup: self.skip_setup, @@ -251,7 +256,8 @@ impl OrchestratorConfig { }], modes: vec![RunnerMode::Simulation], instruments: Instruments::test(), - perf_unwinding_mode: None, + walltime_unwinding_mode: None, + walltime_stack_sample_size: None, walltime_profiler: None, enable_profiler: false, simulation_tool: SimulationTool::default(), diff --git a/src/executor/helpers/introspected_golang/go.sh b/src/executor/helpers/introspected_golang/go.sh index f3e08dd39..a09866f5f 100755 --- a/src/executor/helpers/introspected_golang/go.sh +++ b/src/executor/helpers/introspected_golang/go.sh @@ -111,9 +111,9 @@ if [ $# -eq 0 ]; then exit $? fi -# On arm64, warn about setting CODSPEED_PERF_UNWINDING_MODE to "fp" for correct flamegraphs +# On arm64, warn about setting CODSPEED_WALLTIME_UNWINDING_MODE to "fp" for correct flamegraphs if [ "$(uname -m)" = "aarch64" ] && [ "${CODSPEED_GO_SUPPRESS_PERF_UNWINDING_MODE_WARNING:-}" != "true" ]; then - echo "::warning::Go profiling on arm64 require frame pointer unwinding. Set CODSPEED_PERF_UNWINDING_MODE=fp for better profiling." >&2 + echo "::warning::Go profiling on arm64 require frame pointer unwinding. Set CODSPEED_WALLTIME_UNWINDING_MODE=fp for better profiling." >&2 fi # Route command based on first argument diff --git a/src/executor/wall_time/profiler/perf/mod.rs b/src/executor/wall_time/profiler/perf/mod.rs index dbc0e464a..217e62bbe 100644 --- a/src/executor/wall_time/profiler/perf/mod.rs +++ b/src/executor/wall_time/profiler/perf/mod.rs @@ -90,7 +90,7 @@ impl Profiler for PerfProfiler { let perf_file_path = profile_folder.join(PERF_PIPEDATA_FILE_NAME); // Infer the unwinding mode from the benchmark cmd - let (cg_mode, stack_size) = if let Some(mode) = config.perf_unwinding_mode { + let (cg_mode, stack_size) = if let Some(mode) = config.walltime_unwinding_mode { (mode, None) } else if command_has_executable( &config.command, @@ -117,7 +117,13 @@ impl Profiler for PerfProfiler { suppress_go_perf_unwinding_warning(); "fp" } - UnwindingMode::Dwarf => &format!("dwarf,{}", stack_size.unwrap_or(8192)), + UnwindingMode::Dwarf => &format!( + "dwarf,{}", + config + .walltime_stack_sample_size + .or(stack_size) + .unwrap_or(8192) + ), }; debug!("Using call graph mode: {cg_mode:?}"); diff --git a/src/executor/wall_time/profiler/samply/mod.rs b/src/executor/wall_time/profiler/samply/mod.rs index 394449d17..3b8266a9a 100644 --- a/src/executor/wall_time/profiler/samply/mod.rs +++ b/src/executor/wall_time/profiler/samply/mod.rs @@ -1,6 +1,8 @@ //! Samply profiler integration. use crate::cli::InternalCommands; +#[cfg(target_os = "linux")] +use crate::cli::UnwindingMode; use crate::cli::samply::SamplyArgs; use crate::executor::ExecutorConfig; use crate::executor::helpers::command::CommandBuilder; @@ -81,7 +83,7 @@ impl Profiler for SamplyProfiler { async fn wrap_command( &mut self, mut cmd_builder: CommandBuilder, - _config: &ExecutorConfig, + config: &ExecutorConfig, profile_folder: &Path, requires_sudo: bool, ) -> anyhow::Result { @@ -89,20 +91,34 @@ impl Profiler for SamplyProfiler { // samply is bundled into this binary as the `samply` subcommand; // re-exec ourselves so we don't depend on a system install. - let samply_builder = InternalCommands::Samply(SamplyArgs { - args: vec![ - "record".into(), - "--presymbolicate".into(), - "--no-open".into(), - "--save-only".into(), - "--rate".into(), - SAMPLING_RATE_HZ.to_string().into(), - "-o".into(), - output_path.clone().into(), - "--".into(), - ], - }) - .get_command_builder()?; + let mut args: Vec = vec![ + "record".into(), + "--presymbolicate".into(), + "--no-open".into(), + "--save-only".into(), + "--rate".into(), + SAMPLING_RATE_HZ.to_string().into(), + "-o".into(), + output_path.clone().into(), + ]; + // Unwinding knobs are Linux-only in samply. + #[cfg(target_os = "linux")] + { + if let Some(mode) = config.walltime_unwinding_mode { + let mode = match mode { + UnwindingMode::FramePointer => "fp", + UnwindingMode::Dwarf => "dwarf", + }; + args.extend(["--unwinding-mode".into(), mode.into()]); + } + if let Some(size) = config.walltime_stack_sample_size { + args.extend(["--stack-size".into(), size.to_string().into()]); + } + } + #[cfg(not(target_os = "linux"))] + let _ = config; + args.push("--".into()); + let samply_builder = InternalCommands::Samply(SamplyArgs { args }).get_command_builder()?; cmd_builder.wrap_with(samply_builder); From 3b356c98b3443c5589070225e97301e03f39bdaf Mon Sep 17 00:00:00 2001 From: not-matthias Date: Tue, 13 Oct 2026 17:46:13 +0200 Subject: [PATCH 2/2] fix(samply): pass stack sample size as --unwinding-stack-size Match the option name added to samply-codspeed. --- src/executor/wall_time/profiler/samply/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/executor/wall_time/profiler/samply/mod.rs b/src/executor/wall_time/profiler/samply/mod.rs index 3b8266a9a..59c477fd6 100644 --- a/src/executor/wall_time/profiler/samply/mod.rs +++ b/src/executor/wall_time/profiler/samply/mod.rs @@ -112,7 +112,7 @@ impl Profiler for SamplyProfiler { args.extend(["--unwinding-mode".into(), mode.into()]); } if let Some(size) = config.walltime_stack_sample_size { - args.extend(["--stack-size".into(), size.to_string().into()]); + args.extend(["--unwinding-stack-size".into(), size.to_string().into()]); } } #[cfg(not(target_os = "linux"))]