-
-
Notifications
You must be signed in to change notification settings - Fork 689
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
Refcount mess in mvAddCallback() and mvRunCallback() #2036
Comments
For reference:
The only call that uses this parameter is I bet this parameter can be removed if we fix mvAddCallback so that it always treats all parameters as borrowed; the framebuffer object then can be wrapped into mvPyObject to prevent leaking. |
Update: I've tried to fix this in my local copy, and it seems to work, but the fix still needs quite a bit of testing. I'm going to give it some time and then will open a PR. Unfortunately I don't have time and data to test all the areas the fix touches, like plots, or like frame callbacks; my primary testing will be done on my own project. Maybe somebody else can run more extensive tests in future. |
There is indeed a memory leak for handler callbacks. @Atlamillias suggest one temp workaround within Python itself 👍 import ctypes
Py_DECREF = ctypes.pythonapi.Py_DecRef
Py_DECREF.argtypes = (ctypes.py_object,)
Py_DECREF.restype = None call Reference to discord: |
Version of Dear PyGui
Version: 1.8.0
Operating System: Windows 10
My Issue/Question
I'm adding a custom widget to the C++ part of DPG, in particular trying to schedule a callback from C++ code. It turns out the corresponding DPG functions don't handle reference counters properly for PyObject's passed into them.
Python docs say there are two ways to handle a PyObject reference: either own it (i.e. take away ownership from the caller) or borrow it (the caller holds ownership).
mvAddCallback()
is not very consistent in this sense: themanualCallbacks == true
branch creates its own references (i.e. treats all parameters as borrowed refs) whereas the default (mvSubmitCallback
) branch wants to take ownership.To make matters worse, the code that actually calls
mvAddCallback()
doesn't seem to follow either of the two patterns above. Instead there's a mix of ownership transfer methods:callable
anduser_data
are typically passed in as borrowed references:callable
is usually obtained frommvAppItem::getCallback()
, which returns a reference without INCREF'ing it.user_data
usually comes fromitem.config.user_data
- again withoutPy_INCREF
.app_data
is usually (but not always!) constructed anew on each call, and the reference is not decremented afterwards - as ifmvAddCallback()
was supposed to take ownership.With the current implementation, there's a chance, however small it is, of
callable
oruser_data
getting destroyed while the callback task is waiting in the queue (i.e. between returning frommvAddCallback()
and actual invocation of the callable). This only applies tomanualCallbacks == false
.Also, the
maxNumberOfCalls
branch ofmvAddCallback()
is a different mix of ownership strategies. Luckily that branch is not supposed to run under normal conditions (it even has anassert(false)
in it).Okay, going further, the objects passed to
mvAddCallback()
will eventually end up inmvRunCallback()
, which has a number of its own issues:callable
is a borrowed ref, which means memory leaks (probably on a small scale) ifmanualCallbacks
is true; moreover, ifmvAddCallback()
gets fixed to properly own the callable, all such callbacks will be leaking it even withoutmanualCallbacks
.app_data
oruser_data
are null, they are reasonably replaced withPy_None
; however, thosePy_None
get two INCREFs, and at least one of these INCREFs won't ever be decreased.app_data
anduser_data
as borrowed refs, which is inconsistent with howmvAddCallback()
should be handling them (currently inconsistent with themanualCallbacks
branch only). That is, it might be leaking these objects.callable
is not a real callable (see thePyCallable_Check
condition), it treats these same arguments as owned refs! Well, might be fine as soon asmvAddCallback()
gets fixed; just wanted to point out an inconsistency.count == 2
,count == 1
and the lastelse
branches. As a result,app_data
anduser_data
might be leaking depending on how many arguments the callback function accepts.That is mostly it, except for one little piece of code that has taken a special place in my heart 😍
Not only does
decrementAppData
exist in only one of twomvRunCallback
overloads (and the same formvAddCallback
), it also increments the refcount 😆.To Reproduce
Even with the most frequently running callbacks, like the hover callback, leaks are very small. Also, Python caches frequently used integer values, and when a callback leaks the same UUID every frame, Python just increases refcount for the corresponding (static) integer object.
Leaks themselves are difficult to discover; I can only demonstrate how refcounts grow on certain objects. To see it:
user_data
from lambda parameters and try again - you'll see how None refcount grows faster (2 INCREFs per frame) - this is becausemvRunCallback
goes into thecount==2
branch.Expected behavior
No leaking.
Screenshots/Video
Standalone, minimal, complete and verifiable example
The text was updated successfully, but these errors were encountered: