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/pytest_codspeed/instruments/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def __init__(self, config: CodSpeedConfig, mode: MeasurementMode) -> None:
try:
self.instrument_hooks = InstrumentHooks()
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)
self.instrument_hooks.collect_and_write_python_environment()
except RuntimeError as e:
if os.environ.get("CODSPEED_ENV") is not None:
raise Exception(
Expand Down
105 changes: 104 additions & 1 deletion src/pytest_codspeed/instruments/hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
from __future__ import annotations

import os
import platform
import shlex
import sys
import sysconfig
import warnings
from typing import TYPE_CHECKING

from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE

if TYPE_CHECKING:
from cffi import FFI

from .dist_instrument_hooks import InstrumentHooksPointer, LibType

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

lib: LibType
ffi: FFI
instance: InstrumentHooksPointer

def __init__(self) -> None:
Expand All @@ -28,10 +34,11 @@ def __init__(self) -> None:
)

try:
from .dist_instrument_hooks import lib # type: ignore
from .dist_instrument_hooks import ffi, lib # type: ignore
except ImportError as e:
raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e
self.lib = lib
self.ffi = ffi

self.instance = self.lib.instrument_hooks_init()
if self.instance == 0:
Expand Down Expand Up @@ -92,3 +99,99 @@ def set_feature(self, feature: int, enabled: bool) -> None:
enabled: Whether to enable or disable the feature
"""
self.lib.instrument_hooks_set_feature(feature, enabled)

def set_environment(self, section_name: str, key: str, value: str) -> None:
"""Register a key-value pair under a named section for environment collection.

Args:
section_name: The section name (e.g. "Python")
key: The key (e.g. "version")
value: The value (e.g. "3.13.12")
"""
ret = self.lib.instrument_hooks_set_environment(
self.instance,
section_name.encode("utf-8"),
key.encode("utf-8"),
value.encode("utf-8"),
)
if ret != 0:
warnings.warn("Failed to set environment data", RuntimeWarning)

def set_environment_list(
self, section_name: str, key: str, values: list[str]
) -> None:
"""Register a list of values under a named section for environment collection.

Args:
section_name: The section name (e.g. "python")
key: The key (e.g. "build_args")
values: The list of string values
"""
encoded = [self.ffi.new("char[]", v.encode("utf-8")) for v in values]
c_values = self.ffi.new("char*[]", encoded)
ret = self.lib.instrument_hooks_set_environment_list(
self.instance,
section_name.encode("utf-8"),
key.encode("utf-8"),
c_values,
len(encoded),
)
if ret != 0:
warnings.warn("Failed to set environment list data", RuntimeWarning)

def write_environment(self, pid: int | None = None) -> None:
"""Flush all registered environment sections to disk.

Writes to $CODSPEED_PROFILE_FOLDER/environment-<pid>.json.

Args:
pid: Optional process ID (defaults to current process)
"""
if pid is None:
pid = os.getpid()
ret = self.lib.instrument_hooks_write_environment(self.instance, pid)
if ret != 0:
warnings.warn("Failed to write environment data", RuntimeWarning)

def collect_and_write_python_environment(self) -> None:
"""Collect Python toolchain information and write it to disk."""
section = "python"
set_env = self.set_environment

# Core identity
set_env(section, "version", sys.version)
set_env(section, "implementation", sys.implementation.name)
set_env(section, "compiler", platform.python_compiler())

config_vars = sysconfig.get_config_vars()

# Build arguments as a list
config_args = config_vars.get("CONFIG_ARGS", "")
if config_args:
build_args = shlex.split(config_args)
self.set_environment_list(section, "build_args", build_args)

# Performance-relevant build configuration as "KEY=value" list
_SYSCONFIG_KEYS = (
"abiflags",
"PY_ENABLE_SHARED",
"Py_GIL_DISABLED",
"Py_DEBUG",
"WITH_PYMALLOC",
"WITH_MIMALLOC",
"WITH_FREELISTS",
"HAVE_COMPUTED_GOTOS",
"Py_STATS",
"Py_TRACE_REFS",
"WITH_VALGRIND",
"WITH_DTRACE",
)
config_items = []
for key in _SYSCONFIG_KEYS:
value = config_vars.get(key)
if value is not None:
config_items.append(f"{key}={value}")
config_items.append(f"perf_trampoline={SUPPORTS_PERF_TRAMPOLINE}")
self.set_environment_list(section, "config", config_items)

self.write_environment()
11 changes: 11 additions & 0 deletions src/pytest_codspeed/instruments/hooks/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@
void callgrind_stop_instrumentation();

void instrument_hooks_set_feature(uint64_t feature, bool enabled);

uint8_t instrument_hooks_set_environment(InstrumentHooks *, const char *section_name,
const char *key, const char *value);
uint8_t instrument_hooks_set_environment_list(InstrumentHooks *,
const char *section_name,
const char *key,
const char *const *values,
uint32_t count);
uint8_t instrument_hooks_write_environment(InstrumentHooks *, uint32_t pid);
""")

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

if __name__ == "__main__":
Expand Down
18 changes: 18 additions & 0 deletions src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from cffi import FFI

InstrumentHooksPointer = object

class lib:
Expand Down Expand Up @@ -31,5 +33,21 @@ class lib:
def callgrind_stop_instrumentation() -> int: ...
@staticmethod
def instrument_hooks_set_feature(feature: int, enabled: bool) -> None: ...
@staticmethod
def instrument_hooks_set_environment(
hooks: InstrumentHooksPointer, section_name: bytes, key: bytes, value: bytes
) -> int: ...
@staticmethod
def instrument_hooks_set_environment_list(
hooks: InstrumentHooksPointer,
section_name: bytes,
key: bytes,
values: FFI.CData,
count: int,
) -> int: ...
@staticmethod
def instrument_hooks_write_environment(
hooks: InstrumentHooksPointer, pid: int
) -> int: ...

LibType = type[lib]
1 change: 1 addition & 0 deletions src/pytest_codspeed/instruments/walltime.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ def __init__(self, config: CodSpeedConfig, _mode: MeasurementMode) -> None:
try:
self.instrument_hooks = InstrumentHooks()
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)
self.instrument_hooks.collect_and_write_python_environment()
except RuntimeError as e:
if os.environ.get("CODSPEED_ENV") is not None:
warnings.warn(
Expand Down
Loading