Description
We're seeing an issue where the close
method of a QSelectorEventLoop
doesn't completely clean up resources associated to that event loop.
Reproducer
The following code should reproduce:
import gc
import PySide6.QtGui
import qasync
async def app_main():
# Dummy application.
pass
def run_app(qt_app):
event_loop = qasync.QEventLoop(qt_app)
event_loop.run_until_complete(app_main())
event_loop.close()
qt_app = PySide6.QtGui.QGuiApplication()
for _ in range(10):
run_app(qt_app)
gc.collect()
loops = [
obj for obj in gc.get_objects()
if type(obj).__name__ == "QSelectorEventLoop"
]
print("number of event loop objects: ", len(loops))
del loops
When I run the above on my machine, I get:
(qasync) mdickinson@mirzakhani Desktop % python ~/Desktop/qasync_leak.py
number of event loop objects: 1
number of event loop objects: 2
number of event loop objects: 3
number of event loop objects: 4
number of event loop objects: 5
number of event loop objects: 6
number of event loop objects: 7
number of event loop objects: 8
number of event loop objects: 9
number of event loop objects: 10
The expectation / hope was that the number of event loops would be stable.
Diagnosis
Here's a graph showing all the ancestors of one of the leaked QSelectorEventLoop
objects.
Those two lambda
objects at the top have no referrers (in the sense of gc.get_referrers
); they're apparently being kept alive by PySide6. Those lambda
s correspond to these two lines:
I believe that if we add the corresponding signal disconnect
s at event loop close time, then this will fix the above issue.
System details
- Python 3.10 venv
- qasync 0.23.0 installed from PyPI (via
pip
) - PySide6 6.4.2 installed from PyPI (via
pip
) - macOS 12.6.2 on an Intel MacBook Pro
Context
In case you're wondering why anyone would be creating QEventLoop
s repeatedly, the answer is unit testing. We're using qasync
to provide an asyncio event loop for an embedded IPython kernel to use within a Qt-based application. In our test suite we have many tests that create and then tear down that asyncio event loop, and we were observing test interactions as a result of not being able to cleanly clean up all the resources associated to the event loop. (The Qt application itself is a singleton, of course, so we don't try to clean that up between tests.)