Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,9 @@ directions:
They coexist in one suite. Reach for **pytest-memray** to assert a ceiling, catch
a leak, or see which function allocated; reach for **pytest-benchmem** to track a
measured number over time and gate on a delta. One caveat: memray won't nest two
trackers, so don't run `pytest --memray` and a benchmem fixture on the *same*
test.
trackers, so don't run `pytest --memray` and a benchmem fixture on the *same* test
— if you do, benchmem fails that test with an actionable error rather than a terse
memray one.

## Install

Expand Down
29 changes: 26 additions & 3 deletions src/pytest_benchmem/memray.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,38 @@ def _compute_statistics() -> Callable[[str], Any]:
return compute_statistics


#: Substring of memray's error when a second ``Tracker`` is started while one is active.
#: memray raises a bare ``RuntimeError("No more than one Tracker instance can be active
#: at the same time")``; we match on this to turn it into an actionable message.
_NESTED_TRACKER_MARKER = "more than one Tracker"


def _track_once(action: Action) -> Measurement:
"""One fresh tracker run → a :class:`Measurement` via memray stats."""
"""One fresh tracker run → a :class:`Measurement` via memray stats.

memray allows only one active ``Tracker`` per process, so if one is already
running — most often pytest-memray's ``--memray`` profiling the same test — the
nested ``Tracker`` raises. We translate that into an actionable error rather than
surfacing memray's terse one.
"""
import memray

compute_statistics = _compute_statistics()
with tempfile.TemporaryDirectory(prefix="pytest-benchmem-") as tmp:
out = Path(tmp) / "track.bin" # memray must create the file itself
with memray.Tracker(out):
action()
try:
with memray.Tracker(out):
action()
except RuntimeError as exc:
if _NESTED_TRACKER_MARKER in str(exc):
raise RuntimeError(
"pytest-benchmem couldn't start its memray pass because another memray "
"Tracker is already active — most likely pytest-memray's `--memray` running "
"on the same test. memray allows only one Tracker per process. Run the two "
"on different tests (or different sessions): pytest-memray for whole-test "
"limits/leaks, pytest-benchmem for the benchmarked action's memory."
) from exc
raise
s = compute_statistics(str(out))
return Measurement(
peak_bytes=int(s.peak_memory_allocated),
Expand Down
13 changes: 13 additions & 0 deletions tests/test_memray.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ def test_measure_peak_returns_positive_bytes():
assert peak > 1_000_000 # a list of 1e6 ints is several MB; bytes, not MiB


def test_nested_tracker_raises_actionable_error(tmp_path):
"""If a memray Tracker is already active (e.g. pytest-memray's --memray on the same
test), benchmem's pass re-raises memray's terse error as an actionable one."""
import memray

# outer Tracker stands in for pytest-memray's --memray; it must be active first
with (
memray.Tracker(tmp_path / "outer.bin"),
pytest.raises(RuntimeError, match="another memray Tracker is already active"),
):
measure_memory(lambda: [0] * 1000)


def test_repeats_takes_min_of_n():
# min-of-N: a smaller allocation never reports more than a larger one would.
small = measure_peak(lambda: [0] * 100_000, repeats=3)
Expand Down