Skip to content

Commit 5cab6ac

Browse files
committed
feat: support memory profiling
1 parent 26351c2 commit 5cab6ac

9 files changed

Lines changed: 59 additions & 23 deletions

File tree

src/pytest_codspeed/instruments/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class Instrument(metaclass=ABCMeta):
2121
instrument: ClassVar[str]
2222

2323
@abstractmethod
24-
def __init__(self, config: CodSpeedConfig): ...
24+
def __init__(self, config: CodSpeedConfig, mode: MeasurementMode): ...
2525

2626
@abstractmethod
2727
def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]: ...
@@ -57,6 +57,7 @@ def get_result_dict(
5757

5858
class MeasurementMode(str, Enum):
5959
Simulation = "simulation"
60+
Memory = "memory"
6061
WallTime = "walltime"
6162

6263
@classmethod
@@ -68,12 +69,12 @@ def _missing_(cls, value: object):
6869

6970

7071
def get_instrument_from_mode(mode: MeasurementMode) -> type[Instrument]:
71-
from pytest_codspeed.instruments.valgrind import (
72-
ValgrindInstrument,
72+
from pytest_codspeed.instruments.analysis import (
73+
AnalysisInstrument,
7374
)
7475
from pytest_codspeed.instruments.walltime import WallTimeInstrument
7576

76-
if mode == MeasurementMode.Simulation:
77-
return ValgrindInstrument
77+
if mode in (MeasurementMode.Simulation, MeasurementMode.Memory):
78+
return AnalysisInstrument
7879
else:
7980
return WallTimeInstrument

src/pytest_codspeed/instruments/valgrind.py renamed to src/pytest_codspeed/instruments/analysis.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
from pytest_codspeed import __semver_version__
88
from pytest_codspeed.instruments import Instrument
9-
from pytest_codspeed.instruments.hooks import InstrumentHooks
9+
from pytest_codspeed.instruments.hooks import (
10+
FEATURE_DISABLE_CALLGRIND_MARKERS,
11+
InstrumentHooks,
12+
)
1013
from pytest_codspeed.utils import SUPPORTS_PERF_TRAMPOLINE
1114

1215
if TYPE_CHECKING:
@@ -15,31 +18,33 @@
1518
from pytest import Session
1619

1720
from pytest_codspeed.config import PedanticOptions
18-
from pytest_codspeed.instruments import P, T
21+
from pytest_codspeed.instruments import MeasurementMode, P, T
1922
from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig
2023

2124

22-
class ValgrindInstrument(Instrument):
23-
instrument = "valgrind"
25+
class AnalysisInstrument(Instrument):
26+
instrument = "analysis"
2427
instrument_hooks: InstrumentHooks | None
28+
mode: MeasurementMode
2529

26-
def __init__(self, config: CodSpeedConfig) -> None:
30+
def __init__(self, config: CodSpeedConfig, mode: MeasurementMode) -> None:
31+
self.mode = mode
2732
self.benchmark_count = 0
2833
try:
2934
self.instrument_hooks = InstrumentHooks()
3035
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)
3136
except RuntimeError as e:
3237
if os.environ.get("CODSPEED_ENV") is not None:
3338
raise Exception(
34-
"Failed to initialize CPU simulation instrument hooks"
39+
f"Failed to initialize {self.mode.value} instrument hooks"
3540
) from e
3641
self.instrument_hooks = None
3742

3843
self.should_measure = self.instrument_hooks is not None
3944

4045
def get_instrument_config_str_and_warns(self) -> tuple[str, list[str]]:
4146
config = (
42-
f"mode: simulation, "
47+
f"mode: {self.mode.value}, "
4348
f"callgraph: {'enabled' if SUPPORTS_PERF_TRAMPOLINE else 'not supported'}"
4449
)
4550
warnings = []
@@ -73,6 +78,9 @@ def __codspeed_root_frame__() -> T:
7378
# Warmup CPython performance map cache
7479
__codspeed_root_frame__()
7580

81+
self.instrument_hooks.set_feature(FEATURE_DISABLE_CALLGRIND_MARKERS, True)
82+
self.instrument_hooks.start_benchmark()
83+
7684
# Manually call the library function to avoid an extra stack frame. Also
7785
# call the callgrind markers directly to avoid extra overhead.
7886
self.instrument_hooks.lib.callgrind_start_instrumentation()
@@ -81,6 +89,7 @@ def __codspeed_root_frame__() -> T:
8189
finally:
8290
# Ensure instrumentation is stopped even if the test failed
8391
self.instrument_hooks.lib.callgrind_stop_instrumentation()
92+
self.instrument_hooks.stop_benchmark()
8493
self.instrument_hooks.set_executed_benchmark(uri)
8594

8695
def measure_pedantic(
@@ -92,8 +101,8 @@ def measure_pedantic(
92101
) -> T:
93102
if pedantic_options.rounds != 1 or pedantic_options.iterations != 1:
94103
warnings.warn(
95-
"Valgrind instrument ignores rounds and iterations settings "
96-
"in pedantic mode"
104+
f"{self.mode.value.capitalize()} instrument ignores rounds and "
105+
"iterations settings in pedantic mode"
97106
)
98107
if not self.instrument_hooks:
99108
args, kwargs = pedantic_options.setup_and_get_args_kwargs()
@@ -117,11 +126,18 @@ def __codspeed_root_frame__(*args, **kwargs) -> T:
117126

118127
# Compute the actual result of the function
119128
args, kwargs = pedantic_options.setup_and_get_args_kwargs()
129+
130+
self.instrument_hooks.set_feature(FEATURE_DISABLE_CALLGRIND_MARKERS, True)
131+
self.instrument_hooks.start_benchmark()
132+
133+
# Manually call the library function to avoid an extra stack frame. Also
134+
# call the callgrind markers directly to avoid extra overhead.
120135
self.instrument_hooks.lib.callgrind_start_instrumentation()
121136
try:
122137
out = __codspeed_root_frame__(*args, **kwargs)
123138
finally:
124139
self.instrument_hooks.lib.callgrind_stop_instrumentation()
140+
self.instrument_hooks.stop_benchmark()
125141
self.instrument_hooks.set_executed_benchmark(uri)
126142
if pedantic_options.teardown is not None:
127143
pedantic_options.teardown(*args, **kwargs)
@@ -140,5 +156,5 @@ def report(self, session: Session) -> None:
140156
def get_result_dict(self) -> dict[str, Any]:
141157
return {
142158
"instrument": {"type": self.instrument},
143-
# bench results will be dumped by valgrind
159+
# bench results will be dumped by the runner
144160
}

src/pytest_codspeed/instruments/hooks/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
if TYPE_CHECKING:
1111
from .dist_instrument_hooks import InstrumentHooksPointer, LibType
1212

13+
# Feature flags for instrument hooks
14+
FEATURE_DISABLE_CALLGRIND_MARKERS = 0
15+
1316

1417
class InstrumentHooks:
1518
"""Zig library wrapper class providing benchmark measurement functionality."""
@@ -80,3 +83,12 @@ def set_integration(self, name: str, version: str) -> None:
8083
def is_instrumented(self) -> bool:
8184
"""Check if simulation is active."""
8285
return self.lib.instrument_hooks_is_instrumented(self.instance)
86+
87+
def set_feature(self, feature: int, enabled: bool) -> None:
88+
"""Set a feature flag in the instrument hooks library.
89+
90+
Args:
91+
feature: The feature flag to set
92+
enabled: Whether to enable or disable the feature
93+
"""
94+
self.lib.instrument_hooks_set_feature(feature, enabled)

src/pytest_codspeed/instruments/hooks/build.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
3535
void callgrind_start_instrumentation();
3636
void callgrind_stop_instrumentation();
37+
38+
void instrument_hooks_set_feature(uint64_t feature, bool enabled);
3739
""")
3840

3941
ffibuilder.set_source(

src/pytest_codspeed/instruments/hooks/dist_instrument_hooks.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,7 @@ class lib:
2929
def callgrind_start_instrumentation() -> int: ...
3030
@staticmethod
3131
def callgrind_stop_instrumentation() -> int: ...
32+
@staticmethod
33+
def instrument_hooks_set_feature(feature: int, enabled: bool) -> None: ...
3234

3335
LibType = type[lib]

src/pytest_codspeed/instruments/walltime.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from pytest import Session
2525

2626
from pytest_codspeed.config import PedanticOptions
27-
from pytest_codspeed.instruments import P, T
27+
from pytest_codspeed.instruments import MeasurementMode, P, T
2828
from pytest_codspeed.plugin import BenchmarkMarkerOptions, CodSpeedConfig
2929

3030
DEFAULT_WARMUP_TIME_NS = 1_000_000_000
@@ -159,7 +159,7 @@ class WallTimeInstrument(Instrument):
159159
instrument = "walltime"
160160
instrument_hooks: InstrumentHooks | None
161161

162-
def __init__(self, config: CodSpeedConfig) -> None:
162+
def __init__(self, config: CodSpeedConfig, _mode: MeasurementMode) -> None:
163163
try:
164164
self.instrument_hooks = InstrumentHooks()
165165
self.instrument_hooks.set_integration("pytest-codspeed", __semver_version__)

src/pytest_codspeed/plugin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,11 @@ def pytest_configure(config: pytest.Config):
112112
)
113113

114114
if os.environ.get("CODSPEED_ENV") is not None:
115-
if os.environ.get("CODSPEED_RUNNER_MODE") == "walltime":
115+
runner_mode = os.environ.get("CODSPEED_RUNNER_MODE")
116+
if runner_mode == "walltime":
116117
default_mode = MeasurementMode.WallTime.value
118+
elif runner_mode == "memory":
119+
default_mode = MeasurementMode.Memory.value
117120
else:
118121
default_mode = MeasurementMode.Simulation.value
119122
else:
@@ -142,7 +145,7 @@ def pytest_configure(config: pytest.Config):
142145
disabled_plugins=tuple(disabled_plugins),
143146
is_codspeed_enabled=is_codspeed_enabled,
144147
mode=mode,
145-
instrument=instrument(codspeed_config),
148+
instrument=instrument(codspeed_config, mode),
146149
config=codspeed_config,
147150
profile_folder=Path(profile_folder) if profile_folder else None,
148151
)

tests/test_pytest_plugin_cpu_instrumentation.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def fixtured_child():
7676
with open(perf_filepath) as perf_file:
7777
lines = perf_file.readlines()
7878
assert any(
79-
"py::ValgrindInstrument.measure.<locals>.__codspeed_root_frame__" in line
79+
"py::AnalysisInstrument.measure.<locals>.__codspeed_root_frame__" in line
8080
for line in lines
8181
), "No root frame found in perf map"
8282
assert any("py::test_some_addition_marked" in line for line in lines), (
@@ -135,8 +135,8 @@ def foo():
135135
result = run_pytest_codspeed_with_mode(pytester, MeasurementMode.Simulation)
136136
result.stdout.fnmatch_lines(
137137
[
138-
"*UserWarning: Valgrind instrument ignores rounds and iterations settings "
139-
"in pedantic mode*"
138+
"*UserWarning: Simulation instrument ignores rounds and iterations settings"
139+
" in pedantic mode*"
140140
]
141141
)
142142
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)