pytest-codspeed 2.2.1__tar.gz → 3.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 (20) hide show
  1. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/.gitignore +2 -0
  2. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/PKG-INFO +20 -16
  3. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/README.md +8 -7
  4. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/pyproject.toml +19 -11
  5. pytest_codspeed-3.0.0/src/pytest_codspeed/__init__.py +7 -0
  6. pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/__init__.py +60 -0
  7. pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind/__init__.py +95 -0
  8. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/.gitignore +1 -0
  9. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/__init__.py +3 -1
  10. pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/walltime.py +265 -0
  11. pytest_codspeed-3.0.0/src/pytest_codspeed/plugin.py +363 -0
  12. pytest_codspeed-3.0.0/src/pytest_codspeed/py.typed +0 -0
  13. pytest_codspeed-3.0.0/src/pytest_codspeed/utils.py +63 -0
  14. pytest_codspeed-2.2.1/src/pytest_codspeed/__init__.py +0 -5
  15. pytest_codspeed-2.2.1/src/pytest_codspeed/plugin.py +0 -309
  16. pytest_codspeed-2.2.1/src/pytest_codspeed/utils.py +0 -35
  17. {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/LICENSE +0 -0
  18. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.c +0 -0
  19. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.h +0 -0
  20. {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.pyi +0 -0
@@ -162,3 +162,5 @@ cython_debug/
162
162
  .venvubuntu
163
163
  .python-version
164
164
  *.o
165
+
166
+ .codspeed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pytest-codspeed
3
- Version: 2.2.1
3
+ Version: 3.0.0
4
4
  Summary: Pytest plugin to create CodSpeed benchmarks
5
5
  Project-URL: Homepage, https://codspeed.io/
6
6
  Project-URL: Documentation, https://docs.codspeed.io/
@@ -35,27 +35,30 @@ Classifier: Intended Audience :: Developers
35
35
  Classifier: Intended Audience :: Information Technology
36
36
  Classifier: License :: OSI Approved :: MIT License
37
37
  Classifier: Programming Language :: Python :: 3
38
- Classifier: Programming Language :: Python :: 3.7
39
- Classifier: Programming Language :: Python :: 3.8
40
38
  Classifier: Programming Language :: Python :: 3.9
41
39
  Classifier: Programming Language :: Python :: 3.10
42
40
  Classifier: Programming Language :: Python :: 3.11
43
41
  Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Programming Language :: Python :: 3.13
44
43
  Classifier: Topic :: Software Development :: Testing
45
44
  Classifier: Topic :: System :: Benchmark
46
45
  Classifier: Topic :: Utilities
47
46
  Classifier: Typing :: Typed
48
- Requires-Python: >=3.7
49
- Requires-Dist: cffi>=1.15.1
47
+ Requires-Python: >=3.9
48
+ Requires-Dist: cffi>=1.17.1
50
49
  Requires-Dist: filelock>=3.12.2
50
+ Requires-Dist: importlib-metadata>=8.5.0; python_version < '3.10'
51
51
  Requires-Dist: pytest>=3.8
52
+ Requires-Dist: rich>=13.8.1
52
53
  Requires-Dist: setuptools; python_full_version >= '3.12.0'
54
+ Provides-Extra: build
55
+ Requires-Dist: semver>=3.0.2; extra == 'build'
53
56
  Provides-Extra: compat
54
- Requires-Dist: pytest-benchmark~=4.0.0; extra == 'compat'
55
- Requires-Dist: pytest-xdist~=2.0.0; extra == 'compat'
57
+ Requires-Dist: pytest-benchmark~=5.0.0; extra == 'compat'
58
+ Requires-Dist: pytest-xdist~=3.6.1; extra == 'compat'
56
59
  Provides-Extra: lint
57
- Requires-Dist: mypy~=1.3.0; extra == 'lint'
58
- Requires-Dist: ruff~=0.3.3; extra == 'lint'
60
+ Requires-Dist: mypy~=1.11.2; extra == 'lint'
61
+ Requires-Dist: ruff~=0.6.5; extra == 'lint'
59
62
  Provides-Extra: test
60
63
  Requires-Dist: pytest-cov~=4.0.0; extra == 'test'
61
64
  Requires-Dist: pytest~=7.0; extra == 'test'
@@ -66,7 +69,7 @@ Description-Content-Type: text/markdown
66
69
 
67
70
  [![CI](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
68
71
  [![PyPi Version](https://img.shields.io/pypi/v/pytest-codspeed?color=%2334D058&label=pypi)](https://pypi.org/project/pytest-codspeed)
69
- ![Python Version](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-informational.svg)
72
+ ![Python Version](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12%20|%203.13-informational.svg)
70
73
  [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF)
71
74
  [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/pytest-codspeed)
72
75
 
@@ -76,7 +79,7 @@ Pytest plugin to create CodSpeed benchmarks
76
79
 
77
80
  ## Requirements
78
81
 
79
- **Python**: 3.7 and later
82
+ **Python**: 3.9 and later
80
83
 
81
84
  **pytest**: any recent version
82
85
 
@@ -151,7 +154,7 @@ You can use the [CodSpeedHQ/action](https://github.com/CodSpeedHQ/action) to run
151
154
  Example workflow:
152
155
 
153
156
  ```yaml
154
- name: benchmarks
157
+ name: CodSpeed
155
158
 
156
159
  on:
157
160
  push:
@@ -161,16 +164,17 @@ on:
161
164
 
162
165
  jobs:
163
166
  benchmarks:
167
+ name: Run benchmarks
164
168
  runs-on: ubuntu-latest
165
169
  steps:
166
- - uses: actions/checkout@v3
167
- - uses: actions/setup-python@v3
170
+ - uses: actions/checkout@v4
171
+ - uses: actions/setup-python@v5
168
172
  with:
169
- python-version: "3.9"
173
+ python-version: "3.12"
170
174
  - name: Install dependencies
171
175
  run: pip install -r requirements.txt
172
176
  - name: Run benchmarks
173
- uses: CodSpeedHQ/action@v2
177
+ uses: CodSpeedHQ/action@v3
174
178
  with:
175
179
  token: ${{ secrets.CODSPEED_TOKEN }}
176
180
  run: pytest tests/ --codspeed
@@ -3,7 +3,7 @@
3
3
 
4
4
  [![CI](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml/badge.svg)](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
5
5
  [![PyPi Version](https://img.shields.io/pypi/v/pytest-codspeed?color=%2334D058&label=pypi)](https://pypi.org/project/pytest-codspeed)
6
- ![Python Version](https://img.shields.io/badge/python-3.7%20|%203.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-informational.svg)
6
+ ![Python Version](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12%20|%203.13-informational.svg)
7
7
  [![Discord](https://img.shields.io/badge/chat%20on-discord-7289da.svg)](https://discord.com/invite/MxpaCfKSqF)
8
8
  [![CodSpeed Badge](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/CodSpeedHQ/pytest-codspeed)
9
9
 
@@ -13,7 +13,7 @@ Pytest plugin to create CodSpeed benchmarks
13
13
 
14
14
  ## Requirements
15
15
 
16
- **Python**: 3.7 and later
16
+ **Python**: 3.9 and later
17
17
 
18
18
  **pytest**: any recent version
19
19
 
@@ -88,7 +88,7 @@ You can use the [CodSpeedHQ/action](https://github.com/CodSpeedHQ/action) to run
88
88
  Example workflow:
89
89
 
90
90
  ```yaml
91
- name: benchmarks
91
+ name: CodSpeed
92
92
 
93
93
  on:
94
94
  push:
@@ -98,16 +98,17 @@ on:
98
98
 
99
99
  jobs:
100
100
  benchmarks:
101
+ name: Run benchmarks
101
102
  runs-on: ubuntu-latest
102
103
  steps:
103
- - uses: actions/checkout@v3
104
- - uses: actions/setup-python@v3
104
+ - uses: actions/checkout@v4
105
+ - uses: actions/setup-python@v5
105
106
  with:
106
- python-version: "3.9"
107
+ python-version: "3.12"
107
108
  - name: Install dependencies
108
109
  run: pip install -r requirements.txt
109
110
  - name: Run benchmarks
110
- uses: CodSpeedHQ/action@v2
111
+ uses: CodSpeedHQ/action@v3
111
112
  with:
112
113
  token: ${{ secrets.CODSPEED_TOKEN }}
113
114
  run: pytest tests/ --codspeed
@@ -13,7 +13,7 @@ dynamic = ["version"]
13
13
  description = "Pytest plugin to create CodSpeed benchmarks"
14
14
  readme = "README.md"
15
15
  license = { file = "LICENSE" }
16
- requires-python = ">=3.7"
16
+ requires-python = ">=3.9"
17
17
  authors = [{ name = "Arthur Pastel", email = "arthur@codspeed.io" }]
18
18
  keywords = ["codspeed", "benchmark", "performance", "pytest"]
19
19
  classifiers = [
@@ -23,31 +23,36 @@ classifiers = [
23
23
  "Intended Audience :: Information Technology",
24
24
  "License :: OSI Approved :: MIT License",
25
25
  "Programming Language :: Python :: 3",
26
- "Programming Language :: Python :: 3.7",
27
- "Programming Language :: Python :: 3.8",
28
26
  "Programming Language :: Python :: 3.9",
29
27
  "Programming Language :: Python :: 3.10",
30
28
  "Programming Language :: Python :: 3.11",
31
29
  "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
32
31
  "Topic :: Software Development :: Testing",
33
32
  "Topic :: System :: Benchmark",
34
33
  "Topic :: Utilities",
35
34
  "Typing :: Typed",
36
35
  ]
37
36
  dependencies = [
38
- "cffi >= 1.15.1",
37
+ "cffi >= 1.17.1",
39
38
  # cffi doesn't automatically install setuptools with python 3.12+
40
39
  # cf https://github.com/python-cffi/cffi/releases/tag/v1.16.0
41
40
  "setuptools; python_full_version >= '3.12.0'",
42
41
  "pytest>=3.8",
43
42
  "filelock >= 3.12.2",
43
+ "rich>=13.8.1",
44
+ "importlib-metadata>=8.5.0; python_version < '3.10'",
44
45
  ]
45
46
 
46
47
  [project.optional-dependencies]
47
- lint = ["mypy ~= 1.3.0", "ruff ~= 0.3.3"]
48
- compat = ["pytest-benchmark ~= 4.0.0", "pytest-xdist ~= 2.0.0"]
48
+ build = ["semver>=3.0.2"]
49
+ lint = ["mypy ~= 1.11.2", "ruff ~= 0.6.5"]
50
+ compat = [
51
+ "pytest-benchmark ~= 5.0.0",
52
+ "pytest-xdist ~= 3.6.1",
53
+ # "pytest-speed>=0.3.5",
54
+ ]
49
55
  test = ["pytest ~= 7.0", "pytest-cov ~= 4.0.0"]
50
-
51
56
  [project.entry-points]
52
57
  pytest11 = { codspeed = "pytest_codspeed.plugin" }
53
58
 
@@ -59,7 +64,7 @@ features = ["lint", "test", "compat"]
59
64
  features = ["test"]
60
65
 
61
66
  [[tool.hatch.envs.test.matrix]]
62
- python = ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
67
+ python = ["3.9", "3.10", "3.11", "3.12", "3.13"]
63
68
  features = ["compat", "test"]
64
69
 
65
70
  [tool.hatch.version]
@@ -72,9 +77,11 @@ include = ["/src"]
72
77
  python_version = "3.12"
73
78
 
74
79
  [tool.ruff]
75
- line-length = 88
80
+ target-version = "py37"
81
+
76
82
  [tool.ruff.lint]
77
- select = ["E", "F", "I", "C"]
83
+ select = ["E", "F", "I", "C", "TCH", "FA", "UP"]
84
+ flake8-type-checking = { exempt-modules = [], strict = true }
78
85
 
79
86
  [tool.isort]
80
87
  line_length = 88
@@ -85,8 +92,9 @@ force_grid_wrap = 0
85
92
  float_to_top = true
86
93
 
87
94
  [tool.pytest.ini_options]
88
- addopts = "--ignore=tests/benchmarks --ignore=tests/examples"
95
+ addopts = "--ignore=tests/benchmarks --ignore=tests/examples --ignore=tests/benchmarks/TheAlgorithms"
89
96
  filterwarnings = ["ignore::DeprecationWarning:pytest_benchmark.utils.*:"]
97
+ pythonpath = ["tests/benchmarks/TheAlgorithms", "./scripts"]
90
98
 
91
99
  [tool.coverage.run]
92
100
  branch = true
@@ -0,0 +1,7 @@
1
+ __version__ = "3.0.0"
2
+ # We also have the semver version since __version__ is not semver compliant
3
+ __semver_version__ = "3.0.0"
4
+
5
+ from .plugin import BenchmarkFixture
6
+
7
+ __all__ = ["BenchmarkFixture", "__version__", "__semver_version__"]
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABCMeta, abstractmethod
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Any, Callable, ClassVar, ParamSpec, TypeVar
9
+
10
+ import pytest
11
+
12
+ from pytest_codspeed.plugin import CodSpeedConfig
13
+
14
+ T = TypeVar("T")
15
+ P = ParamSpec("P")
16
+
17
+
18
+ class Instrument(metaclass=ABCMeta):
19
+ instrument: ClassVar[str]
20
+
21
+ @abstractmethod
22
+ def __init__(self, config: CodSpeedConfig): ...
23
+
24
+ @abstractmethod
25
+ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: ...
26
+
27
+ @abstractmethod
28
+ def measure(
29
+ self,
30
+ name: str,
31
+ uri: str,
32
+ fn: Callable[P, T],
33
+ *args: P.args,
34
+ **kwargs: P.kwargs,
35
+ ) -> T: ...
36
+
37
+ @abstractmethod
38
+ def report(self, session: pytest.Session) -> None: ...
39
+
40
+ @abstractmethod
41
+ def get_result_dict(
42
+ self,
43
+ ) -> dict[str, Any]: ...
44
+
45
+
46
+ class MeasurementMode(str, Enum):
47
+ Instrumentation = "instrumentation"
48
+ WallTime = "walltime"
49
+
50
+
51
+ def get_instrument_from_mode(mode: MeasurementMode) -> type[Instrument]:
52
+ from pytest_codspeed.instruments.valgrind import (
53
+ ValgrindInstrument,
54
+ )
55
+ from pytest_codspeed.instruments.walltime import WallTimeInstrument
56
+
57
+ if mode == MeasurementMode.Instrumentation:
58
+ return ValgrindInstrument
59
+ else:
60
+ return WallTimeInstrument
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pytest_codspeed import __semver_version__
8
+ from pytest_codspeed.instruments import Instrument
9
+ from pytest_codspeed.instruments.valgrind._wrapper import get_lib
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Any, Callable
13
+
14
+ from pytest import Session
15
+
16
+ from pytest_codspeed.instruments import P, T
17
+ from pytest_codspeed.instruments.valgrind._wrapper import LibType
18
+ from pytest_codspeed.plugin import CodSpeedConfig
19
+
20
+ SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)
21
+
22
+
23
+ class ValgrindInstrument(Instrument):
24
+ instrument = "valgrind"
25
+ lib: LibType | None
26
+
27
+ def __init__(self, config: CodSpeedConfig) -> None:
28
+ self.benchmark_count = 0
29
+ self.should_measure = os.environ.get("CODSPEED_ENV") is not None
30
+ if self.should_measure:
31
+ self.lib = get_lib()
32
+ self.lib.dump_stats_at(
33
+ f"Metadata: pytest-codspeed {__semver_version__}".encode("ascii")
34
+ )
35
+ if SUPPORTS_PERF_TRAMPOLINE:
36
+ sys.activate_stack_trampoline("perf") # type: ignore
37
+ else:
38
+ self.lib = None
39
+
40
+ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
41
+ config = (
42
+ f"mode: instrumentation, "
43
+ f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}"
44
+ )
45
+ warnings = []
46
+ if not self.should_measure:
47
+ warnings.append(
48
+ "\033[1m"
49
+ "NOTICE: codspeed is enabled, but no performance measurement"
50
+ " will be made since it's running in an unknown environment."
51
+ "\033[0m"
52
+ )
53
+ return config, warnings
54
+
55
+ def measure(
56
+ self,
57
+ name: str,
58
+ uri: str,
59
+ fn: Callable[P, T],
60
+ *args: P.args,
61
+ **kwargs: P.kwargs,
62
+ ) -> T:
63
+ self.benchmark_count += 1
64
+ if self.lib is None: # Thus should_measure is False
65
+ return fn(*args, **kwargs)
66
+
67
+ def __codspeed_root_frame__() -> T:
68
+ return fn(*args, **kwargs)
69
+
70
+ if SUPPORTS_PERF_TRAMPOLINE:
71
+ # Warmup CPython performance map cache
72
+ __codspeed_root_frame__()
73
+
74
+ self.lib.zero_stats()
75
+ self.lib.start_instrumentation()
76
+ try:
77
+ return __codspeed_root_frame__()
78
+ finally:
79
+ # Ensure instrumentation is stopped even if the test failed
80
+ self.lib.stop_instrumentation()
81
+ self.lib.dump_stats_at(uri.encode("ascii"))
82
+
83
+ def report(self, session: Session) -> None:
84
+ reporter = session.config.pluginmanager.get_plugin("terminalreporter")
85
+ count_suffix = "benchmarked" if self.should_measure else "benchmark tested"
86
+ reporter.write_sep(
87
+ "=",
88
+ f"{self.benchmark_count} {count_suffix}",
89
+ )
90
+
91
+ def get_result_dict(self) -> dict[str, Any]:
92
+ return {
93
+ "instrument": {"type": self.instrument},
94
+ # bench results will be dumped by valgrind
95
+ }
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  from typing import TYPE_CHECKING
3
5
 
@@ -22,7 +24,7 @@ def _get_ffi():
22
24
  return ffi
23
25
 
24
26
 
25
- def get_lib() -> "LibType":
27
+ def get_lib() -> LibType:
26
28
  try:
27
29
  ffi = _get_ffi()
28
30
  build_lock = FileLock(f"{_wrapper_dir}/build.lock")
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from math import ceil
5
+ from statistics import mean, quantiles, stdev
6
+ from time import get_clock_info, perf_counter_ns
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rich.console import Console
10
+ from rich.markup import escape
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+
14
+ from pytest_codspeed.instruments import Instrument
15
+
16
+ if TYPE_CHECKING:
17
+ from typing import Any, Callable
18
+
19
+ from pytest import Session
20
+
21
+ from pytest_codspeed.instruments import P, T
22
+ from pytest_codspeed.plugin import CodSpeedConfig
23
+
24
+ DEFAULT_WARMUP_TIME_NS = 1_000_000_000
25
+ DEFAULT_MAX_TIME_NS = 3_000_000_000
26
+ TIMER_RESOLUTION_NS = get_clock_info("perf_counter").resolution * 1e9
27
+ DEFAULT_MIN_ROUND_TIME_NS = TIMER_RESOLUTION_NS * 100
28
+
29
+
30
+ @dataclass
31
+ class BenchmarkConfig:
32
+ warmup_time_ns: int
33
+ min_round_time_ns: float
34
+ max_time_ns: int
35
+ max_rounds: int | None
36
+
37
+ @classmethod
38
+ def from_codspeed_config(cls, config: CodSpeedConfig) -> BenchmarkConfig:
39
+ return cls(
40
+ warmup_time_ns=config.warmup_time_ns
41
+ if config.warmup_time_ns is not None
42
+ else DEFAULT_WARMUP_TIME_NS,
43
+ min_round_time_ns=DEFAULT_MIN_ROUND_TIME_NS,
44
+ max_time_ns=config.max_time_ns
45
+ if config.max_time_ns is not None
46
+ else DEFAULT_MAX_TIME_NS,
47
+ max_rounds=config.max_rounds,
48
+ )
49
+
50
+
51
+ @dataclass
52
+ class BenchmarkStats:
53
+ min_ns: float
54
+ max_ns: float
55
+ mean_ns: float
56
+ stdev_ns: float
57
+
58
+ q1_ns: float
59
+ median_ns: float
60
+ q3_ns: float
61
+
62
+ rounds: int
63
+ total_time: float
64
+ iqr_outlier_rounds: int
65
+ stdev_outlier_rounds: int
66
+ iter_per_round: int
67
+ warmup_iters: int
68
+
69
+ @classmethod
70
+ def from_list(
71
+ cls,
72
+ times_ns: list[float],
73
+ *,
74
+ rounds: int,
75
+ iter_per_round: int,
76
+ warmup_iters: int,
77
+ total_time: float,
78
+ ) -> BenchmarkStats:
79
+ stdev_ns = stdev(times_ns) if len(times_ns) > 1 else 0
80
+ mean_ns = mean(times_ns)
81
+ if len(times_ns) > 1:
82
+ q1_ns, median_ns, q3_ns = quantiles(times_ns, n=4)
83
+ else:
84
+ q1_ns, median_ns, q3_ns = (
85
+ mean_ns,
86
+ mean_ns,
87
+ mean_ns,
88
+ )
89
+ iqr_ns = q3_ns - q1_ns
90
+ iqr_outlier_rounds = sum(
91
+ 1 for t in times_ns if t < q1_ns - 1.5 * iqr_ns or t > q3_ns + 1.5 * iqr_ns
92
+ )
93
+ stdev_outlier_rounds = sum(
94
+ 1
95
+ for t in times_ns
96
+ if t < mean_ns - 3 * stdev_ns or t > mean_ns + 3 * stdev_ns
97
+ )
98
+
99
+ return cls(
100
+ min_ns=min(times_ns),
101
+ max_ns=max(times_ns),
102
+ stdev_ns=stdev_ns,
103
+ mean_ns=mean_ns,
104
+ q1_ns=q1_ns,
105
+ median_ns=median_ns,
106
+ q3_ns=q3_ns,
107
+ rounds=rounds,
108
+ total_time=total_time,
109
+ iqr_outlier_rounds=iqr_outlier_rounds,
110
+ stdev_outlier_rounds=stdev_outlier_rounds,
111
+ iter_per_round=iter_per_round,
112
+ warmup_iters=warmup_iters,
113
+ )
114
+
115
+
116
+ @dataclass
117
+ class Benchmark:
118
+ name: str
119
+ uri: str
120
+
121
+ config: BenchmarkConfig
122
+ stats: BenchmarkStats
123
+
124
+
125
+ def run_benchmark(
126
+ name: str, uri: str, fn: Callable[P, T], args, kwargs, config: BenchmarkConfig
127
+ ) -> tuple[Benchmark, T]:
128
+ # Compute the actual result of the function
129
+ out = fn(*args, **kwargs)
130
+
131
+ # Warmup
132
+ times_ns: list[float] = []
133
+ warmup_start = start = perf_counter_ns()
134
+ while True:
135
+ start = perf_counter_ns()
136
+ fn(*args, **kwargs)
137
+ end = perf_counter_ns()
138
+ times_ns.append(end - start)
139
+ if end - warmup_start > config.warmup_time_ns:
140
+ break
141
+
142
+ # Round sizing
143
+ warmup_mean_ns = mean(times_ns)
144
+ warmup_iters = len(times_ns)
145
+ times_ns.clear()
146
+ iter_per_round = (
147
+ int(ceil(config.min_round_time_ns / warmup_mean_ns))
148
+ if warmup_mean_ns <= config.min_round_time_ns
149
+ else 1
150
+ )
151
+ if config.max_rounds is None:
152
+ round_time_ns = warmup_mean_ns * iter_per_round
153
+ rounds = int(config.max_time_ns / round_time_ns)
154
+ else:
155
+ rounds = config.max_rounds
156
+ rounds = max(1, rounds)
157
+
158
+ # Benchmark
159
+ iter_range = range(iter_per_round)
160
+ run_start = perf_counter_ns()
161
+ for _ in range(rounds):
162
+ start = perf_counter_ns()
163
+ for _ in iter_range:
164
+ fn(*args, **kwargs)
165
+ end = perf_counter_ns()
166
+ times_ns.append(end - start)
167
+
168
+ if end - run_start > config.max_time_ns:
169
+ # TODO: log something
170
+ break
171
+ benchmark_end = perf_counter_ns()
172
+ total_time = (benchmark_end - run_start) / 1e9
173
+
174
+ stats = BenchmarkStats.from_list(
175
+ times_ns,
176
+ rounds=rounds,
177
+ total_time=total_time,
178
+ iter_per_round=iter_per_round,
179
+ warmup_iters=warmup_iters,
180
+ )
181
+
182
+ return Benchmark(name=name, uri=uri, config=config, stats=stats), out
183
+
184
+
185
+ class WallTimeInstrument(Instrument):
186
+ instrument = "walltime"
187
+
188
+ def __init__(self, config: CodSpeedConfig) -> None:
189
+ self.config = config
190
+ self.benchmarks: list[Benchmark] = []
191
+
192
+ def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
193
+ return f"mode: walltime, timer_resolution: {TIMER_RESOLUTION_NS:.1f}ns", []
194
+
195
+ def measure(
196
+ self,
197
+ name: str,
198
+ uri: str,
199
+ fn: Callable[P, T],
200
+ *args: P.args,
201
+ **kwargs: P.kwargs,
202
+ ) -> T:
203
+ bench, out = run_benchmark(
204
+ name=name,
205
+ uri=uri,
206
+ fn=fn,
207
+ args=args,
208
+ kwargs=kwargs,
209
+ config=BenchmarkConfig.from_codspeed_config(self.config),
210
+ )
211
+ self.benchmarks.append(bench)
212
+ return out
213
+
214
+ def report(self, session: Session) -> None:
215
+ reporter = session.config.pluginmanager.get_plugin("terminalreporter")
216
+
217
+ if len(self.benchmarks) == 0:
218
+ reporter.write_sep(
219
+ "=",
220
+ f"{len(self.benchmarks)} benchmarked",
221
+ )
222
+ return
223
+ self._print_benchmark_table()
224
+ reporter.write_sep(
225
+ "=",
226
+ f"{len(self.benchmarks)} benchmarked",
227
+ )
228
+
229
+ def _print_benchmark_table(self) -> None:
230
+ table = Table(title="Benchmark Results")
231
+
232
+ table.add_column("Benchmark", justify="right", style="cyan", no_wrap=True)
233
+ table.add_column("Time (best)", justify="right", style="green bold")
234
+ table.add_column(
235
+ "Rel. StdDev",
236
+ justify="right",
237
+ )
238
+ table.add_column("Run time", justify="right")
239
+ table.add_column("Iters", justify="right")
240
+
241
+ for bench in self.benchmarks:
242
+ rsd = bench.stats.stdev_ns / bench.stats.mean_ns
243
+ rsd_text = Text(f"{rsd*100:.1f}%")
244
+ if rsd > 0.1:
245
+ rsd_text.stylize("red bold")
246
+ table.add_row(
247
+ escape(bench.name),
248
+ f"{bench.stats.min_ns/bench.stats.iter_per_round:,.0f}ns",
249
+ rsd_text,
250
+ f"{bench.stats.total_time:,.2f}s",
251
+ f"{bench.stats.iter_per_round * bench.stats.rounds:,}",
252
+ )
253
+
254
+ console = Console()
255
+ print("\n")
256
+ console.print(table)
257
+
258
+ def get_result_dict(self) -> dict[str, Any]:
259
+ return {
260
+ "instrument": {
261
+ "type": self.instrument,
262
+ "clock_info": get_clock_info("perf_counter").__dict__,
263
+ },
264
+ "benchmarks": [asdict(bench) for bench in self.benchmarks],
265
+ }