Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/cli/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ 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: args.shared.profiler_run_args.walltime.resolve_unwinding(),
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(),
Expand Down
7 changes: 4 additions & 3 deletions src/cli/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -77,7 +77,8 @@ impl RunArgs {
profiler_run_args: ProfilerRunArgs {
enable_profiler: false,
enable_perf: None,
perf: PerfRunArgs {
walltime: WalltimeRunArgs {
walltime_unwinding_mode: None,
perf_unwinding_mode: None,
},
},
Expand Down Expand Up @@ -127,7 +128,7 @@ fn build_orchestrator_config(
targets,
modes,
instruments,
perf_unwinding_mode: args.shared.profiler_run_args.perf.perf_unwinding_mode,
walltime_unwinding: args.shared.profiler_run_args.walltime.resolve_unwinding(),
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(),
Expand Down
71 changes: 53 additions & 18 deletions src/cli/shared.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use super::experimental::ExperimentalArgs;
use crate::VERSION;
use crate::executor::config::{SimulationTool, WalltimeProfiler};
use crate::executor::config::{SimulationTool, WalltimeProfiler, WalltimeUnwinding};
use crate::prelude::*;
use crate::run_environment::interfaces::RepositoryProvider;
use crate::runner_mode::{RunnerMode, load_shell_session_mode};
use clap::Args;
use clap::ValueEnum;
use std::path::PathBuf;

pub(crate) fn show_banner() {
Expand Down Expand Up @@ -205,17 +204,6 @@ impl ExecAndRunSharedArgs {
}
}

#[derive(Debug, Copy, Clone, PartialEq, ValueEnum, Default)]
pub enum UnwindingMode {
/// Use the frame pointer for unwinding. Requires the binary to be compiled with frame pointers enabled.
#[clap(name = "fp")]
FramePointer,

/// Use DWARF unwinding. This does not require any special compilation flags and is enabled by default.
#[default]
Dwarf,
}

#[derive(Args, Debug, Clone)]
pub struct ProfilerRunArgs {
/// Enable a profiler to collect granular performance data.
Expand All @@ -228,14 +216,37 @@ pub struct ProfilerRunArgs {
pub enable_perf: Option<bool>,

#[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 perf_unwinding_mode: Option<UnwindingMode>,
pub struct WalltimeRunArgs {
/// Call stack unwinding for the walltime profiler: `fp`, `dwarf`, or `dwarf,<bytes>`.
/// `fp` requires binaries compiled with frame pointers. `<bytes>` sets the user stack size
/// copied per sample; larger values produce deeper call stacks but bigger profiles.
/// Inferred from the benchmark command when unset.
#[arg(long, env = "CODSPEED_WALLTIME_UNWINDING_MODE", value_parser = parse_walltime_unwinding)]
pub walltime_unwinding_mode: Option<WalltimeUnwinding>,

/// Deprecated alias for --walltime-unwinding-mode / CODSPEED_WALLTIME_UNWINDING_MODE.
#[arg(long, env = "CODSPEED_PERF_UNWINDING_MODE", hide = true, value_parser = parse_walltime_unwinding)]
pub perf_unwinding_mode: Option<WalltimeUnwinding>,
}

impl WalltimeRunArgs {
/// Resolves the effective unwinding, 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(&self) -> Option<WalltimeUnwinding> {
let Some(legacy) = self.perf_unwinding_mode else {
return self.walltime_unwinding_mode;
};
// Configuration is resolved before the orchestrator installs the logger.
eprintln!(
"Warning: 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 {
Expand All @@ -256,3 +267,27 @@ impl ProfilerRunArgs {
fn parse_version(s: &str) -> Result<semver::Version, String> {
semver::Version::parse(s).map_err(|e| format!("Invalid semantic version: {e}"))
}

const FRAME_POINTER_UNWINDING: &str = "fp";
const DWARF_UNWINDING: &str = "dwarf";

/// Parses `fp`, `dwarf`, or `dwarf,<bytes>`.
fn parse_walltime_unwinding(value: &str) -> Result<WalltimeUnwinding, String> {
let (mode, stack_size) = match value.split_once(',') {
Some((mode, size)) => {
let size = size
.parse()
.map_err(|e| format!("Invalid stack size `{size}`: {e}"))?;
(mode, Some(size))
}
None => (value, None),
};
match (mode, stack_size) {
(DWARF_UNWINDING, stack_size) => Ok(WalltimeUnwinding::Dwarf { stack_size }),
(FRAME_POINTER_UNWINDING, None) => Ok(WalltimeUnwinding::FramePointer),
(FRAME_POINTER_UNWINDING, Some(_)) => {
Err("A stack size only applies to `dwarf` unwinding".into())
}
_ => Err("Expected `fp`, `dwarf`, or `dwarf,<bytes>`".into()),
}
}
23 changes: 16 additions & 7 deletions src/executor/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::cli::UnwindingMode;
use crate::instruments::Instruments;
use crate::prelude::*;
use crate::run_environment::RepositoryProvider;
Expand Down Expand Up @@ -51,6 +50,16 @@ pub enum WalltimeProfiler {
Samply,
}

/// Explicit walltime unwinding settings. An absent setting preserves backend inference.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalltimeUnwinding {
FramePointer,
/// An absent size preserves the profiler's default stack copy size.
Dwarf {
stack_size: Option<u32>,
},
}

/// Run-level configuration owned by the orchestrator.
///
/// Holds all parameters that are constant across benchmark targets within a run,
Expand All @@ -68,8 +77,8 @@ pub struct OrchestratorConfig {
pub modes: Vec<RunnerMode>,
pub instruments: Instruments,
pub enable_profiler: bool,
/// Stack unwinding mode for perf (if enabled)
pub perf_unwinding_mode: Option<UnwindingMode>,
/// Explicit unwinding settings; None preserves the profiler's automatic selection.
pub walltime_unwinding: Option<WalltimeUnwinding>,
/// Profiler override for walltime mode (if None, selected based on the platform)
pub walltime_profiler: Option<WalltimeProfiler>,

Expand Down Expand Up @@ -120,8 +129,8 @@ pub struct ExecutorConfig {

pub instruments: Instruments,
pub enable_profiler: bool,
/// Stack unwinding mode for perf (if enabled)
pub perf_unwinding_mode: Option<UnwindingMode>,
/// Explicit unwinding settings; None preserves the profiler's automatic selection.
pub walltime_unwinding: Option<WalltimeUnwinding>,

pub simulation_tool: SimulationTool,

Expand Down Expand Up @@ -218,7 +227,7 @@ impl OrchestratorConfig {
command,
instruments: self.instruments.clone(),
enable_profiler: self.enable_profiler,
perf_unwinding_mode: self.perf_unwinding_mode,
walltime_unwinding: self.walltime_unwinding,
simulation_tool: self.simulation_tool,
skip_run: self.skip_run,
skip_setup: self.skip_setup,
Expand Down Expand Up @@ -251,7 +260,7 @@ impl OrchestratorConfig {
}],
modes: vec![RunnerMode::Simulation],
instruments: Instruments::test(),
perf_unwinding_mode: None,
walltime_unwinding: None,
walltime_profiler: None,
enable_profiler: false,
simulation_tool: SimulationTool::default(),
Expand Down
4 changes: 2 additions & 2 deletions src/executor/helpers/introspected_golang/go.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 62 additions & 26 deletions src/executor/wall_time/profiler/perf/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![cfg_attr(not(unix), allow(dead_code, unused_mut))]

use crate::cli::UnwindingMode;
use crate::executor::ExecutorConfig;
use crate::executor::ToolStatus;
use crate::executor::config::WalltimeUnwinding;
use crate::executor::helpers::command::CommandBuilder;
use crate::executor::helpers::detect_executable::command_has_executable;
use crate::executor::helpers::env::is_codspeed_debug_enabled;
Expand Down Expand Up @@ -38,6 +38,32 @@ pub mod fifo;
pub mod perf_executable;

const PERF_PIPEDATA_FILE_NAME: &str = "perf.pipedata";
const DEFAULT_STACK_SIZE: u32 = 8 * 1024;
const PYTHON_STACK_SIZE: u32 = 32 * 1024;

fn resolve_unwinding(command: &str, explicit: Option<WalltimeUnwinding>) -> WalltimeUnwinding {
if let Some(unwinding) = explicit {
return unwinding;
}
if command_has_executable(
command,
&["gradle", "gradlew", "java", "maven", "mvn", "mvnw"],
) {
return WalltimeUnwinding::FramePointer;
}
if command_has_executable(command, &["cargo"]) {
return WalltimeUnwinding::Dwarf { stack_size: None };
}
if command_has_executable(command, &["go"]) {
return WalltimeUnwinding::FramePointer;
}
if command_has_executable(command, &["pytest", "uv", "python", "python3"]) {
return WalltimeUnwinding::Dwarf {
stack_size: Some(PYTHON_STACK_SIZE),
};
}
WalltimeUnwinding::Dwarf { stack_size: None }
}

pub struct PerfProfiler {
/// Set by [`Profiler::wrap_command`]; used by the FIFO hooks to control event
Expand Down Expand Up @@ -89,35 +115,16 @@ impl Profiler for PerfProfiler {
let perf_fifo = PerfFifo::new()?;
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 {
(mode, None)
} else if command_has_executable(
&config.command,
&["gradle", "gradlew", "java", "maven", "mvn", "mvnw"],
) {
// In Java, we must use FP unwinding otherwise we'll have broken call stacks.
(UnwindingMode::FramePointer, None)
} else if command_has_executable(&config.command, &["cargo"]) {
(UnwindingMode::Dwarf, None)
} else if command_has_executable(&config.command, &["go"]) {
(UnwindingMode::FramePointer, None)
} else if command_has_executable(&config.command, &["pytest", "uv", "python", "python3"]) {
// Note that the higher the stack size, the larger the file, although it is mitigated
// by zstd compression
(UnwindingMode::Dwarf, Some(32 * 1024))
} else {
// Default to dwarf unwinding since it works well with most binaries.
debug!("No call graph mode detected, defaulting to dwarf");
(UnwindingMode::Dwarf, None)
};
let unwinding = resolve_unwinding(&config.command, config.walltime_unwinding);

let cg_mode = match cg_mode {
UnwindingMode::FramePointer => {
let cg_mode = match unwinding {
WalltimeUnwinding::FramePointer => {
suppress_go_perf_unwinding_warning();
"fp"
}
UnwindingMode::Dwarf => &format!("dwarf,{}", stack_size.unwrap_or(8192)),
WalltimeUnwinding::Dwarf { stack_size } => {
&format!("dwarf,{}", stack_size.unwrap_or(DEFAULT_STACK_SIZE))
}
};
debug!("Using call graph mode: {cg_mode:?}");

Expand Down Expand Up @@ -310,3 +317,32 @@ impl BenchmarkData<'_> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn explicit_unwinding_overrides_language_inference() {
let explicit = WalltimeUnwinding::Dwarf {
stack_size: Some(16 * 1024),
};
for (command, inferred) in [
(
"python bench.py",
WalltimeUnwinding::Dwarf {
stack_size: Some(PYTHON_STACK_SIZE),
},
),
("cargo bench", WalltimeUnwinding::Dwarf { stack_size: None }),
("go test -bench=.", WalltimeUnwinding::FramePointer),
] {
assert_eq!(resolve_unwinding(command, None), inferred);
assert_eq!(resolve_unwinding(command, Some(explicit)), explicit);
}
assert_eq!(
resolve_unwinding("python bench.py", Some(WalltimeUnwinding::FramePointer)),
WalltimeUnwinding::FramePointer,
);
}
}
Loading
Loading