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
1 change: 1 addition & 0 deletions src/cli/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ fn build_orchestrator_config(
exclude_allocations: args.shared.exclude_allocations,
simulation_track_subprocess: args.shared.simulation_track_subprocess,
memory_track_physical: args.shared.experimental.experimental_memory_track_physical,
disable_pythonmalloc: args.shared.experimental.experimental_disable_pythonmalloc,
})
}

Expand Down
12 changes: 12 additions & 0 deletions src/cli/experimental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ pub struct ExperimentalArgs {
)]
pub experimental_memory_track_physical: bool,

/// Do not set PYTHONMALLOC for simulation runs.
#[arg(
long,
default_value_t = false,
help_heading = "Experimental",
env = "CODSPEED_EXPERIMENTAL_DISABLE_PYTHONMALLOC"
)]
pub experimental_disable_pythonmalloc: bool,

/// Deprecated: cycle estimation is enabled by default and this flag has no effect.
#[arg(long, hide = true, env = "CODSPEED_EXPERIMENTAL_CYCLE_ESTIMATION")]
pub experimental_cycle_estimation: bool,
Expand All @@ -46,6 +55,9 @@ impl ExperimentalArgs {
if self.experimental_memory_track_physical {
flags.push("--experimental-memory-track-physical");
}
if self.experimental_disable_pythonmalloc {
flags.push("--experimental-disable-pythonmalloc");
}
flags
}

Expand Down
2 changes: 2 additions & 0 deletions src/cli/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl RunArgs {
experimental_cycle_estimation: false,
experimental_exclude_allocations: false,
experimental_memory_track_physical: false,
experimental_disable_pythonmalloc: false,
},
},
instruments: vec![],
Expand Down Expand Up @@ -137,6 +138,7 @@ fn build_orchestrator_config(
exclude_allocations: args.shared.exclude_allocations,
simulation_track_subprocess: args.shared.simulation_track_subprocess,
memory_track_physical: args.shared.experimental.experimental_memory_track_physical,
disable_pythonmalloc: args.shared.experimental.experimental_disable_pythonmalloc,
})
}

Expand Down
6 changes: 6 additions & 0 deletions src/executor/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ pub struct OrchestratorConfig {
pub simulation_track_subprocess: bool,
/// Enable physical (resident) memory tracking in memory mode.
pub memory_track_physical: bool,
/// Do not set PYTHONMALLOC for simulation runs.
pub disable_pythonmalloc: bool,
}

/// Per-execution configuration passed to executors.
Expand Down Expand Up @@ -145,6 +147,8 @@ pub struct ExecutorConfig {
/// Only read by the memory executor, which is Linux-only.
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub memory_track_physical: bool,
/// Do not set PYTHONMALLOC for simulation runs.
pub disable_pythonmalloc: bool,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -218,6 +222,7 @@ impl OrchestratorConfig {
exclude_allocations: self.exclude_allocations,
simulation_track_subprocess: self.simulation_track_subprocess,
memory_track_physical: self.memory_track_physical,
disable_pythonmalloc: self.disable_pythonmalloc,
}
}
}
Expand Down Expand Up @@ -254,6 +259,7 @@ impl OrchestratorConfig {
exclude_allocations: false,
simulation_track_subprocess: false,
memory_track_physical: false,
disable_pythonmalloc: false,
}
}
}
Expand Down
114 changes: 98 additions & 16 deletions src/executor/valgrind/helpers/python.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,103 @@
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;

/// Checks if the Python interpreter supports free-threaded mode.
/// Returns true if Python is free-threaded (GIL disabled), false otherwise.
pub fn is_free_threaded_python() -> bool {
// Use sysconfig.get_config_var("Py_GIL_DISABLED") as recommended by Python docs at https://docs.python.org/3/howto/free-threading-python.html#identifying-free-threaded-python
let output = Command::new("python")
.args([
"-c",
"import sysconfig; print(sysconfig.get_config_var('Py_GIL_DISABLED') or 0)",
])
.output();

match output {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.trim() == "1"
use crate::prelude::*;

/// Free-threaded builds carry the `t` ABI flag. `sys.abiflags` needs no `sysconfig`
/// import, which fails when `_PYTHON_SYSCONFIGDATA_NAME` names a module the
/// interpreter does not ship.
const GIL_DISABLED_PROBE: &str = "import sys; print(int('t' in sys.abiflags))";

/// Returns true if any Python interpreter the benchmark command could resolve to is
/// free-threaded: PATH `python`/`python3`, `$VIRTUAL_ENV`, the working directory's
/// `.venv`, and the interpreter `uv` selects for `$UV_PYTHON`.
///
/// `uv` downloads a requested interpreter lazily, so when `$UV_PYTHON` is not installed
/// yet the request string itself decides.
pub fn is_free_threaded_python(working_directory: Option<&Path>) -> bool {
let cwd = working_directory.unwrap_or(Path::new("."));

let mut candidates: Vec<PathBuf> = vec![PathBuf::from("python"), PathBuf::from("python3")];
if let Some(venv) = std::env::var_os("VIRTUAL_ENV") {
candidates.push(Path::new(&venv).join("bin/python"));
}
candidates.push(cwd.join(".venv/bin/python"));
Comment on lines +20 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Benchmark commands can invoke a free-threaded interpreter through a name or path outside this fixed candidate list, such as python3.13t script.py or a pytest console script from a custom-named virtual environment. Those commands are accepted as arbitrary shell commands, but detection checks only python, python3, two conventional virtual-environment paths, and UV_PYTHON. The detector therefore returns false, measure sets PYTHONMALLOC=malloc, and the free-threaded interpreter refuses to start. Resolve or probe the interpreter used by the configured command rather than assuming this list covers every possible interpreter.

Knowledge Base Used: Valgrind measurement

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/executor/valgrind/helpers/python.rs
Line: 20-25

Comment:
Benchmark commands can invoke a free-threaded interpreter through a name or path outside this fixed candidate list, such as `python3.13t script.py` or a `pytest` console script from a custom-named virtual environment. Those commands are accepted as arbitrary shell commands, but detection checks only `python`, `python3`, two conventional virtual-environment paths, and `UV_PYTHON`. The detector therefore returns false, `measure` sets `PYTHONMALLOC=malloc`, and the free-threaded interpreter refuses to start. Resolve or probe the interpreter used by the configured command rather than assuming this list covers every possible interpreter.

**Knowledge Base Used:** [Valgrind measurement](https://app.greptile.com/codspeed/-/custom-context/knowledge-base/codspeedhq/codspeed/-/docs/valgrind-measurement.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

if let Some(request) = std::env::var_os("UV_PYTHON") {
match uv_python_find(&request, cwd) {
Some(python) => candidates.push(python),
None if is_free_threaded_request(&request.to_string_lossy()) => {
debug!("free-threaded Python requested via UV_PYTHON={request:?}");
return true;
}
None => {}
}
_ => false, // If Python is not available or command fails, assume not free-threaded
}

candidates.into_iter().any(|python| {
let free_threaded = is_gil_disabled(&python);
if free_threaded {
debug!("detected free-threaded Python: {}", python.display());
}
free_threaded
})
}

fn is_gil_disabled(python: &Path) -> bool {
let Ok(output) = Command::new(python)
.args(["-c", GIL_DISABLED_PROBE])
.output()
else {
return false;
};
output.status.success() && String::from_utf8_lossy(&output.stdout).trim() == "1"
}

fn uv_python_find(request: &OsStr, cwd: &Path) -> Option<PathBuf> {
let output = Command::new("uv")
.args(["python", "find"])
.arg(request)
.current_dir(cwd)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let path = String::from_utf8_lossy(&output.stdout).trim().to_owned();
(!path.is_empty()).then(|| PathBuf::from(path))
}

/// Matches a full key variant (`cpython-3.13.0+freethreaded-linux-x86_64-gnu`) or a
/// version with the `t` suffix (`3.13t`, `cpython@3.13.1t`).
fn is_free_threaded_request(request: &str) -> bool {
if request.contains("+freethreaded") {
return true;
}
request.split(['-', '@']).any(|segment| {
let Some(version) = segment.strip_suffix('t') else {
return false;
};
version.ends_with(|c: char| c.is_ascii_digit())
})
}

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

#[rstest]
#[case("3.13t", true)]
#[case("3.13.1t", true)]
#[case("cpython@3.14t", true)]
#[case("cpython-3.13t-linux-x86_64-gnu", true)]
#[case("cpython-3.13.0+freethreaded-linux-x86_64-gnu", true)]
#[case("3.13", false)]
#[case("cpython@3.13", false)]
#[case("cpython-3.13.0-linux-x86_64-gnu", false)]
#[case("pypy@3.10", false)]
#[case("graalpy", false)]
fn classifies_uv_python_request(#[case] request: &str, #[case] expected: bool) {
assert_eq!(is_free_threaded_request(request), expected);
}
}
9 changes: 5 additions & 4 deletions src/executor/valgrind/measure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ pub async fn measure(
config,
));

// Only set PYTHONMALLOC=malloc for non-free-threaded Python builds.
// Free-threaded Python (with GIL disabled) manages memory differently and
// should not have PYTHONMALLOC overridden.
if !is_free_threaded_python() {
// Free-threaded Python (GIL disabled) does not support PYTHONMALLOC=malloc
// and refuses to start with it set.
if !config.disable_pythonmalloc
&& !is_free_threaded_python(config.working_directory.as_deref().map(Path::new))
{
cmd.env("PYTHONMALLOC", "malloc");
}

Expand Down