-
-
Notifications
You must be signed in to change notification settings - Fork 52
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
Updated memory management #543
base: main
Are you sure you want to change the base?
Conversation
@mhsmith, care to have first look? Please note the TODOs still listed above. |
89a4d62
to
931c352
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This turned out to be a lot less invasive than I thought it would be.
I've done a check of the toga-chart issue that triggered this change; and it seems to resolve that problem.
I also did a quick check with Toga's testbed suite on macOS; that code has a bunch of manual retain
s and autorelease
/release
s. I thought they should all be balanced though - worst case, objects would be over-retained - so I was a little surprised that it the testbed segfaults almost immediately (and the stack trace doesn't give any obvious pointers what is causing the issue).
If I remove all the retains and releases, the testbed segfaults; but on inspection, some of the uses are for objects that are created in Python, then handed to ObjC to manage (e.g., Toolbar items created here), or the copyWithZone
handler here). But I guess those uses of memory handling make sense - and they're a lot closer aligned to the "spirit" of ObjC memory handling, Plus, in at least the ToolbarItem case, it could be avoided by keeping the toolbar instance in the cache of items.
Related - if we land this, I suspect a version bump to 0.5 might be called for. This is just backwards incompatible enough that I think it's worth flagging the significance of the change. |
Thanks for the thorough checks! I've updated the PR description to give a better summary of the change and also discuss why this should be non-breaking for most users. I'll have a closer look at the segfaults that you encountered, later. They might be caused by the usage of Maybe there are also ways to prevent users from shooting themselves in the foot, e.g., raise an exception on manual release calls if there is only a single reference left. |
Thanks, this looks great. I'm busy today, but I'll take a look at this as soon as I can. |
I've had a closer look now at the segfaults and could identify two cases where they happen:
beeware/toga#2978 contains all the changes that I found to (1) prevent segfaults and (2) remove now unneeded manual memory management. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Co-authored-by: Malcolm Smith <[email protected]>
6648cd0
to
f0edb5b
Compare
Co-authored-by: Malcolm Smith <[email protected]>
Co-authored-by: Malcolm Smith <[email protected]>
@samschott FYI - I'm on the last day of PyCon AU sprints; I might not get a chance to look at this until tomorrow when I'm back in my office for a return to semi-normal business. |
No worries. I've gotten a quite a thorough review from @mhsmith in the meantime, but will wait for both of your approvals before merging. Enjoy PyCon AU! |
# it here to prevent leaking memory, Python already owns a refcount from | ||
# when the item was put in the cache. | ||
if _returned_from_method.startswith(_OWNERSHIP_METHOD_PREFIXES): | ||
send_message(object_ptr, "release", restype=objc_id, argtypes=[]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry a bit that the mental load of following when we retain and release is a bit much now since the reader needs to think though the __new__
method being invoked multiple times with the same pointer.
Alternatives such as explicitly tracking a "Python refcount" might be easier to understand but would also add complexity.
Suggestions are welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with this approach: if it's possible to deal with the situation immediately after the copy
, that's definitely easier to follow than maintaining an extra refcount.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. An extra refcount starts to get into re-implementing garbage collection territory, and I'm not sure that's a game that is worth it. I'm comfortable with this as a known edge case, and documenting it with some related usage patterns and advice.
I've made one more change, to accommodate how |
There is yet another hairy issue with the current implementation which will lead to memory leaks:
This is the case for example with from rubicon.objc import *
from rubicon.objc.runtime import autoreleasepool
with autoreleasepool():
adict = NSDictionary.alloc()
idict = adict.initWithObjects(["some_object"], forKeys=["some_key"])
assert idict.retainCount() == 2 # Retained manually by Rubicon and implicitly.
print(adict) # segaults because adict was deallocated The above has two problems:
Edit: Segfault seems to be for different reasons. |
Co-authored-by: Malcolm Smith <[email protected]>
Co-authored-by: Malcolm Smith <[email protected]>
Co-authored-by: Malcolm Smith <[email protected]>
Looks like my assessment from #543 (comment) was not quite correct: The |
I guess then we'll also have to special-case methods starting with
I wouldn't worry too much about that object. It might be deallocated, or it might be a placeholder that gets recycled by every call to |
Interesting... I wonder if this was the thing triggering #539...
To make sure we're on the same page, AIUI, your proposal is extra handling in
That sounds broadly reasonable to me; I have 2 questions/concerns:
|
Something that just occurred to me in reviewing beeware/toga#2978 in the context of this change - does this also resolve the NSImage "init fail" issue? The return value from init will be different to the alloc'd object... so the alloc'd object should be released; the only catch is that we don't need to create the ObjCInstance because it's a None object. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking pretty good to me. Toga has an existing <0.5 pin, so we're safe to do a major update here without breaking Toga; and beeware/toga#2978 is a pretty clear "Delete all your manual retain/release" PR, except for one copy
edge case, and the NSImage
init failure case (which, AFAICT, will be solved if we address the "init may change address" issue.
cached_obj = cls._cached_objects[object_ptr.value] | ||
|
||
# If a cached instance was returned from a call such as `copy` or | ||
# `mutableCopy`, we take ownership of an additional refcount. Release |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Flagging so it isn't forgotten - a link here to the NSCopying
docs would be helpful, plus highlighting that copy
can return the same object as an optimisation if the object is immutable.
# it here to prevent leaking memory, Python already owns a refcount from | ||
# when the item was put in the cache. | ||
if _returned_from_method.startswith(_OWNERSHIP_METHOD_PREFIXES): | ||
send_message(object_ptr, "release", restype=objc_id, argtypes=[]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. An extra refcount starts to get into re-implementing garbage collection territory, and I'm not sure that's a game that is worth it. I'm comfortable with this as a known edge case, and documenting it with some related usage patterns and advice.
This PR changes the memory management model of Rubicon. Previously, we would release objects on Python
__del__
calls only if we created them ourselves and own them from alloc and similar calls.In this PR, we always ensure that we own objects when we create a Python wrapper, by explicitly calling
retain
if we did not get them fromalloc
etc and always callingautorelease
when the Python wrapper is garbage collected. This has a few advantages:This change should be backward compatible for most users because existing manual
retain
andrelease
calls don't cause any issues if balanced and would have already caused segfaults if there are more releases than retains.TODO:
PR Checklist: