pytest-codspeed 4.4.0__tar.gz → 5.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {pytest_codspeed-4.4.0/src/pytest_codspeed.egg-info → pytest_codspeed-5.0.0}/PKG-INFO +1 -2
  2. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/pyproject.toml +12 -5
  3. pytest_codspeed-5.0.0/setup.py +65 -0
  4. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/__init__.py +2 -2
  5. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/analysis.py +7 -10
  6. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/hooks/__init__.py +63 -35
  7. pytest_codspeed-5.0.0/src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c +60952 -0
  8. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/core.h +24 -25
  9. pytest_codspeed-5.0.0/src/pytest_codspeed/instruments/hooks/instrument-hooks/scripts/release.py +54 -0
  10. pytest_codspeed-5.0.0/src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c +306 -0
  11. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/walltime.py +31 -11
  12. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0/src/pytest_codspeed.egg-info}/PKG-INFO +1 -2
  13. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed.egg-info/SOURCES.txt +1 -2
  14. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed.egg-info/requires.txt +0 -1
  15. pytest_codspeed-4.4.0/setup.py +0 -60
  16. pytest_codspeed-4.4.0/src/pytest_codspeed/instruments/hooks/build.py +0 -64
  17. pytest_codspeed-4.4.0/src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi +0 -53
  18. pytest_codspeed-4.4.0/src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c +0 -30987
  19. pytest_codspeed-4.4.0/src/pytest_codspeed/instruments/hooks/instrument-hooks/scripts/release.py +0 -25
  20. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/LICENSE +0 -0
  21. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/README.md +0 -0
  22. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/setup.cfg +0 -0
  23. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/config.py +0 -0
  24. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/__init__.py +0 -0
  25. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/callgrind.h +0 -0
  26. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/compat.h +0 -0
  27. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/valgrind.h +0 -0
  28. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/instruments/hooks/instrument-hooks/includes/zig.h +0 -0
  29. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/plugin.py +0 -0
  30. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/py.typed +0 -0
  31. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed/utils.py +0 -0
  32. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed.egg-info/dependency_links.txt +0 -0
  33. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed.egg-info/entry_points.txt +0 -0
  34. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/src/pytest_codspeed.egg-info/top_level.txt +0 -0
  35. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/tests/test_format_time.py +0 -0
  36. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/tests/test_pytest_plugin.py +0 -0
  37. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/tests/test_pytest_plugin_cpu_instrumentation.py +0 -0
  38. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/tests/test_pytest_plugin_walltime.py +0 -0
  39. {pytest_codspeed-4.4.0 → pytest_codspeed-5.0.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-codspeed
3
- Version: 4.4.0
3
+ Version: 5.0.0
4
4
  Summary: Pytest plugin to create CodSpeed benchmarks
5
5
  Author-email: Arthur Pastel <arthur@codspeed.io>
6
6
  License: The MIT License (MIT)
@@ -47,7 +47,6 @@ Classifier: Topic :: Utilities
47
47
  Classifier: Typing :: Typed
48
48
  Requires-Python: >=3.9
49
49
  Description-Content-Type: text/markdown
50
- Requires-Dist: cffi>=1.17.1
51
50
  Requires-Dist: pytest>=3.8
52
51
  Requires-Dist: rich>=13.8.1
53
52
  Requires-Dist: importlib-metadata>=8.5.0; python_version < "3.10"
@@ -31,7 +31,6 @@ classifiers = [
31
31
  "Typing :: Typed",
32
32
  ]
33
33
  dependencies = [
34
- "cffi >= 1.17.1",
35
34
  "pytest>=3.8",
36
35
  "rich>=13.8.1",
37
36
  "importlib-metadata>=8.5.0; python_version < '3.10'",
@@ -47,7 +46,7 @@ compat = [
47
46
  [tool.uv]
48
47
  # Python builds change with uv versions, and we are quite susceptible to that.
49
48
  # We pin uv to to make sure reproducibility is maintained for any contributor.
50
- required-version = "0.9.5"
49
+ required-version = "0.11.14"
51
50
 
52
51
  [tool.uv.sources]
53
52
  pytest-codspeed = { workspace = true }
@@ -66,18 +65,23 @@ dev = [
66
65
  pytest11 = { codspeed = "pytest_codspeed.plugin" }
67
66
 
68
67
  [build-system]
69
- requires = ["setuptools >= 61", "cffi >= 1.17.1"]
68
+ requires = ["setuptools >= 61"]
70
69
  build-backend = "setuptools.build_meta"
71
70
 
72
71
  [tool.setuptools]
73
72
  license-files = [] # Workaround of https://github.com/astral-sh/uv/issues/9513
74
73
 
74
+ [tool.setuptools.package-data]
75
+ pytest_codspeed = [
76
+ "instruments/hooks/instrument-hooks/includes/*.h",
77
+ ]
78
+
75
79
  [tool.setuptools.dynamic]
76
80
  version = { attr = "pytest_codspeed.__version__" }
77
81
 
78
82
 
79
83
  [tool.bumpver]
80
- current_version = "4.4.0"
84
+ current_version = "5.0.0"
81
85
  version_pattern = "MAJOR.MINOR.PATCH[-TAG[NUM]]"
82
86
  commit_message = "Release v{new_version} 🚀"
83
87
  tag_message = "Release v{new_version} 🚀"
@@ -98,7 +102,7 @@ push = false
98
102
  ]
99
103
 
100
104
  [tool.cibuildwheel]
101
- build = "cp*manylinux*"
105
+ build = "cp*{manylinux,macosx}*"
102
106
  test-extras = ["build", "test", "compat"]
103
107
  test-command = "pytest -v --ignore={project}/tests/benchmarks {project}/tests"
104
108
 
@@ -108,6 +112,9 @@ manylinux-x86_64-image = "manylinux_2_28"
108
112
  manylinux-aarch64-image = "manylinux_2_28"
109
113
  before-all = "yum -y install valgrind-devel"
110
114
 
115
+ [tool.cibuildwheel.macos]
116
+ environment = { PYTEST_CODSPEED_FORCE_EXTENSION_BUILD = "1" }
117
+
111
118
  [tool.mypy]
112
119
  python_version = "3.12"
113
120
 
@@ -0,0 +1,65 @@
1
+ import os
2
+ import platform
3
+
4
+ from setuptools import Extension, setup
5
+
6
+ system = platform.system()
7
+ current_arch = platform.machine()
8
+
9
+ print(f"System: {system} ({current_arch})")
10
+
11
+ IS_EXTENSION_BUILDABLE = (
12
+ system == "Linux" and current_arch in ["x86_64", "aarch64"]
13
+ ) or (system == "Darwin" and current_arch == "arm64")
14
+
15
+ IS_EXTENSION_REQUIRED = (
16
+ os.environ.get("PYTEST_CODSPEED_FORCE_EXTENSION_BUILD") is not None
17
+ )
18
+
19
+ SKIP_EXTENSION_BUILD = (
20
+ os.environ.get("PYTEST_CODSPEED_SKIP_EXTENSION_BUILD") is not None
21
+ )
22
+
23
+ if SKIP_EXTENSION_BUILD and IS_EXTENSION_REQUIRED:
24
+ raise ValueError("Extension build required but the build requires to skip it")
25
+
26
+ if IS_EXTENSION_REQUIRED and not IS_EXTENSION_BUILDABLE:
27
+ raise ValueError(
28
+ "The extension is required but the current platform is not supported"
29
+ )
30
+
31
+ # Build native C extension
32
+ native_extension = Extension(
33
+ "pytest_codspeed.instruments.hooks.dist_instrument_hooks",
34
+ sources=[
35
+ "src/pytest_codspeed/instruments/hooks/instrument_hooks_module.c",
36
+ "src/pytest_codspeed/instruments/hooks/instrument-hooks/dist/core.c",
37
+ ],
38
+ include_dirs=["src/pytest_codspeed/instruments/hooks/instrument-hooks/includes"],
39
+ # IMPORTANT: Keep in sync with instrument-hooks/.github/workflows/ci.yml
40
+ # (COMMON_CFLAGS). The Zig-generated core.c emits many warnings that
41
+ # upstream silences; in particular distros like Nix/Debian/Fedora inject
42
+ # -Werror=format-security, which would otherwise fail the build.
43
+ extra_compile_args=[
44
+ "-Wno-format",
45
+ "-Wno-format-security",
46
+ "-Wno-unused-but-set-variable",
47
+ "-Wno-unused-const-variable",
48
+ "-Wno-type-limits",
49
+ "-Wno-uninitialized",
50
+ ],
51
+ optional=not IS_EXTENSION_REQUIRED,
52
+ )
53
+
54
+ print(
55
+ "CodSpeed native extension is "
56
+ + ("required" if IS_EXTENSION_REQUIRED else "optional")
57
+ )
58
+
59
+ setup(
60
+ ext_modules=(
61
+ [native_extension]
62
+ if IS_EXTENSION_BUILDABLE and not SKIP_EXTENSION_BUILD
63
+ else []
64
+ ),
65
+ )
@@ -1,6 +1,6 @@
1
- __version__ = "4.4.0"
1
+ __version__ = "5.0.0"
2
2
  # We also have the semver version since __version__ is not semver compliant
3
- __semver_version__ = "4.4.0"
3
+ __semver_version__ = "5.0.0"
4
4
 
5
5
  from .plugin import BenchmarkFixture
6
6
 
@@ -6,10 +6,7 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from pytest_codspeed import __semver_version__
8
8
  from pytest_codspeed.instruments import Instrument
9
- from pytest_codspeed.instruments.hooks import (
10
- FEATURE_DISABLE_CALLGRIND_MARKERS,
11
- InstrumentHooks,
12
- )
9
+ from pytest_codspeed.instruments.hooks import InstrumentHooks
13
10
  from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE
14
11
 
15
12
  if TYPE_CHECKING:
@@ -79,17 +76,17 @@ class AnalysisInstrument(Instrument):
79
76
  # Warmup CPython performance map cache
80
77
  __codspeed_root_frame__()
81
78
 
82
- self.instrument_hooks.set_feature(FEATURE_DISABLE_CALLGRIND_MARKERS, True)
79
+ self.instrument_hooks.disable_callgrind_markers()
83
80
  self.instrument_hooks.start_benchmark()
84
81
 
85
82
  # Manually call the library function to avoid an extra stack frame. Also
86
83
  # call the callgrind markers directly to avoid extra overhead.
87
- self.instrument_hooks.lib.callgrind_start_instrumentation()
84
+ self.instrument_hooks.callgrind_start_instrumentation()
88
85
  try:
89
86
  return __codspeed_root_frame__()
90
87
  finally:
91
88
  # Ensure instrumentation is stopped even if the test failed
92
- self.instrument_hooks.lib.callgrind_stop_instrumentation()
89
+ self.instrument_hooks.callgrind_stop_instrumentation()
93
90
  self.instrument_hooks.stop_benchmark()
94
91
  self.instrument_hooks.set_executed_benchmark(uri)
95
92
 
@@ -128,16 +125,16 @@ class AnalysisInstrument(Instrument):
128
125
  # Compute the actual result of the function
129
126
  args, kwargs = pedantic_options.setup_and_get_args_kwargs()
130
127
 
131
- self.instrument_hooks.set_feature(FEATURE_DISABLE_CALLGRIND_MARKERS, True)
128
+ self.instrument_hooks.disable_callgrind_markers()
132
129
  self.instrument_hooks.start_benchmark()
133
130
 
134
131
  # Manually call the library function to avoid an extra stack frame. Also
135
132
  # call the callgrind markers directly to avoid extra overhead.
136
- self.instrument_hooks.lib.callgrind_start_instrumentation()
133
+ self.instrument_hooks.callgrind_start_instrumentation()
137
134
  try:
138
135
  out = __codspeed_root_frame__(*args, **kwargs)
139
136
  finally:
140
- self.instrument_hooks.lib.callgrind_stop_instrumentation()
137
+ self.instrument_hooks.callgrind_stop_instrumentation()
141
138
  self.instrument_hooks.stop_benchmark()
142
139
  self.instrument_hooks.set_executed_benchmark(uri)
143
140
  if pedantic_options.teardown is not None:
@@ -11,20 +11,18 @@ from typing import TYPE_CHECKING
11
11
  from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE
12
12
 
13
13
  if TYPE_CHECKING:
14
- from cffi import FFI
15
-
16
- from .dist_instrument_hooks import InstrumentHooksPointer, LibType
17
-
18
- # Feature flags for instrument hooks
19
- FEATURE_DISABLE_CALLGRIND_MARKERS = 0
14
+ from typing import Any, Callable
20
15
 
21
16
 
22
17
  class InstrumentHooks:
23
- """Zig library wrapper class providing benchmark measurement functionality."""
18
+ """Native library wrapper class providing benchmark measurement functionality."""
24
19
 
25
- lib: LibType
26
- ffi: FFI
27
- instance: InstrumentHooksPointer
20
+ _module: Any
21
+ _instance: Any
22
+ # Bound directly to the C functions in __init__ to avoid an extra Python
23
+ # stack frame when starting/stopping callgrind instrumentation.
24
+ callgrind_start_instrumentation: Callable[[], None]
25
+ callgrind_stop_instrumentation: Callable[[], None]
28
26
 
29
27
  def __init__(self) -> None:
30
28
  if os.environ.get("CODSPEED_ENV") is None:
@@ -34,32 +32,37 @@ class InstrumentHooks:
34
32
  )
35
33
 
36
34
  try:
37
- from .dist_instrument_hooks import ffi, lib # type: ignore
35
+ from . import dist_instrument_hooks # type: ignore
38
36
  except ImportError as e:
39
37
  raise RuntimeError(f"Failed to load instrument hooks library: {e}") from e
40
- self.lib = lib
41
- self.ffi = ffi
38
+ self._module = dist_instrument_hooks
39
+ self.callgrind_start_instrumentation = (
40
+ dist_instrument_hooks.callgrind_start_instrumentation
41
+ )
42
+ self.callgrind_stop_instrumentation = (
43
+ dist_instrument_hooks.callgrind_stop_instrumentation
44
+ )
42
45
 
43
- self.instance = self.lib.instrument_hooks_init()
44
- if self.instance == 0:
46
+ self._instance = self._module.instrument_hooks_init()
47
+ if self._instance is None:
45
48
  raise RuntimeError("Failed to initialize CodSpeed instrumentation library.")
46
49
 
47
50
  if SUPPORTS_PERF_TRAMPOLINE and not sys.is_stack_trampoline_active():
48
51
  sys.activate_stack_trampoline("perf") # type: ignore
49
52
 
50
53
  def __del__(self):
51
- if hasattr(self, "lib") and hasattr(self, "instance"):
52
- self.lib.instrument_hooks_deinit(self.instance)
54
+ # Don't manually deinit - let the capsule destructor handle it
55
+ pass
53
56
 
54
57
  def start_benchmark(self) -> None:
55
58
  """Start a new benchmark measurement."""
56
- ret = self.lib.instrument_hooks_start_benchmark(self.instance)
59
+ ret = self._module.instrument_hooks_start_benchmark(self._instance)
57
60
  if ret != 0:
58
61
  warnings.warn("Failed to start benchmark measurement", RuntimeWarning)
59
62
 
60
63
  def stop_benchmark(self) -> None:
61
64
  """Stop the current benchmark measurement."""
62
- ret = self.lib.instrument_hooks_stop_benchmark(self.instance)
65
+ ret = self._module.instrument_hooks_stop_benchmark(self._instance)
63
66
  if ret != 0:
64
67
  warnings.warn("Failed to stop benchmark measurement", RuntimeWarning)
65
68
 
@@ -73,32 +76,60 @@ class InstrumentHooks:
73
76
  if pid is None:
74
77
  pid = os.getpid()
75
78
 
76
- ret = self.lib.instrument_hooks_set_executed_benchmark(
77
- self.instance, pid, uri.encode("ascii")
79
+ ret = self._module.instrument_hooks_set_executed_benchmark(
80
+ self._instance, pid, uri.encode("ascii")
78
81
  )
79
82
  if ret != 0:
80
83
  warnings.warn("Failed to set executed benchmark", RuntimeWarning)
81
84
 
85
+ @staticmethod
86
+ def current_timestamp() -> int:
87
+ """Return a monotonic timestamp in nanoseconds from the native library."""
88
+ from . import dist_instrument_hooks # type: ignore
89
+
90
+ return dist_instrument_hooks.instrument_hooks_current_timestamp()
91
+
92
+ def add_marker(
93
+ self, marker_type: int, timestamp: int, pid: int | None = None
94
+ ) -> None:
95
+ """Emit a single marker at the given timestamp."""
96
+ if pid is None:
97
+ pid = os.getpid()
98
+ ret = self._module.instrument_hooks_add_marker(
99
+ self._instance, pid, marker_type, timestamp
100
+ )
101
+ if ret != 0:
102
+ warnings.warn("Failed to add marker", RuntimeWarning)
103
+
104
+ def add_benchmark_timestamps(self, start: int, end: int) -> None:
105
+ """Emit a BenchmarkStart/BenchmarkEnd marker pair around a captured window."""
106
+ self.add_marker(self._module.MARKER_TYPE_BENCHMARK_START, start)
107
+ self.add_marker(self._module.MARKER_TYPE_BENCHMARK_END, end)
108
+
82
109
  def set_integration(self, name: str, version: str) -> None:
83
110
  """Set the integration name and version."""
84
- ret = self.lib.instrument_hooks_set_integration(
85
- self.instance, name.encode("ascii"), version.encode("ascii")
111
+ ret = self._module.instrument_hooks_set_integration(
112
+ self._instance, name.encode("ascii"), version.encode("ascii")
86
113
  )
87
114
  if ret != 0:
88
115
  warnings.warn("Failed to set integration name and version", RuntimeWarning)
89
116
 
90
117
  def is_instrumented(self) -> bool:
91
118
  """Check if simulation is active."""
92
- return self.lib.instrument_hooks_is_instrumented(self.instance)
119
+ return self._module.instrument_hooks_is_instrumented(self._instance)
93
120
 
94
- def set_feature(self, feature: int, enabled: bool) -> None:
121
+ def _set_feature(self, feature: int, enabled: bool) -> None:
95
122
  """Set a feature flag in the instrument hooks library.
96
123
 
97
124
  Args:
98
125
  feature: The feature flag to set
99
126
  enabled: Whether to enable or disable the feature
100
127
  """
101
- self.lib.instrument_hooks_set_feature(feature, enabled)
128
+ self._module.instrument_hooks_set_feature(feature, enabled)
129
+
130
+ def disable_callgrind_markers(self, disabled: bool = True) -> None:
131
+ """Disable automatic callgrind markers around benchmark start/stop."""
132
+ self._set_feature(self._module.FEATURE_DISABLE_CALLGRIND_MARKERS, disabled)
102
133
 
103
134
  def set_environment(self, section_name: str, key: str, value: str) -> None:
104
135
  """Register a key-value pair under a named section for environment collection.
@@ -108,8 +139,8 @@ class InstrumentHooks:
108
139
  key: The key (e.g. "version")
109
140
  value: The value (e.g. "3.13.12")
110
141
  """
111
- ret = self.lib.instrument_hooks_set_environment(
112
- self.instance,
142
+ ret = self._module.instrument_hooks_set_environment(
143
+ self._instance,
113
144
  section_name.encode("utf-8"),
114
145
  key.encode("utf-8"),
115
146
  value.encode("utf-8"),
@@ -127,14 +158,11 @@ class InstrumentHooks:
127
158
  key: The key (e.g. "build_args")
128
159
  values: The list of string values
129
160
  """
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,
161
+ ret = self._module.instrument_hooks_set_environment_list(
162
+ self._instance,
134
163
  section_name.encode("utf-8"),
135
164
  key.encode("utf-8"),
136
- c_values,
137
- len(encoded),
165
+ [v.encode("utf-8") for v in values],
138
166
  )
139
167
  if ret != 0:
140
168
  warnings.warn("Failed to set environment list data", RuntimeWarning)
@@ -149,7 +177,7 @@ class InstrumentHooks:
149
177
  """
150
178
  if pid is None:
151
179
  pid = os.getpid()
152
- ret = self.lib.instrument_hooks_write_environment(self.instance, pid)
180
+ ret = self._module.instrument_hooks_write_environment(self._instance, pid)
153
181
  if ret != 0:
154
182
  warnings.warn("Failed to write environment data", RuntimeWarning)
155
183