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.
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/.gitignore +2 -0
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/PKG-INFO +20 -16
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/README.md +8 -7
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/pyproject.toml +19 -11
- pytest_codspeed-3.0.0/src/pytest_codspeed/__init__.py +7 -0
- pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/__init__.py +60 -0
- pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind/__init__.py +95 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/.gitignore +1 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/__init__.py +3 -1
- pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/walltime.py +265 -0
- pytest_codspeed-3.0.0/src/pytest_codspeed/plugin.py +363 -0
- pytest_codspeed-3.0.0/src/pytest_codspeed/py.typed +0 -0
- pytest_codspeed-3.0.0/src/pytest_codspeed/utils.py +63 -0
- pytest_codspeed-2.2.1/src/pytest_codspeed/__init__.py +0 -5
- pytest_codspeed-2.2.1/src/pytest_codspeed/plugin.py +0 -309
- pytest_codspeed-2.2.1/src/pytest_codspeed/utils.py +0 -35
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0}/LICENSE +0 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.c +0 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.h +0 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.pyi +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pytest-codspeed
|
|
3
|
-
Version:
|
|
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.
|
|
49
|
-
Requires-Dist: cffi>=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~=
|
|
55
|
-
Requires-Dist: pytest-xdist~=
|
|
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.
|
|
58
|
-
Requires-Dist: ruff~=0.
|
|
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
|
[](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
|
|
68
71
|
[](https://pypi.org/project/pytest-codspeed)
|
|
69
|
-

|
|
70
73
|
[](https://discord.com/invite/MxpaCfKSqF)
|
|
71
74
|
[](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.
|
|
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:
|
|
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@
|
|
167
|
-
- uses: actions/setup-python@
|
|
170
|
+
- uses: actions/checkout@v4
|
|
171
|
+
- uses: actions/setup-python@v5
|
|
168
172
|
with:
|
|
169
|
-
python-version: "3.
|
|
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@
|
|
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
|
[](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
|
|
5
5
|
[](https://pypi.org/project/pytest-codspeed)
|
|
6
|
-

|
|
7
7
|
[](https://discord.com/invite/MxpaCfKSqF)
|
|
8
8
|
[](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.
|
|
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:
|
|
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@
|
|
104
|
-
- uses: actions/setup-python@
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
- uses: actions/setup-python@v5
|
|
105
106
|
with:
|
|
106
|
-
python-version: "3.
|
|
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@
|
|
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.
|
|
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.
|
|
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
|
-
|
|
48
|
-
|
|
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.
|
|
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
|
-
|
|
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,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() ->
|
|
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
|
+
}
|