Skip to content

Commit 1139c39

Browse files
feat: collect Python toolchain information via instrument hooks environment API
Update instrument-hooks submodule to pick up the new environment collection API and use it to report Python version and build info at runtime. Generated with AI Agent (Claude Code)
1 parent aa267f3 commit 1139c39

6 files changed

Lines changed: 136 additions & 2 deletions

File tree

src/pytest_codspeed/instruments/analysis.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(self, config: CodSpeedConfig, mode: MeasurementMode) -> None:
3333
try:
3434
self.instrument_hooks = InstrumentHooks()
3535
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)
36+
self.instrument_hooks.collect_and_write_python_environment()
3637
except RuntimeError as e:
3738
if os.environ.get("CODSPEED_ENV") is not None:
3839
raise Exception(

src/pytest_codspeed/instruments/hooks/__init__.py

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
from __future__ import annotations
22

33
import os
4+
import platform
5+
import shlex
46
import sys
7+
import sysconfig
58
import warnings
69
from typing import TYPE_CHECKING
710

811
from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE
912

1013
if TYPE_CHECKING:
14+
from cffi import FFI
15+
1116
from .dist_instrument_hooks import InstrumentHooksPointer, LibType
1217

1318
# Feature flags for instrument hooks
@@ -18,6 +23,7 @@ class InstrumentHooks:
1823
"""Zig library wrapper class providing benchmark measurement functionality."""
1924

2025
lib: LibType
26+
ffi: FFI
2127
instance: InstrumentHooksPointer
2228

2329
def __init__(self) -> None:
@@ -28,10 +34,11 @@ def __init__(self) -> None:
2834
)
2935

3036
try:
31-
from .dist_instrument_hooks import lib # type: ignore
37+
from .dist_instrument_hooks import ffi, lib # type: ignore
3238
except ImportError as e:
3339
raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e
3440
self.lib = lib
41+
self.ffi = ffi
3542

