pytest-codspeed 2.2.1__tar.gz → 3.0.0b0__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.0b0}/.gitignore +2 -0
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/PKG-INFO +15 -13
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/README.md +6 -6
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/pyproject.toml +18 -10
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/src/pytest_codspeed/__init__.py +1 -1
- pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/__init__.py +60 -0
- pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind/__init__.py +95 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/.gitignore +1 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/__init__.py +3 -1
- pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/walltime.py +236 -0
- pytest_codspeed-3.0.0b0/src/pytest_codspeed/plugin.py +338 -0
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/src/pytest_codspeed/utils.py +33 -5
- pytest_codspeed-2.2.1/src/pytest_codspeed/plugin.py +0 -309
- {pytest_codspeed-2.2.1 → pytest_codspeed-3.0.0b0}/LICENSE +0 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.c +0 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/src/pytest_codspeed/instruments/valgrind}/_wrapper/wrapper.h +0 -0
- {pytest_codspeed-2.2.1/src/pytest_codspeed → pytest_codspeed-3.0.0b0/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.0b0
|
|
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,29 @@ 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
38
|
Classifier: Programming Language :: Python :: 3.8
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.9
|
|
41
40
|
Classifier: Programming Language :: Python :: 3.10
|
|
42
41
|
Classifier: Programming Language :: Python :: 3.11
|
|
43
42
|
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
44
|
Classifier: Topic :: Software Development :: Testing
|
|
45
45
|
Classifier: Topic :: System :: Benchmark
|
|
46
46
|
Classifier: Topic :: Utilities
|
|
47
47
|
Classifier: Typing :: Typed
|
|
48
|
-
Requires-Python: >=3.
|
|
49
|
-
Requires-Dist: cffi>=1.
|
|
48
|
+
Requires-Python: >=3.8
|
|
49
|
+
Requires-Dist: cffi>=1.17.1
|
|
50
50
|
Requires-Dist: filelock>=3.12.2
|
|
51
|
+
Requires-Dist: importlib-metadata>=8.5.0; python_version < '3.10'
|
|
51
52
|
Requires-Dist: pytest>=3.8
|
|
53
|
+
Requires-Dist: rich>=13.8.1
|
|
52
54
|
Requires-Dist: setuptools; python_full_version >= '3.12.0'
|
|
53
55
|
Provides-Extra: compat
|
|
54
56
|
Requires-Dist: pytest-benchmark~=4.0.0; extra == 'compat'
|
|
55
|
-
Requires-Dist: pytest-xdist~=
|
|
57
|
+
Requires-Dist: pytest-xdist~=3.6.1; extra == 'compat'
|
|
56
58
|
Provides-Extra: lint
|
|
57
|
-
Requires-Dist: mypy~=1.
|
|
58
|
-
Requires-Dist: ruff~=0.
|
|
59
|
+
Requires-Dist: mypy~=1.11.2; extra == 'lint'
|
|
60
|
+
Requires-Dist: ruff~=0.6.5; extra == 'lint'
|
|
59
61
|
Provides-Extra: test
|
|
60
62
|
Requires-Dist: pytest-cov~=4.0.0; extra == 'test'
|
|
61
63
|
Requires-Dist: pytest~=7.0; extra == 'test'
|
|
@@ -66,7 +68,7 @@ Description-Content-Type: text/markdown
|
|
|
66
68
|
|
|
67
69
|
[](https://github.com/CodSpeedHQ/pytest-codspeed/actions/workflows/ci.yml)
|
|
68
70
|
[](https://pypi.org/project/pytest-codspeed)
|
|
69
|
-

|
|
70
72
|
[](https://discord.com/invite/MxpaCfKSqF)
|
|
71
73
|
[](https://codspeed.io/CodSpeedHQ/pytest-codspeed)
|
|
72
74
|
|
|
@@ -76,7 +78,7 @@ Pytest plugin to create CodSpeed benchmarks
|
|
|
76
78
|
|
|
77
79
|
## Requirements
|
|
78
80
|
|
|
79
|
-
**Python**: 3.
|
|
81
|
+
**Python**: 3.8 and later
|
|
80
82
|
|
|
81
83
|
**pytest**: any recent version
|
|
82
84
|
|
|
@@ -163,14 +165,14 @@ jobs:
|
|
|
163
165
|
benchmarks:
|
|
164
166
|
runs-on: ubuntu-latest
|
|
165
167
|
steps:
|
|
166
|
-
- uses: actions/checkout@
|
|
167
|
-
- uses: actions/setup-python@
|
|
168
|
+
- uses: actions/checkout@v4
|
|
169
|
+
- uses: actions/setup-python@v5
|
|
168
170
|
with:
|
|
169
|
-
python-version: "3.
|
|
171
|
+
python-version: "3.12"
|
|
170
172
|
- name: Install dependencies
|
|
171
173
|
run: pip install -r requirements.txt
|
|
172
174
|
- name: Run benchmarks
|
|
173
|
-
uses: CodSpeedHQ/action@
|
|
175
|
+
uses: CodSpeedHQ/action@v3
|
|
174
176
|
with:
|
|
175
177
|
token: ${{ secrets.CODSPEED_TOKEN }}
|
|
176
178
|
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.8 and later
|
|
17
17
|
|
|
18
18
|
**pytest**: any recent version
|
|
19
19
|
|
|
@@ -100,14 +100,14 @@ jobs:
|
|
|
100
100
|
benchmarks:
|
|
101
101
|
runs-on: ubuntu-latest
|
|
102
102
|
steps:
|
|
103
|
-
- uses: actions/checkout@
|
|
104
|
-
- uses: actions/setup-python@
|
|
103
|
+
- uses: actions/checkout@v4
|
|
104
|
+
- uses: actions/setup-python@v5
|
|
105
105
|
with:
|
|
106
|
-
python-version: "3.
|
|
106
|
+
python-version: "3.12"
|
|
107
107
|
- name: Install dependencies
|
|
108
108
|
run: pip install -r requirements.txt
|
|
109
109
|
- name: Run benchmarks
|
|
110
|
-
uses: CodSpeedHQ/action@
|
|
110
|
+
uses: CodSpeedHQ/action@v3
|
|
111
111
|
with:
|
|
112
112
|
token: ${{ secrets.CODSPEED_TOKEN }}
|
|
113
113
|
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.8"
|
|
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
26
|
"Programming Language :: Python :: 3.8",
|
|
28
27
|
"Programming Language :: Python :: 3.9",
|
|
29
28
|
"Programming Language :: Python :: 3.10",
|
|
30
29
|
"Programming Language :: Python :: 3.11",
|
|
31
30
|
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
32
|
"Topic :: Software Development :: Testing",
|
|
33
33
|
"Topic :: System :: Benchmark",
|
|
34
34
|
"Topic :: Utilities",
|
|
35
35
|
"Typing :: Typed",
|
|
36
36
|
]
|
|
37
37
|
dependencies = [
|
|
38
|
-
"cffi >= 1.
|
|
38
|
+
"cffi >= 1.17.1",
|
|
39
39
|
# cffi doesn't automatically install setuptools with python 3.12+
|
|
40
40
|
# cf https://github.com/python-cffi/cffi/releases/tag/v1.16.0
|
|
41
41
|
"setuptools; python_full_version >= '3.12.0'",
|
|
42
42
|
"pytest>=3.8",
|
|
43
43
|
"filelock >= 3.12.2",
|
|
44
|
+
"rich>=13.8.1",
|
|
45
|
+
"importlib-metadata>=8.5.0; python_version < '3.10'",
|
|
44
46
|
]
|
|
45
47
|
|
|
46
48
|
[project.optional-dependencies]
|
|
47
|
-
lint = ["mypy ~= 1.
|
|
48
|
-
compat = [
|
|
49
|
+
lint = ["mypy ~= 1.11.2", "ruff ~= 0.6.5"]
|
|
50
|
+
compat = [
|
|
51
|
+
"pytest-benchmark ~= 4.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.8", "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"]
|
|
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 __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 {__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,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict, dataclass
|
|
4
|
+
from math import ceil
|
|
5
|
+
from statistics import mean, median, 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.table import Table
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from pytest_codspeed.instruments import Instrument
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
|
|
18
|
+
from pytest import Session
|
|
19
|
+
|
|
20
|
+
from pytest_codspeed.instruments import P, T
|
|
21
|
+
from pytest_codspeed.plugin import CodSpeedConfig
|
|
22
|
+
|
|
23
|
+
DEFAULT_WARMUP_TIME_NS = 1_000_000_000
|
|
24
|
+
DEFAULT_MAX_TIME_NS = 3_000_000_000
|
|
25
|
+
TIMER_RESOLUTION_NS = get_clock_info("perf_counter").resolution * 1e9
|
|
26
|
+
DEFAULT_MIN_ROUND_TIME_NS = TIMER_RESOLUTION_NS * 100
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class BenchmarkConfig:
|
|
31
|
+
warmup_time_ns: int
|
|
32
|
+
min_round_time_ns: float
|
|
33
|
+
max_time_ns: int
|
|
34
|
+
max_rounds: int | None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def from_codspeed_config(cls, config: CodSpeedConfig) -> BenchmarkConfig:
|
|
38
|
+
return cls(
|
|
39
|
+
warmup_time_ns=config.warmup_time_ns
|
|
40
|
+
if config.warmup_time_ns is not None
|
|
41
|
+
else DEFAULT_WARMUP_TIME_NS,
|
|
42
|
+
min_round_time_ns=DEFAULT_MIN_ROUND_TIME_NS,
|
|
43
|
+
max_time_ns=DEFAULT_MAX_TIME_NS,
|
|
44
|
+
max_rounds=config.max_rounds or None,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class BenchmarkStats:
|
|
50
|
+
min_ns: float
|
|
51
|
+
max_ns: float
|
|
52
|
+
stdev_ns: float
|
|
53
|
+
mean_ns: float
|
|
54
|
+
median_ns: float
|
|
55
|
+
|
|
56
|
+
rounds: int
|
|
57
|
+
total_time: float
|
|
58
|
+
outlier_rounds: int
|
|
59
|
+
iter_per_round: int
|
|
60
|
+
warmup_iters: int
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_list(
|
|
64
|
+
cls,
|
|
65
|
+
times_ns: list[float],
|
|
66
|
+
*,
|
|
67
|
+
rounds: int,
|
|
68
|
+
iter_per_round: int,
|
|
69
|
+
warmup_iters: int,
|
|
70
|
+
total_time: float,
|
|
71
|
+
) -> BenchmarkStats:
|
|
72
|
+
stdev_ns = stdev(times_ns) if len(times_ns) > 1 else 0
|
|
73
|
+
mean_ns = mean(times_ns)
|
|
74
|
+
return cls(
|
|
75
|
+
min_ns=min(times_ns),
|
|
76
|
+
max_ns=max(times_ns),
|
|
77
|
+
stdev_ns=stdev_ns,
|
|
78
|
+
mean_ns=mean_ns,
|
|
79
|
+
median_ns=median(times_ns),
|
|
80
|
+
rounds=rounds,
|
|
81
|
+
total_time=total_time,
|
|
82
|
+
outlier_rounds=sum(1 for t in times_ns if abs(t - mean_ns) > stdev_ns),
|
|
83
|
+
iter_per_round=iter_per_round,
|
|
84
|
+
warmup_iters=warmup_iters,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Benchmark:
|
|
90
|
+
name: str
|
|
91
|
+
uri: str
|
|
92
|
+
|
|
93
|
+
config: BenchmarkConfig
|
|
94
|
+
stats: BenchmarkStats
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_benchmark(
|
|
98
|
+
name: str, uri: str, fn: Callable[P, T], args, kwargs, config: BenchmarkConfig
|
|
99
|
+
) -> tuple[Benchmark, T]:
|
|
100
|
+
# Compute the actual result of the function
|
|
101
|
+
out = fn(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
# Warmup
|
|
104
|
+
times_ns: list[float] = []
|
|
105
|
+
warmup_start = start = perf_counter_ns()
|
|
106
|
+
while True:
|
|
107
|
+
start = perf_counter_ns()
|
|
108
|
+
fn(*args, **kwargs)
|
|
109
|
+
end = perf_counter_ns()
|
|
110
|
+
times_ns.append(end - start)
|
|
111
|
+
if end - warmup_start > config.warmup_time_ns:
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
# Round sizing
|
|
115
|
+
warmup_mean_ns = mean(times_ns)
|
|
116
|
+
warmup_iters = len(times_ns)
|
|
117
|
+
times_ns.clear()
|
|
118
|
+
iter_per_round = (
|
|
119
|
+
int(ceil(config.min_round_time_ns / warmup_mean_ns))
|
|
120
|
+
if warmup_mean_ns <= config.min_round_time_ns
|
|
121
|
+
else 1
|
|
122
|
+
)
|
|
123
|
+
if config.max_rounds is None:
|
|
124
|
+
round_time_ns = warmup_mean_ns * iter_per_round
|
|
125
|
+
rounds = int(config.max_time_ns / round_time_ns)
|
|
126
|
+
else:
|
|
127
|
+
rounds = config.max_rounds
|
|
128
|
+
|
|
129
|
+
# Benchmark
|
|
130
|
+
iter_range = range(iter_per_round)
|
|
131
|
+
run_start = perf_counter_ns()
|
|
132
|
+
for _ in range(rounds):
|
|
133
|
+
start = perf_counter_ns()
|
|
134
|
+
for _ in iter_range:
|
|
135
|
+
fn(*args, **kwargs)
|
|
136
|
+
end = perf_counter_ns()
|
|
137
|
+
times_ns.append(end - start)
|
|
138
|
+
|
|
139
|
+
if end - run_start > config.max_time_ns:
|
|
140
|
+
# TODO: log something
|
|
141
|
+
break
|
|
142
|
+
benchmark_end = perf_counter_ns()
|
|
143
|
+
total_time = (benchmark_end - run_start) / 1e9
|
|
144
|
+
|
|
145
|
+
stats = BenchmarkStats.from_list(
|
|
146
|
+
times_ns,
|
|
147
|
+
rounds=rounds,
|
|
148
|
+
total_time=total_time,
|
|
149
|
+
iter_per_round=iter_per_round,
|
|
150
|
+
warmup_iters=warmup_iters,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return Benchmark(name=name, uri=uri, config=config, stats=stats), out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class WallTimeInstrument(Instrument):
|
|
157
|
+
instrument = "walltime"
|
|
158
|
+
|
|
159
|
+
def __init__(self, config: CodSpeedConfig) -> None:
|
|
160
|
+
self.config = config
|
|
161
|
+
self.benchmarks: list[Benchmark] = []
|
|
162
|
+
|
|
163
|
+
def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
|
|
164
|
+
return f"mode: walltime, timer_resolution: {TIMER_RESOLUTION_NS:.1f}ns", []
|
|
165
|
+
|
|
166
|
+
def measure(
|
|
167
|
+
self,
|
|
168
|
+
name: str,
|
|
169
|
+
uri: str,
|
|
170
|
+
fn: Callable[P, T],
|
|
171
|
+
*args: P.args,
|
|
172
|
+
**kwargs: P.kwargs,
|
|
173
|
+
) -> T:
|
|
174
|
+
bench, out = run_benchmark(
|
|
175
|
+
name=name,
|
|
176
|
+
uri=uri,
|
|
177
|
+
fn=fn,
|
|
178
|
+
args=args,
|
|
179
|
+
kwargs=kwargs,
|
|
180
|
+
config=BenchmarkConfig.from_codspeed_config(self.config),
|
|
181
|
+
)
|
|
182
|
+
self.benchmarks.append(bench)
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
def report(self, session: Session) -> None:
|
|
186
|
+
reporter = session.config.pluginmanager.get_plugin("terminalreporter")
|
|
187
|
+
|
|
188
|
+
if len(self.benchmarks) == 0:
|
|
189
|
+
reporter.write_sep(
|
|
190
|
+
"=",
|
|
191
|
+
f"{len(self.benchmarks)} benchmarked",
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
self._print_benchmark_table()
|
|
195
|
+
reporter.write_sep(
|
|
196
|
+
"=",
|
|
197
|
+
f"{len(self.benchmarks)} benchmarked",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def _print_benchmark_table(self) -> None:
|
|
201
|
+
table = Table(title="Benchmark Results")
|
|
202
|
+
|
|
203
|
+
table.add_column("Benchmark", justify="right", style="cyan", no_wrap=True)
|
|
204
|
+
table.add_column("Time (best)", justify="right", style="green bold")
|
|
205
|
+
table.add_column(
|
|
206
|
+
"Rel. StdDev",
|
|
207
|
+
justify="right",
|
|
208
|
+
)
|
|
209
|
+
table.add_column("Run time", justify="right")
|
|
210
|
+
table.add_column("Iters", justify="right")
|
|
211
|
+
|
|
212
|
+
for bench in self.benchmarks:
|
|
213
|
+
rsd = bench.stats.stdev_ns / bench.stats.mean_ns
|
|
214
|
+
rsd_text = Text(f"{rsd*100:.1f}%")
|
|
215
|
+
if rsd > 0.1:
|
|
216
|
+
rsd_text.stylize("red bold")
|
|
217
|
+
table.add_row(
|
|
218
|
+
bench.name,
|
|
219
|
+
f"{bench.stats.min_ns/bench.stats.iter_per_round:,.0f}ns",
|
|
220
|
+
rsd_text,
|
|
221
|
+
f"{bench.stats.total_time:,.2f}s",
|
|
222
|
+
f"{bench.stats.iter_per_round * bench.stats.rounds:,}",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
console = Console()
|
|
226
|
+
print("\n")
|
|
227
|
+
console.print(table)
|
|
228
|
+
|
|
229
|
+
def get_result_dict(self) -> dict[str, Any]:
|
|
230
|
+
return {
|
|
231
|
+
"instrument": {
|
|
232
|
+
"type": self.instrument,
|
|
233
|
+
"clock_info": get_clock_info("perf_counter").__dict__,
|
|
234
|
+
},
|
|
235
|
+
"benchmarks": [asdict(bench) for bench in self.benchmarks],
|
|
236
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import gc
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from time import time
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from _pytest.fixtures import FixtureManager
|
|
15
|
+
|
|
16
|
+
from pytest_codspeed.instruments import (
|
|
17
|
+
MeasurementMode,
|
|
18
|
+
get_instrument_from_mode,
|
|
19
|
+
)
|
|
20
|
+
from pytest_codspeed.utils import (
|
|
21
|
+
get_environment_metadata,
|
|
22
|
+
get_git_relative_uri_and_name,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
from . import __version__
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from typing import Callable, ParamSpec, TypeVar
|
|
29
|
+
|
|
30
|
+
from pytest_codspeed.instruments import Instrument
|
|
31
|
+
|
|
32
|
+
T = TypeVar("T")
|
|
33
|
+
P = ParamSpec("P")
|
|
34
|
+
|
|
35
|
+
IS_PYTEST_BENCHMARK_INSTALLED = importlib.util.find_spec("pytest_benchmark") is not None
|
|
36
|
+
IS_PYTEST_SPEED_INSTALLED = importlib.util.find_spec("pytest_speed") is not None
|
|
37
|
+
BEFORE_PYTEST_8_1_1 = pytest.version_tuple < (8, 1, 1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.hookimpl(trylast=True)
|
|
41
|
+
def pytest_addoption(parser: pytest.Parser):
|
|
42
|
+
group = parser.getgroup("CodSpeed benchmarking")
|
|
43
|
+
group.addoption(
|
|
44
|
+
"--codspeed",
|
|
45
|
+
action="store_true",
|
|
46
|
+
default=False,
|
|
47
|
+
help="Enable codspeed (not required when using the CodSpeed action)",
|
|
48
|
+
)
|
|
49
|
+
group.addoption(
|
|
50
|
+
"--codspeed-mode",
|
|
51
|
+
action="store",
|
|
52
|
+
choices=[mode.value for mode in MeasurementMode],
|
|
53
|
+
help="The measurement tool to use for measuring performance",
|
|
54
|
+
)
|
|
55
|
+
group.addoption(
|
|
56
|
+
"--codspeed-warmup-time",
|
|
57
|
+
action="store",
|
|
58
|
+
type=float,
|
|
59
|
+
help=(
|
|
60
|
+
"The time to warm up the benchmark for (in seconds), "
|
|
61
|
+
"only for walltime mode"
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
group.addoption(
|
|
65
|
+
"--codspeed-max-rounds",
|
|
66
|
+
action="store",
|
|
67
|
+
type=int,
|
|
68
|
+
help=(
|
|
69
|
+
"The maximum number of rounds to run a benchmark for"
|
|
70
|
+
", only for walltime mode"
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class CodSpeedConfig:
|
|
77
|
+
warmup_time_ns: int | None = None
|
|
78
|
+
max_rounds: int | None = None
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_pytest_config(cls, config: pytest.Config) -> CodSpeedConfig:
|
|
82
|
+
warmup_time = config.getoption("--codspeed-warmup-time", None)
|
|
83
|
+
warmup_time_ns = (
|
|
84
|
+
int(warmup_time * 1_000_000_000) if warmup_time is not None else None
|
|
85
|
+
)
|
|
86
|
+
return cls(
|
|
87
|
+
warmup_time_ns=warmup_time_ns,
|
|
88
|
+
max_rounds=config.getoption("--codspeed-max-rounds", None),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(unsafe_hash=True)
|
|
93
|
+
class CodSpeedPlugin:
|
|
94
|
+
is_codspeed_enabled: bool
|
|
95
|
+
mode: MeasurementMode
|
|
96
|
+
instrument: Instrument
|
|
97
|
+
config: CodSpeedConfig
|
|
98
|
+
disabled_plugins: tuple[str, ...]
|
|
99
|
+
profile_folder: Path | None
|
|
100
|
+
benchmark_count: int = field(default=0, hash=False, compare=False)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
PLUGIN_NAME = "codspeed_plugin"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_plugin(config: pytest.Config) -> CodSpeedPlugin:
|
|
107
|
+
return config.pluginmanager.get_plugin(PLUGIN_NAME)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.hookimpl(tryfirst=True)
|
|
111
|
+
def pytest_configure(config: pytest.Config):
|
|
112
|
+
config.addinivalue_line(
|
|
113
|
+
"markers", "codspeed_benchmark: mark an entire test for codspeed benchmarking"
|
|
114
|
+
)
|
|
115
|
+
config.addinivalue_line(
|
|
116
|
+
"markers", "benchmark: mark an entire test for codspeed benchmarking"
|
|
117
|
+
)
|
|
118
|
+
is_codspeed_enabled = (
|
|
119
|
+
config.getoption("--codspeed") or os.environ.get("CODSPEED_ENV") is not None
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if os.environ.get("CODSPEED_ENV") is not None:
|
|
123
|
+
if os.environ.get("CODSPEED_RUNNER_MODE") == "walltime":
|
|
124
|
+
default_mode = MeasurementMode.WallTime.value
|
|
125
|
+
else:
|
|
126
|
+
default_mode = MeasurementMode.Instrumentation.value
|
|
127
|
+
else:
|
|
128
|
+
default_mode = MeasurementMode.WallTime.value
|
|
129
|
+
|
|
130
|
+
mode = MeasurementMode(config.getoption("--codspeed-mode", None) or default_mode)
|
|
131
|
+
instrument = get_instrument_from_mode(mode)
|
|
132
|
+
disabled_plugins: list[str] = []
|
|
133
|
+
if is_codspeed_enabled:
|
|
134
|
+
if IS_PYTEST_BENCHMARK_INSTALLED:
|
|
135
|
+
# Disable pytest-benchmark
|
|
136
|
+
object.__setattr__(config.option, "benchmark_disable", True)
|
|
137
|
+
config.pluginmanager.set_blocked("pytest_benchmark")
|
|
138
|
+
config.pluginmanager.set_blocked("pytest-benchmark")
|
|
139
|
+
disabled_plugins.append("pytest-benchmark")
|
|
140
|
+
if IS_PYTEST_SPEED_INSTALLED:
|
|
141
|
+
# Disable pytest-speed
|
|
142
|
+
config.pluginmanager.set_blocked("speed")
|
|
143
|
+
disabled_plugins.append("pytest-speed")
|
|
144
|
+
|
|
145
|
+
profile_folder = os.environ.get("CODSPEED_PROFILE_FOLDER")
|
|
146
|
+
|
|
147
|
+
codspeedconfig = CodSpeedConfig.from_pytest_config(config)
|
|
148
|
+
|
|
149
|
+
plugin = CodSpeedPlugin(
|
|
150
|
+
disabled_plugins=tuple(disabled_plugins),
|
|
151
|
+
is_codspeed_enabled=is_codspeed_enabled,
|
|
152
|
+
mode=mode,
|
|
153
|
+
instrument=instrument(codspeedconfig),
|
|
154
|
+
config=codspeedconfig,
|
|
155
|
+
profile_folder=Path(profile_folder) if profile_folder else None,
|
|
156
|
+
)
|
|
157
|
+
config.pluginmanager.register(plugin, PLUGIN_NAME)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.hookimpl()
|
|
161
|
+
def pytest_plugin_registered(plugin, manager: pytest.PytestPluginManager):
|
|
162
|
+
"""
|
|
163
|
+
Patch the benchmark fixture to use the codspeed one if codspeed is enabled and an
|
|
164
|
+
alternative benchmark fixture is available
|
|
165
|
+
"""
|
|
166
|
+
if (IS_PYTEST_BENCHMARK_INSTALLED or IS_PYTEST_SPEED_INSTALLED) and isinstance(
|
|
167
|
+
plugin, FixtureManager
|
|
168
|
+
):
|
|
169
|
+
fixture_manager = plugin
|
|
170
|
+
codspeed_plugin: CodSpeedPlugin = manager.get_plugin(PLUGIN_NAME)
|
|
171
|
+
if codspeed_plugin.is_codspeed_enabled:
|
|
172
|
+
codspeed_benchmark_fixtures = plugin.getfixturedefs(
|
|
173
|
+
"codspeed_benchmark",
|
|
174
|
+
fixture_manager.session.nodeid
|
|
175
|
+
if BEFORE_PYTEST_8_1_1
|
|
176
|
+
else fixture_manager.session,
|
|
177
|
+
)
|
|
178
|
+
assert codspeed_benchmark_fixtures is not None
|
|
179
|
+
# Archive the alternative benchmark fixture
|
|
180
|
+
fixture_manager._arg2fixturedefs["__benchmark"] = (
|
|
181
|
+
fixture_manager._arg2fixturedefs["benchmark"]
|
|
182
|
+
)
|
|
183
|
+
# Replace the alternative fixture with the codspeed one
|
|
184
|
+
fixture_manager._arg2fixturedefs["benchmark"] = codspeed_benchmark_fixtures
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.hookimpl(trylast=True)
|
|
188
|
+
def pytest_report_header(config: pytest.Config):
|
|
189
|
+
plugin = get_plugin(config)
|
|
190
|
+
config_str, warns = plugin.instrument.get_instrument_config_str_and_warns()
|
|
191
|
+
out = [
|
|
192
|
+
(
|
|
193
|
+
f"codspeed: {__version__} ("
|
|
194
|
+
f"{'enabled' if plugin.is_codspeed_enabled else 'disabled'}, {config_str}"
|
|
195
|
+
")"
|
|
196
|
+
),
|
|
197
|
+
*warns,
|
|
198
|
+
]
|
|
199
|
+
if len(plugin.disabled_plugins) > 0:
|
|
200
|
+
out.append(
|
|
201
|
+
"\033[93mCodSpeed had to disable the following plugins: "
|
|
202
|
+
f"{', '.join(plugin.disabled_plugins)}\033[0m"
|
|
203
|
+
)
|
|
204
|
+
return "\n".join(out)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def has_benchmark_fixture(item: pytest.Item) -> bool:
|
|
208
|
+
item_fixtures = getattr(item, "fixturenames", [])
|
|
209
|
+
return "benchmark" in item_fixtures or "codspeed_benchmark" in item_fixtures
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def has_benchmark_marker(item: pytest.Item) -> bool:
|
|
213
|
+
return (
|
|
214
|
+
item.get_closest_marker("codspeed_benchmark") is not None
|
|
215
|
+
or item.get_closest_marker("benchmark") is not None
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def should_benchmark_item(item: pytest.Item) -> bool:
|
|
220
|
+
return has_benchmark_fixture(item) or has_benchmark_marker(item)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@pytest.hookimpl(trylast=True)
|
|
224
|
+
def pytest_collection_modifyitems(
|
|
225
|
+
session: pytest.Session, config: pytest.Config, items: list[pytest.Item]
|
|
226
|
+
):
|
|
227
|
+
"""Filter out items that should not be benchmarked when codspeed is enabled"""
|
|
228
|
+
plugin = get_plugin(config)
|
|
229
|
+
if plugin.is_codspeed_enabled:
|
|
230
|
+
deselected = []
|
|
231
|
+
selected = []
|
|
232
|
+
for item in items:
|
|
233
|
+
if should_benchmark_item(item):
|
|
234
|
+
selected.append(item)
|
|
235
|
+
else:
|
|
236
|
+
deselected.append(item)
|
|
237
|
+
config.hook.pytest_deselected(items=deselected)
|
|
238
|
+
items[:] = selected
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _measure(
|
|
242
|
+
plugin: CodSpeedPlugin,
|
|
243
|
+
nodeid: str,
|
|
244
|
+
config: pytest.Config,
|
|
245
|
+
fn: Callable[P, T],
|
|
246
|
+
*args: P.args,
|
|
247
|
+
**kwargs: P.kwargs,
|
|
248
|
+
) -> T:
|
|
249
|
+
is_gc_enabled = gc.isenabled()
|
|
250
|
+
if is_gc_enabled:
|
|
251
|
+
gc.collect()
|
|
252
|
+
gc.disable()
|
|
253
|
+
try:
|
|
254
|
+
uri, name = get_git_relative_uri_and_name(nodeid, config.rootpath)
|
|
255
|
+
return plugin.instrument.measure(name, uri, fn, *args, **kwargs)
|
|
256
|
+
finally:
|
|
257
|
+
# Ensure GC is re-enabled even if the test failed
|
|
258
|
+
if is_gc_enabled:
|
|
259
|
+
gc.enable()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def wrap_runtest(
|
|
263
|
+
plugin: CodSpeedPlugin,
|
|
264
|
+
nodeid: str,
|
|
265
|
+
config: pytest.Config,
|
|
266
|
+
fn: Callable[P, T],
|
|
267
|
+
) -> Callable[P, T]:
|
|
268
|
+
@functools.wraps(fn)
|
|
269
|
+
def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
|
|
270
|
+
return _measure(plugin, nodeid, config, fn, *args, **kwargs)
|
|
271
|
+
|
|
272
|
+
return wrapped
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@pytest.hookimpl(tryfirst=True)
|
|
276
|
+
def pytest_runtest_protocol(item: pytest.Item, nextitem: pytest.Item | None):
|
|
277
|
+
plugin = get_plugin(item.config)
|
|
278
|
+
if not plugin.is_codspeed_enabled or not should_benchmark_item(item):
|
|
279
|
+
# Defer to the default test protocol since no benchmarking is needed
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
if has_benchmark_fixture(item):
|
|
283
|
+
# Instrumentation is handled by the fixture
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
# Wrap runtest and defer to default protocol
|
|
287
|
+
item.runtest = wrap_runtest(plugin, item.nodeid, item.config, item.runtest)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@pytest.hookimpl()
|
|
292
|
+
def pytest_sessionfinish(session: pytest.Session, exitstatus):
|
|
293
|
+
plugin = get_plugin(session.config)
|
|
294
|
+
if plugin.is_codspeed_enabled:
|
|
295
|
+
plugin.instrument.report(session)
|
|
296
|
+
if plugin.profile_folder:
|
|
297
|
+
result_path = plugin.profile_folder / "results" / f"{os.getpid()}.json"
|
|
298
|
+
else:
|
|
299
|
+
result_path = (
|
|
300
|
+
session.config.rootpath / f".codspeed/results_{time() * 1000:.0f}.json"
|
|
301
|
+
)
|
|
302
|
+
data = {**get_environment_metadata(), **plugin.instrument.get_result_dict()}
|
|
303
|
+
result_path.parent.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
result_path.write_text(json.dumps(data, indent=2))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class BenchmarkFixture:
|
|
308
|
+
"""The fixture that can be used to benchmark a function."""
|
|
309
|
+
|
|
310
|
+
def __init__(self, request: pytest.FixtureRequest):
|
|
311
|
+
self.extra_info: dict = {}
|
|
312
|
+
|
|
313
|
+
self._request = request
|
|
314
|
+
|
|
315
|
+
def __call__(self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
|
|
316
|
+
config = self._request.config
|
|
317
|
+
plugin = get_plugin(config)
|
|
318
|
+
if plugin.is_codspeed_enabled:
|
|
319
|
+
return _measure(
|
|
320
|
+
plugin, self._request.node.nodeid, config, func, *args, **kwargs
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
return func(*args, **kwargs)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@pytest.fixture(scope="function")
|
|
327
|
+
def codspeed_benchmark(request: pytest.FixtureRequest) -> Callable:
|
|
328
|
+
return BenchmarkFixture(request)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if not IS_PYTEST_BENCHMARK_INSTALLED:
|
|
332
|
+
|
|
333
|
+
@pytest.fixture(scope="function")
|
|
334
|
+
def benchmark(codspeed_benchmark, request: pytest.FixtureRequest):
|
|
335
|
+
"""
|
|
336
|
+
Compatibility with pytest-benchmark
|
|
337
|
+
"""
|
|
338
|
+
return codspeed_benchmark
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import sysconfig
|
|
1
6
|
from pathlib import Path
|
|
2
7
|
|
|
8
|
+
from pytest_codspeed import __version__
|
|
9
|
+
|
|
10
|
+
if sys.version_info < (3, 10):
|
|
11
|
+
import importlib_metadata as importlib_metadata
|
|
12
|
+
else:
|
|
13
|
+
import importlib.metadata as importlib_metadata
|
|
14
|
+
|
|
3
15
|
|
|
4
16
|
def get_git_relative_path(abs_path: Path) -> Path:
|
|
5
17
|
"""Get the path relative to the git root directory. If the path is not
|
|
@@ -15,11 +27,11 @@ def get_git_relative_path(abs_path: Path) -> Path:
|
|
|
15
27
|
return abs_path
|
|
16
28
|
|
|
17
29
|
|
|
18
|
-
def
|
|
19
|
-
"""Get the benchmark uri relative to the git root dir.
|
|
30
|
+
def get_git_relative_uri_and_name(nodeid: str, pytest_rootdir: Path) -> tuple[str, str]:
|
|
31
|
+
"""Get the benchmark uri relative to the git root dir and the benchmark name.
|
|
20
32
|
|
|
21
33
|
Args:
|
|
22
|
-
|
|
34
|
+
nodeid (str): the pytest nodeid, for example:
|
|
23
35
|
testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source
|
|
24
36
|
pytest_rootdir (str): the pytest root dir, for example:
|
|
25
37
|
/home/user/gitrepo/folder
|
|
@@ -29,7 +41,23 @@ def get_git_relative_uri(uri: str, pytest_rootdir: Path) -> str:
|
|
|
29
41
|
folder/testing/test_excinfo.py::TestFormattedExcinfo::test_repr_source
|
|
30
42
|
|
|
31
43
|
"""
|
|
32
|
-
file_path,
|
|
44
|
+
file_path, bench_name = nodeid.split("::", 1)
|
|
33
45
|
absolute_file_path = pytest_rootdir / Path(file_path)
|
|
34
46
|
relative_git_path = get_git_relative_path(absolute_file_path)
|
|
35
|
-
return f"{str(relative_git_path)}::{
|
|
47
|
+
return (f"{str(relative_git_path)}::{bench_name}", bench_name)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_environment_metadata() -> dict[str, dict]:
|
|
51
|
+
return {
|
|
52
|
+
"creator": {
|
|
53
|
+
"name": "pytest-codspeed",
|
|
54
|
+
"version": __version__,
|
|
55
|
+
"pid": os.getpid(),
|
|
56
|
+
},
|
|
57
|
+
"python": {
|
|
58
|
+
"sysconfig": sysconfig.get_config_vars(),
|
|
59
|
+
"dependencies": {
|
|
60
|
+
d.name: d.version for d in importlib_metadata.distributions()
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
}
|
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import gc
|
|
2
|
-
import os
|
|
3
|
-
import pkgutil
|
|
4
|
-
import sys
|
|
5
|
-
from dataclasses import dataclass, field
|
|
6
|
-
from typing import (
|
|
7
|
-
TYPE_CHECKING,
|
|
8
|
-
Any,
|
|
9
|
-
Callable,
|
|
10
|
-
Dict,
|
|
11
|
-
List,
|
|
12
|
-
Optional,
|
|
13
|
-
Tuple,
|
|
14
|
-
TypeVar,
|
|
15
|
-
Union,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
import pytest
|
|
19
|
-
from _pytest.fixtures import FixtureManager
|
|
20
|
-
|
|
21
|
-
from pytest_codspeed.utils import get_git_relative_uri
|
|
22
|
-
|
|
23
|
-
from . import __version__
|
|
24
|
-
from ._wrapper import get_lib
|
|
25
|
-
|
|
26
|
-
if TYPE_CHECKING:
|
|
27
|
-
from ._wrapper import LibType
|
|
28
|
-
|
|
29
|
-
IS_PYTEST_BENCHMARK_INSTALLED = pkgutil.find_loader("pytest_benchmark") is not None
|
|
30
|
-
SUPPORTS_PERF_TRAMPOLINE = sys.version_info >= (3, 12)
|
|
31
|
-
BEFORE_PYTEST_8_1_1 = pytest.version_tuple < (8, 1, 1)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest.hookimpl(trylast=True)
|
|
35
|
-
def pytest_addoption(parser: "pytest.Parser"):
|
|
36
|
-
group = parser.getgroup("CodSpeed benchmarking")
|
|
37
|
-
group.addoption(
|
|
38
|
-
"--codspeed",
|
|
39
|
-
action="store_true",
|
|
40
|
-
default=False,
|
|
41
|
-
help="Enable codspeed (not required when using the CodSpeed action)",
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
@dataclass(unsafe_hash=True)
|
|
46
|
-
class CodSpeedPlugin:
|
|
47
|
-
is_codspeed_enabled: bool
|
|
48
|
-
should_measure: bool
|
|
49
|
-
lib: Optional["LibType"]
|
|
50
|
-
disabled_plugins: Tuple[str, ...]
|
|
51
|
-
benchmark_count: int = field(default=0, hash=False, compare=False)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
PLUGIN_NAME = "codspeed_plugin"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get_plugin(config: "pytest.Config") -> "CodSpeedPlugin":
|
|
58
|
-
return config.pluginmanager.get_plugin(PLUGIN_NAME)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@pytest.hookimpl(tryfirst=True)
|
|
62
|
-
def pytest_configure(config: "pytest.Config"):
|
|
63
|
-
config.addinivalue_line(
|
|
64
|
-
"markers", "codspeed_benchmark: mark an entire test for codspeed benchmarking"
|
|
65
|
-
)
|
|
66
|
-
config.addinivalue_line(
|
|
67
|
-
"markers", "benchmark: mark an entire test for codspeed benchmarking"
|
|
68
|
-
)
|
|
69
|
-
is_codspeed_enabled = (
|
|
70
|
-
config.getoption("--codspeed") or os.environ.get("CODSPEED_ENV") is not None
|
|
71
|
-
)
|
|
72
|
-
should_measure = os.environ.get("CODSPEED_ENV") is not None
|
|
73
|
-
lib = get_lib() if should_measure else None
|
|
74
|
-
if lib is not None:
|
|
75
|
-
lib.dump_stats_at(f"Metadata: pytest-codspeed {__version__}".encode("ascii"))
|
|
76
|
-
disabled_plugins: List[str] = []
|
|
77
|
-
# Disable pytest-benchmark if codspeed is enabled
|
|
78
|
-
if is_codspeed_enabled and IS_PYTEST_BENCHMARK_INSTALLED:
|
|
79
|
-
object.__setattr__(config.option, "benchmark_disable", True)
|
|
80
|
-
config.pluginmanager.set_blocked("pytest-benchmark")
|
|
81
|
-
disabled_plugins.append("pytest-benchmark")
|
|
82
|
-
|
|
83
|
-
plugin = CodSpeedPlugin(
|
|
84
|
-
is_codspeed_enabled=is_codspeed_enabled,
|
|
85
|
-
should_measure=should_measure,
|
|
86
|
-
lib=lib,
|
|
87
|
-
disabled_plugins=tuple(disabled_plugins),
|
|
88
|
-
)
|
|
89
|
-
config.pluginmanager.register(plugin, PLUGIN_NAME)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def pytest_plugin_registered(plugin, manager: "pytest.PytestPluginManager"):
|
|
93
|
-
"""Patch the benchmark fixture to use the codspeed one if codspeed is enabled"""
|
|
94
|
-
if IS_PYTEST_BENCHMARK_INSTALLED and isinstance(plugin, FixtureManager):
|
|
95
|
-
fixture_manager = plugin
|
|
96
|
-
codspeed_plugin: CodSpeedPlugin = manager.get_plugin(PLUGIN_NAME)
|
|
97
|
-
if codspeed_plugin.is_codspeed_enabled:
|
|
98
|
-
codspeed_benchmark_fixtures = plugin.getfixturedefs(
|
|
99
|
-
"codspeed_benchmark",
|
|
100
|
-
fixture_manager.session.nodeid
|
|
101
|
-
if BEFORE_PYTEST_8_1_1
|
|
102
|
-
else fixture_manager.session,
|
|
103
|
-
)
|
|
104
|
-
assert codspeed_benchmark_fixtures is not None
|
|
105
|
-
# Archive the pytest-benchmark fixture
|
|
106
|
-
fixture_manager._arg2fixturedefs["__benchmark"] = (
|
|
107
|
-
fixture_manager._arg2fixturedefs["benchmark"]
|
|
108
|
-
)
|
|
109
|
-
# Replace the pytest-benchmark fixture with the codspeed one
|
|
110
|
-
fixture_manager._arg2fixturedefs["benchmark"] = codspeed_benchmark_fixtures
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@pytest.hookimpl(trylast=True)
|
|
114
|
-
def pytest_report_header(config: "pytest.Config"):
|
|
115
|
-
out = [
|
|
116
|
-
f"codspeed: {__version__} "
|
|
117
|
-
f"(callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'})"
|
|
118
|
-
]
|
|
119
|
-
plugin = get_plugin(config)
|
|
120
|
-
if plugin.is_codspeed_enabled and not plugin.should_measure:
|
|
121
|
-
out.append(
|
|
122
|
-
"\033[1m"
|
|
123
|
-
"NOTICE: codspeed is enabled, but no performance measurement"
|
|
124
|
-
" will be made since it's running in an unknown environment."
|
|
125
|
-
"\033[0m"
|
|
126
|
-
)
|
|
127
|
-
if len(plugin.disabled_plugins) > 0:
|
|
128
|
-
out.append(
|
|
129
|
-
"\033[93mCodSpeed had to disable the following plugins: "
|
|
130
|
-
f"{', '.join(plugin.disabled_plugins)}\033[0m"
|
|
131
|
-
)
|
|
132
|
-
return "\n".join(out)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def has_benchmark_fixture(item: "pytest.Item") -> bool:
|
|
136
|
-
item_fixtures = getattr(item, "fixturenames", [])
|
|
137
|
-
return "benchmark" in item_fixtures or "codspeed_benchmark" in item_fixtures
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def has_benchmark_marker(item: "pytest.Item") -> bool:
|
|
141
|
-
return (
|
|
142
|
-
item.get_closest_marker("codspeed_benchmark") is not None
|
|
143
|
-
or item.get_closest_marker("benchmark") is not None
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def should_benchmark_item(item: "pytest.Item") -> bool:
|
|
148
|
-
return has_benchmark_fixture(item) or has_benchmark_marker(item)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@pytest.hookimpl()
|
|
152
|
-
def pytest_sessionstart(session: "pytest.Session"):
|
|
153
|
-
plugin = get_plugin(session.config)
|
|
154
|
-
if plugin.is_codspeed_enabled:
|
|
155
|
-
plugin.benchmark_count = 0
|
|
156
|
-
if plugin.should_measure and SUPPORTS_PERF_TRAMPOLINE:
|
|
157
|
-
sys.activate_stack_trampoline("perf") # type: ignore
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
@pytest.hookimpl(trylast=True)
|
|
161
|
-
def pytest_collection_modifyitems(
|
|
162
|
-
session: "pytest.Session", config: "pytest.Config", items: "List[pytest.Item]"
|
|
163
|
-
):
|
|
164
|
-
plugin = get_plugin(config)
|
|
165
|
-
if plugin.is_codspeed_enabled:
|
|
166
|
-
deselected = []
|
|
167
|
-
selected = []
|
|
168
|
-
for item in items:
|
|
169
|
-
if should_benchmark_item(item):
|
|
170
|
-
selected.append(item)
|
|
171
|
-
else:
|
|
172
|
-
deselected.append(item)
|
|
173
|
-
config.hook.pytest_deselected(items=deselected)
|
|
174
|
-
items[:] = selected
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _run_with_instrumentation(
|
|
178
|
-
lib: "LibType",
|
|
179
|
-
nodeId: str,
|
|
180
|
-
config: "pytest.Config",
|
|
181
|
-
fn: Callable[..., Any],
|
|
182
|
-
*args,
|
|
183
|
-
**kwargs,
|
|
184
|
-
):
|
|
185
|
-
is_gc_enabled = gc.isenabled()
|
|
186
|
-
if is_gc_enabled:
|
|
187
|
-
gc.collect()
|
|
188
|
-
gc.disable()
|
|
189
|
-
|
|
190
|
-
result = None
|
|
191
|
-
|
|
192
|
-
def __codspeed_root_frame__():
|
|
193
|
-
nonlocal result
|
|
194
|
-
result = fn(*args, **kwargs)
|
|
195
|
-
|
|
196
|
-
if SUPPORTS_PERF_TRAMPOLINE:
|
|
197
|
-
# Warmup CPython performance map cache
|
|
198
|
-
__codspeed_root_frame__()
|
|
199
|
-
lib.zero_stats()
|
|
200
|
-
lib.start_instrumentation()
|
|
201
|
-
__codspeed_root_frame__()
|
|
202
|
-
lib.stop_instrumentation()
|
|
203
|
-
uri = get_git_relative_uri(nodeId, config.rootpath)
|
|
204
|
-
lib.dump_stats_at(uri.encode("ascii"))
|
|
205
|
-
if is_gc_enabled:
|
|
206
|
-
gc.enable()
|
|
207
|
-
|
|
208
|
-
return result
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
@pytest.hookimpl(tryfirst=True)
|
|
212
|
-
def pytest_runtest_protocol(item: "pytest.Item", nextitem: Union["pytest.Item", None]):
|
|
213
|
-
plugin = get_plugin(item.config)
|
|
214
|
-
if not plugin.is_codspeed_enabled or not should_benchmark_item(item):
|
|
215
|
-
return (
|
|
216
|
-
None # Defer to the default test protocol since no benchmarking is needed
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
if has_benchmark_fixture(item):
|
|
220
|
-
return None # Instrumentation is handled by the fixture
|
|
221
|
-
|
|
222
|
-
plugin.benchmark_count += 1
|
|
223
|
-
if not plugin.should_measure:
|
|
224
|
-
return None # Benchmark counted but will be run in the default protocol
|
|
225
|
-
|
|
226
|
-
# Setup phase
|
|
227
|
-
reports = []
|
|
228
|
-
ihook = item.ihook
|
|
229
|
-
ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location)
|
|
230
|
-
setup_call = pytest.CallInfo.from_call(
|
|
231
|
-
lambda: ihook.pytest_runtest_setup(item=item, nextitem=nextitem), "setup"
|
|
232
|
-
)
|
|
233
|
-
setup_report = ihook.pytest_runtest_makereport(item=item, call=setup_call)
|
|
234
|
-
ihook.pytest_runtest_logreport(report=setup_report)
|
|
235
|
-
reports.append(setup_report)
|
|
236
|
-
# Run phase
|
|
237
|
-
if setup_report.passed and not item.config.getoption("setuponly"):
|
|
238
|
-
assert plugin.lib is not None
|
|
239
|
-
runtest_call = pytest.CallInfo.from_call(
|
|
240
|
-
lambda: _run_with_instrumentation(
|
|
241
|
-
plugin.lib, item.nodeid, item.config, item.runtest
|
|
242
|
-
),
|
|
243
|
-
"call",
|
|
244
|
-
)
|
|
245
|
-
runtest_report = ihook.pytest_runtest_makereport(item=item, call=runtest_call)
|
|
246
|
-
ihook.pytest_runtest_logreport(report=runtest_report)
|
|
247
|
-
reports.append(runtest_report)
|
|
248
|
-
|
|
249
|
-
# Teardown phase
|
|
250
|
-
teardown_call = pytest.CallInfo.from_call(
|
|
251
|
-
lambda: ihook.pytest_runtest_teardown(item=item, nextitem=nextitem), "teardown"
|
|
252
|
-
)
|
|
253
|
-
teardown_report = ihook.pytest_runtest_makereport(item=item, call=teardown_call)
|
|
254
|
-
ihook.pytest_runtest_logreport(report=teardown_report)
|
|
255
|
-
reports.append(teardown_report)
|
|
256
|
-
ihook.pytest_runtest_logfinish(nodeid=item.nodeid, location=item.location)
|
|
257
|
-
|
|
258
|
-
return reports # Deny further protocol hooks execution
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
T = TypeVar("T")
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class BenchmarkFixture:
|
|
265
|
-
"""The fixture that can be used to benchmark a function."""
|
|
266
|
-
|
|
267
|
-
def __init__(self, request: "pytest.FixtureRequest"):
|
|
268
|
-
self.extra_info: Dict = {}
|
|
269
|
-
|
|
270
|
-
self._request = request
|
|
271
|
-
|
|
272
|
-
def __call__(self, func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
|
273
|
-
config = self._request.config
|
|
274
|
-
plugin = get_plugin(config)
|
|
275
|
-
plugin.benchmark_count += 1
|
|
276
|
-
if plugin.is_codspeed_enabled and plugin.should_measure:
|
|
277
|
-
assert plugin.lib is not None
|
|
278
|
-
return _run_with_instrumentation(
|
|
279
|
-
plugin.lib, self._request.node.nodeid, config, func, *args, **kwargs
|
|
280
|
-
)
|
|
281
|
-
else:
|
|
282
|
-
return func(*args, **kwargs)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@pytest.fixture(scope="function")
|
|
286
|
-
def codspeed_benchmark(request: "pytest.FixtureRequest") -> Callable:
|
|
287
|
-
return BenchmarkFixture(request)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if not IS_PYTEST_BENCHMARK_INSTALLED:
|
|
291
|
-
|
|
292
|
-
@pytest.fixture(scope="function")
|
|
293
|
-
def benchmark(codspeed_benchmark, request: "pytest.FixtureRequest"):
|
|
294
|
-
"""
|
|
295
|
-
Compatibility with pytest-benchmark
|
|
296
|
-
"""
|
|
297
|
-
return codspeed_benchmark
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
@pytest.hookimpl()
|
|
301
|
-
def pytest_sessionfinish(session: "pytest.Session", exitstatus):
|
|
302
|
-
plugin = get_plugin(session.config)
|
|
303
|
-
if plugin.is_codspeed_enabled:
|
|
304
|
-
reporter = session.config.pluginmanager.get_plugin("terminalreporter")
|
|
305
|
-
count_suffix = "benchmarked" if plugin.should_measure else "benchmark tested"
|
|
306
|
-
reporter.write_sep(
|
|
307
|
-
"=",
|
|
308
|
-
f"{plugin.benchmark_count} {count_suffix}",
|
|
309
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|