Skip to content
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

Merged
merged 66 commits into from
Dec 5, 2024
Merged

Conversation

samschott
Copy link
Member

@samschott samschott commented Nov 19, 2024

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 from alloc etc and always calling autorelease when the Python wrapper is garbage collected. This has a few advantages:

  1. Users will no longer need to manually retain objects when receiving them from non-alloc methods and to release them before the Python object goes out of scope to avoid memory leaks.
  2. Unless users manually release an object, Rubicon guarantees that a Python wrapper always points to an existing Objective-C object -- the one that it was created for.

This change should be backward compatible for most users because existing manual retain and release calls don't cause any issues if balanced and would have already caused segfaults if there are more releases than retains.

TODO:

  • Update docs.
  • Test with toga.
  • Figure out a decent transition plan for clients.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

@samschott
Copy link
Member Author

@mhsmith, care to have first look? Please note the TODOs still listed above.

@samschott samschott force-pushed the updated-memory-management branch from 89a4d62 to 931c352 Compare November 19, 2024 22:44
Copy link
Member

@freakboy3742 freakboy3742 left a 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 retains and autorelease/releases. 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.

@freakboy3742
Copy link
Member

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.

@samschott
Copy link
Member Author

samschott commented Nov 20, 2024

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 release instead of autorelease in __del__ which does not give Objective-C a chance to take over ownership.

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.

@mhsmith
Copy link
Member

mhsmith commented Nov 20, 2024

Thanks, this looks great. I'm busy today, but I'll take a look at this as soon as I can.

@samschott samschott marked this pull request as ready for review November 21, 2024 01:12
@samschott
Copy link
Member Author

samschott commented Nov 23, 2024

I've had a closer look now at the segfaults and could identify two cases where they happen:

  1. The object is released by Rubicon but still needed in ObjC, for example because it is being assigned to a property. Replacing release by autorelease in __del__ fixes this by giving ObjC a chance to take over ownership.
  2. Toga has a few "stray" release or autorelease calls sprinkled throughout the codebase to manually clean up memory, for example because of point (1). This relies on Rubicon internally setting _needs_release = False after those calls and disabling its own cleanup logic. This no longer works and results in one too many release calls now.

beeware/toga#2978 contains all the changes that I found to (1) prevent segfaults and (2) remove now unneeded manual memory management.

Copy link
Member

@mhsmith mhsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added #539 and #48 to the Fixes list in the top comment.

docs/how-to/memory-management.rst Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
changes/256.bugfix.rst Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
tests/test_core.py Outdated Show resolved Hide resolved
@samschott samschott force-pushed the updated-memory-management branch from 6648cd0 to f0edb5b Compare November 24, 2024 21:52
changes/256.removal.rst Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
changes/256.bugfix.rst Outdated Show resolved Hide resolved
tests/test_core.py Outdated Show resolved Hide resolved
tests/test_core.py Outdated Show resolved Hide resolved
tests/test_core.py Outdated Show resolved Hide resolved
Copy link
Member

@freakboy3742 freakboy3742 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with this PR as it stands now; in addition to solving the originally reported problem (beeware/toga-chart#191), it solves the bigger problem of memory management (#256) problem; it addresses the remaining iOS leakage issues (see beeware/toga#2853); and apparently also fixes a method caching issue that has been unreported to date.

I've left one comment about the argument used when constructing arguments, but that's a minor cleanup suggestion, and one that I'll gladly back down from if you disagrees.

Before we merge, I'd also like @mhsmith's final review in case he can think of any edge cases or other issues I might have missed.

src/rubicon/objc/api.py Outdated Show resolved Hide resolved
if self.superclass.methods_ptr is None:
with self.superclass.cache_lock:
self.superclass._load_methods()
# Traverse superclasses and load methods.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the method lookup isn't strictly related to the memory retention issue, but I'm OK with the level of complexity it adds to this PR in the interest of addressing some issues that we know exist when this PR is used in Toga.

Copy link
Member

@mhsmith mhsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this solution as well. Just a few more comments:

src/rubicon/objc/api.py Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
tests/test_core.py Outdated Show resolved Hide resolved
src/rubicon/objc/api.py Outdated Show resolved Hide resolved
tests/test_core.py Show resolved Hide resolved
tests/test_core.py Outdated Show resolved Hide resolved
@mhsmith mhsmith merged commit 99e7ae0 into beeware:main Dec 5, 2024
20 checks passed
@mhsmith
Copy link
Member

mhsmith commented Dec 5, 2024

Thanks again for doing this!

@HalfWhitt
Copy link

Never would I have dreamed that one graph resizing incorrectly would lead to all this work! Hats off for sure!

@freakboy3742
Copy link
Member

Echoing what @mhsmith said - a huge thanks for this. This removes a huge wart on Rubicon's design; and as the Toga update PR highlights, resolves a bunch of weird edge cases and memory issues.

Following up on this comment - should I cut a 0.5.0 release now, or do you want me to hold off until you've had a chance to take a swing at that update as well? There's no particular pressure to get a release out the door, but given this is a big change, it feels like we should make it official sooner rather than later so that the impact isn't forgotten.

@samschott samschott deleted the updated-memory-management branch December 6, 2024 11:14
@samschott
Copy link
Member Author

I am happy with you to go ahead with 0.5.0 release.

The methods-loading update should be entirely under the hood, without any breaking changes, so will be a easy one to follow in 0.5.1 but also with viewer benefits to users.

There are also still a few issues my methods loading update #547 and I might not get to them until end of next week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants