Skip to content

partial fix for Qmenu leak#2518

Merged
j9ac9k merged 4 commits intopyqtgraph:masterfrom
pijyoi:qmenu-leak-no-segfault
Nov 3, 2022
Merged

partial fix for Qmenu leak#2518
j9ac9k merged 4 commits intopyqtgraph:masterfrom
pijyoi:qmenu-leak-no-segfault

Conversation

@pijyoi
Copy link
Copy Markdown
Contributor

@pijyoi pijyoi commented Nov 2, 2022

Partially fixes #2497

There are 3 places that leak QMenus. This PR fixes 2 of them. The last one is left unfixed as fixing it causes a CI segfault on PySide6 >= 6.3 on Linux platforms. It is believed that the last leak is masking a latent issue.

The workaround used has changed from #2502. In #2502, "topmenu" was made the C++ parent of "submenu".
It was found that PySide{2,6} binding makes "topmenu" a Python-wrapper parent of "submenu", increasing the refcnt of "submenu", even though "submenu" has no C++ parent.
The workaround here relies on PySide's setParent(None) dropping this refcnt. (Note that the C++ parent was already None to begin with)
The end-result is that the life-state of "submenu" remains the same before and after addMenu, as shown in the output of the following script: refcnt and shiboken.dump output remains the same.

If upstream fixes the leak, then this setParent(None) call should have no effect.

from PySide6 import QtGui, QtWidgets
import shiboken6 as shiboken
import sys

app = QtWidgets.QApplication([])
topmenu = QtWidgets.QMenu()
topmenu.setObjectName("topmenu")
submenu = QtWidgets.QMenu()
submenu.setObjectName("submenu")

print(shiboken.dump(submenu))

print(sys.getrefcount(submenu))
topmenu.addMenu(submenu)
submenu.setParent(None) # let Python do the cleanup
print(sys.getrefcount(submenu))
del topmenu
print(sys.getrefcount(submenu))
print(shiboken.dump(submenu))

Output

C++ address....... PySide6.QtWidgets.QMenu/0000021A1C923C40
hasOwnership...... 1
containsCppWrapper 1
validCppObject.... 1
wasCreatedByPython 1

2
2
2
C++ address....... PySide6.QtWidgets.QMenu/0000021A1C923C40
hasOwnership...... 1
containsCppWrapper 1
validCppObject.... 1
wasCreatedByPython 1

As mentioned, this PR only fixes 2 of the leaks. When running the test scripts in #2497, the rate of memory growth is reduced and only 1 QMenu is leaked.

the existing implementation was to workaround the previous (reverted)
QMenu leak implementation.

the new QMenu leak workaround implementation does not need it.
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Nov 3, 2022

The following script better explains the leak and the workarounds.

from PySide6 import QtWidgets
import shiboken6 as shiboken
import sys

def dump(obj):
    d = dict(
        refcnt=sys.getrefcount(obj),
        creator='Python' if shiboken.createdByPython(obj) else 'C++',
        owner='Python' if shiboken.ownedByPython(obj) else 'C++',
        valid=shiboken.isValid(obj)
    )
    print(d)

app = QtWidgets.QApplication([])

def case1():
    # no problem when a menu creates its own submenu
    topmenu = QtWidgets.QMenu()
    submenu = topmenu.addMenu("submenu")

    dump(submenu)
    del topmenu
    dump(submenu)

    # topmenu was the C++ parent
    assert not shiboken.isValid(submenu)

def case2():
    # PySide{2,6} has a leak here
    topmenu = QtWidgets.QMenu()
    submenu = QtWidgets.QMenu()

    dump(submenu)

    # C++ QMenu::addMenu does not take ownership
    topmenu.addMenu(submenu)

    dump(submenu)
    del topmenu
    dump(submenu)

def workaround1():
    # let C++ handle the life-cycle
    topmenu = QtWidgets.QMenu()
    submenu = QtWidgets.QMenu(topmenu) # specify topmenu as C++ parent

    dump(submenu)

    topmenu.addMenu(submenu)

    dump(submenu)
    del topmenu
    dump(submenu)

    # topmenu was the C++ parent
    assert not shiboken.isValid(submenu)

def workaround2():
    # let Python wrapper handle the life-cycle
    topmenu = QtWidgets.QMenu()
    submenu = QtWidgets.QMenu()

    dump(submenu)

    topmenu.addMenu(submenu)
    submenu.setParent(None)     # workaround

    dump(submenu)
    del topmenu
    dump(submenu)

    # topmenu was not the C++ parent
    assert shiboken.isValid(submenu)


case1()
print()
case2()
print()
workaround1()
print()
workaround2()
print()

Output

{'refcnt': 5, 'creator': 'C++', 'owner': 'C++', 'valid': True}
{'refcnt': 4, 'creator': 'C++', 'owner': 'C++', 'valid': False}

{'refcnt': 4, 'creator': 'Python', 'owner': 'Python', 'valid': True}
{'refcnt': 5, 'creator': 'Python', 'owner': 'C++', 'valid': True}
{'refcnt': 5, 'creator': 'Python', 'owner': 'C++', 'valid': True}

{'refcnt': 5, 'creator': 'Python', 'owner': 'C++', 'valid': True}
{'refcnt': 5, 'creator': 'Python', 'owner': 'C++', 'valid': True}
{'refcnt': 4, 'creator': 'Python', 'owner': 'C++', 'valid': False}

{'refcnt': 4, 'creator': 'Python', 'owner': 'Python', 'valid': True}
{'refcnt': 4, 'creator': 'Python', 'owner': 'Python', 'valid': True}
{'refcnt': 4, 'creator': 'Python', 'owner': 'Python', 'valid': True}

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Nov 3, 2022

@pijyoi that post is brilliant; thanks for making it. the PR LGTM, and issues like these keep making me think that there may be some interest in developing a python qt-bindings code scanner within codeql

Anyway, merging!

@j9ac9k j9ac9k merged commit fbe80e8 into pyqtgraph:master Nov 3, 2022
@pijyoi pijyoi deleted the qmenu-leak-no-segfault branch November 3, 2022 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QMenus not destroyed (memory leak)

2 participants