3643
self.instance = self.lib.instrument_hooks_init()
3744
if self.instance == 0:
@@ -92,3 +99,99 @@ def set_feature(self, feature: int, enabled: bool) -> None:
9299
enabled: Whether to enable or disable the feature
93100
"""
94101
self.lib.instrument_hooks_set_feature(feature, enabled)
102+
103+
def set_environment(self, section_name: str, key: str, value: str) -> None:
104+
"""Register a key-value pair under a named section for environment collection.
105+
106+
Args:
107+
section_name: The section name (e.g. "Python")
108+
key: The key (e.g. "version")
109+
value: The value (e.g. "3.13.12")
110+
"""
111+
ret = self.lib.instrument_hooks_set_environment(
112+
self.instance,
113+
section_name.encode("utf-8"),
114+
key.encode("utf-8"),
115+
value.encode("utf-8"),
116+
)
117+
if ret != 0:
118+
warnings.warn("Failed to set environment data", RuntimeWarning)
119+
120+
def set_environment_list(
121+
self, section_name: str, key: str, values: list[str]
122+
) -> None:
123+
"""Register a list of values under a named section for environment collection.
124+
125+
Args:
126+
section_name: The section name (e.g. "python")
127+
key: The key (e.g. "build_args")
128+
values: The list of string values
129+
"""
130+
encoded = [self.ffi.new("char[]", v.encode("utf-8")) for v in values]
131+
c_values = self.ffi.new("char*[]", encoded)
132+
ret = self.lib.instrument_hooks_set_environment_list(
133+
self.instance,
134+
section_name.encode("utf-8"),
135+
key.encode("utf-8"),
136+
c_values,
137+
len(encoded),
138+
)
139+
if ret != 0:
140+
warnings.warn("Failed to set environment list data", RuntimeWarning)
141+
142+
def write_environment(self, pid: int | None = None) -> None:
143+
"""Flush all registered environment sections to disk.
144+
145+
Writes to $CODSPEED_PROFILE_FOLDER/environment-<pid>.json.
146+
147+
Args:
148+
pid: Optional process ID (defaults to current process)
149+
"""
150+
if pid is None:
151+
pid = os.getpid()
152+
ret = self.lib.instrument_hooks_write_environment(self.instance, pid)
153+
if ret != 0:
154+
warnings.warn("Failed to write environment data", RuntimeWarning)
155+
156+
def collect_and_write_python_environment(self) -> None:
157+
"""Collect Python toolchain information and write it to disk."""
158+
section = "python"
159+
set_env = self.set_environment
160+
161+
# Core identity
162+
set_env(section, "version", sys.version.strip())
163+
set_env(section, "implementation", sys.implementation.name.strip())
164+
set_env(section, "compiler", platform.python_compiler().strip())
165+
166+
config_vars = sysconfig.get_config_vars()
167+
168+
# Build arguments as a list
169+
config_args = config_vars.get("CONFIG_ARGS", "")
170+
if config_args:
171+
build_args = [arg.strip() for arg in shlex.split(config_args)]
172+
self.set_environment_list(section, "build_args", build_args)
173+
174+
# Performance-relevant build configuration as "KEY=value" list
175+
_SYSCONFIG_KEYS = (
176+
"abiflags",
177+
"PY_ENABLE_SHARED",
178+
"Py_GIL_DISABLED",
179+
"Py_DEBUG",
180+
"WITH_PYMALLOC",
181+
"WITH_MIMALLOC",
182+
"WITH_FREELISTS",
183+
"HAVE_COMPUTED_GOTOS",
184+
"Py_STATS",
185+
"Py_TRACE_REFS",
186+
"WITH_VALGRIND",
187+
"WITH_DTRACE",
188+
)
189+
config_items = []
190+
for key in _SYSCONFIG_KEYS:
191+
value = config_vars.get(key)
192+
if value is not None:
193+
config_items.append(f"{key}={str(value).strip()}")
194+
config_items.append(f"perf_trampoline={SUPPORTS_PERF_TRAMPOLINE}")
195+
self.set_environment_list(section, "config", config_items)
196+
197+
self.write_environment()

src/pytest_codspeed/instruments/hooks/build.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@
3636
void callgrind_stop_instrumentation();
3737
3838
void instrument_hooks_set_feature(uint64_t feature, bool enabled);
39+
40+
uint8_t instrument_hooks_set_environment(InstrumentHooks *, const char *section_name,
41+
const char *key, const char *value);
42+
uint8_t instrument_hooks_set_environment_list(InstrumentHooks *,
43+
const char *section_name,
44+
const char *key,
45+
const char *const *values,
46+
uint32_t count);
47+
uint8_t instrument_hooks_write_environment(InstrumentHooks *, uint32_t pid);
3948
""")
4049

4150
ffibuilder.set_source(
@@ -47,6 +56,8 @@
4756
"src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c",
4857
],
4958
include_dirs=[str(includes_dir)],
59+
# IMPORTANT: Keep in sync with instrument-hooks/ci.yml (COMMON_CFLAGS)
60+
extra_compile_args=["-Wno-format-security"],
5061
)
5162

5263
if __name__ == "__main__":

src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from cffi import FFI
2+
13
InstrumentHooksPointer = object
24

35
class lib:
@@ -31,5 +33,21 @@ class lib:
3133
def callgrind_stop_instrumentation() -> int: ...
3234
@staticmethod
3335
def instrument_hooks_set_feature(feature: int, enabled: bool) -> None: ...
36+
@staticmethod
37+
def instrument_hooks_set_environment(
38+
hooks: InstrumentHooksPointer, section_name: bytes, key: bytes, value: bytes
39+
) -> int: ...
40+
@staticmethod
41+
def instrument_hooks_set_environment_list(
42+
hooks: InstrumentHooksPointer,
43+
section_name: bytes,
44+
key: bytes,
45+
values: FFI.CData,
46+
count: int,
47+
) -> int: ...
48+
@staticmethod
49+
def instrument_hooks_write_environment(
50+
hooks: InstrumentHooksPointer, pid: int
51+
) -> int: ...
3452

3553
LibType = type[lib]

src/pytest_codspeed/instruments/walltime.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ def __init__(self, config: CodSpeedConfig, _mode: MeasurementMode) -> None:
163163
try:
164164
self.instrument_hooks = InstrumentHooks()
165165
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)
166+
self.instrument_hooks.collect_and_write_python_environment()
166167
except RuntimeError as e:
167168
if os.environ.get("CODSPEED_ENV") is not None:
168169
warnings.warn(

0 commit comments

Comments
 (0)