-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pytest holds on to fixtures too long causing a memory leak #5642
Comments
Just some quick ideas: is this a regression maybe? In that case git-bisect could be useful. |
I updated to pytest 5.0.1 and the result was the same ( |
Is there a pytest version I should give a try to see if this is a regression? I can downgrade and test to see if a bisect would help at all. |
Likely not then - just thought that you've might have noticed this yourself as a regression. |
I'm not seeing a leak here, or at least the memory for that is freed once the fixture is used up The way fixtures work is they are (potentially) reused so a fixture may be called once and used multiple times. What pytest does is store the fixture's yielded / returned value in the when that fixture is no longer needed, the pytest/src/_pytest/fixtures.py Lines 860 to 861 in 13c4b7d
so I think this is not a bug |
here is, for example, a test demoing that the object in fact does not leak: import pytest
import gc
import weakref
class C(object):
def check_app(self):
assert True
print('app checked')
@pytest.fixture
def my_app():
app = C()
yield app
app = weakref.ref(app)
def test_my_app(my_app):
my_app.check_app()
def test2():
obj = next(iter(o for o in gc.get_objects() if isinstance(o, C)), None)
assert obj is None $ pytest t.py -vs
============================= test session starts ==============================
platform linux -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- /tmp/y/venv/bin/python3.7
cachedir: .pytest_cache
rootdir: /tmp/y
collected 2 items
t.py::test_my_app app checked
PASSED
t.py::test2 PASSED
=========================== 2 passed in 0.01 seconds =========================== |
But what about the objgraph image I uploaded? Admittedly, it I don't have example code as I saw it in a larger app. That image was generated in a test subsequent to the test that used the fixture. I.e. the fixture was already done, but terminal reporter seems to have held on to that until all the tests were sone. I saw similar behavior when there are warnings in the logs I'll try to replicate this. |
Also, for the example code I posted, is there some post fixture hook I can use to make sure the fixture has been garbage collected? Rather than having to wait for the next test. E.g. if I wanted to make sure the fixture is not being leaked. |
I tried reading the objgraph but have no idea what I'm looking at and can't ^F -- could you post an svg instead? |
Sorry I'm on a phone now so I cannot crop it, and anyway, the only relevant thing afaict in the giant graph is that the object at the root that ultimately keeps all these other objects alive, which ultimately keeps the app object alive, is a TerminalReporter object. In other words it seems like TerminalReporter keeps the app fixture alive until all the tests are all done. Is it possible those logging object are keeping a ref to the fixtures? I'll see if I can replicate this with a minimal example. |
at least in that case:
not sure there's anything to do here 🤷♂ the only ~slighty improvement would be to tear down the |
As for the warnings: could be collect/stringify them earlier in pytest? It might also be that that's the case already, and |
it's not just pytest internally, they're captured and exposed through this hook: https://pytest.readthedocs.io/en/latest/reference.html#_pytest.hookspec.pytest_warning_captured since this hook is |
Is there any action to be done by pytest here? |
afaik no -- since we need to be able to pass the original warning along to hook implementers (unless we were to deprecate and remove that hook -- but I don't see that happening) |
Yeah, that's what I was thinking too. So I'm closing this as "wont/cant fix" @matham thanks again for the detailed report, but I'm afraid there's not anything that can be done to solve this. |
will |
Is here a way to disable the caching of a @pytest.fixture(autouse=True, scope="function")
def init():
mymod.init_heap() # init GPU context
yield
mymod.free_heap() # free GPU context; double-free once obj fixture is later trying to free
@pytest.fixture(scope="function") # can I disable pytest caching for this yield?
def obj():
v = mymod.Obj()
yield v
del v # still refs remaining |
@ax3l not sure what you're asking -- those fixtures will be collected and destroyed per-test (since you have |
What I am observing is the following:
For that reason, the following runtime logic occurs:
The last line, free, is too late. |
Complete reproducer (please assume # conftest.py
import sys
import numpy as np
import pytest
@pytest.fixture(autouse=True, scope="function")
def init():
yield
@pytest.fixture(scope="function") # can I disable pytest caching for this fixture?
def obj():
v = np.array([1, 2, 3])
print("startup: ", sys.getrefcount(v) - 1)
yield v # can I disable pytest caching for this yield?
print("\nteardown: ", sys.getrefcount(v) - 1)
del v # still refs remaining # test_foo.py
import sys
def test_foo(obj):
print("++ test_foo ++")
print("in test:", sys.getrefcount(obj) - 1) $ python3 -m pytest . -s -vvv
======================================== test session starts ========================================
platform linux -- Python 3.8.10, pytest-7.1.3, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/axel/tmp/pytest-refs
collected 1 item
test_foo.py::test_foo startup: 1
++ test_foo ++
in test: 6
PASSED
teardown: 3
========================================= 1 passed in 0.01s ========================================= |
if you're depending on the garbage collector collecting your objects in order you've probably got bigger problems. python's if you want specific and consistent memory management that's what a context manager is for |
Good point, demonstrator is CPython only where ref counting releases immediately after last ref is freed.
How can one use a context manager with pytest fixtures? |
the easiest is to just: @pytest.fixture
def whatever():
with whatever_context() as obj:
yield obj |
Let's assume this is CPython for a second, where last refs immediately frees. I think pytest still holds on to the # conftest.py
import contextlib
import sys
import numpy as np
import pytest
@contextlib.contextmanager
def init():
v = np.array([1, 2, 3])
print("\nenter: ", sys.getrefcount(v) - 1)
yield v
print("\nexit: ", sys.getrefcount(v) - 1)
del v
@pytest.fixture(scope="function")
def obj():
with init() as v:
print("startup: ", sys.getrefcount(v) - 1)
yield v # can I disable pytest caching for this yield?
print("\nteardown: ", sys.getrefcount(v) - 1)
|
Hm, I guess you want to yield the context manager itself as a fixture, not the objects... |
my point is more that you'd use a context manager to do the allocation and deallocation explicitly -- rather than relying on the garbage collector |
Thanks, got ya. Just to loop back to the original question, the moment we actually hand out objects to pytest: is there a way to deactivate caching in a fixture in pytest? Or is there a recommended way to write generators of objects that are uncached in pytest but still use decorators like |
that sounds like a completely different question -- I'd make a new issue / discussion -- this one has gotten pretty off topic already |
Thanks for your help. I'll move this to #10387 |
Pytest seems to hold on to the object yielded, even after the test is done, sometimes, even until all the tests are done.
The following code:
Results in this:
I tried to investigate using objgraph, and various pytest modules (e.g. warning) were blamed depending on the exact code. When I tested the app after the fixture has fully returned at the next test, then the app seemed to have been released.
However, in a more complex app that I have, pytest seemed to hold on to the app fixture instances forever, untill all the test has finished. I'm attaching what objgraph shows for this case that Terminal reporter is holding on to it.
Tested on Windows10, py3.7.
The text was updated successfully, but these errors were encountered: