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

Dispose render objects when owning element is unmounted. #82883

Merged
merged 14 commits into from
Jun 2, 2021

Conversation

dnfield
Copy link
Contributor

@dnfield dnfield commented May 19, 2021

When a RenderObjectElement is unmounted, the associated RenderObject should not be retained.

This patch adds a dispose method to RenderObject, which is called in RenderObjectElement.unmount. It also updates RenderImage to use this to dispose of its image, along with updating a single instance where we accessed a renderObject after calling super.unmount(). I've checked internal repo, and don't see any other instances that this should fail on.

This will help with #81514 (because in addition to nulling out a repaing boundary's layer, we'll want to dispose it and whatever pictures it holds) as well as making some cases of #79605 better.

/cc @jacob314

Pre-launch Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I signed the CLA.
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test-exempt.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@flutter-dashboard flutter-dashboard bot added the framework flutter/packages/flutter repository. See also f: labels. label May 19, 2021
@google-cla google-cla bot added the cla: yes label May 19, 2021
@dnfield dnfield marked this pull request as draft May 19, 2021 04:04
@dnfield
Copy link
Contributor Author

dnfield commented May 19, 2021

Something is missing here. Need to add it back and make sure it still passes tests.

@dnfield dnfield marked this pull request as ready for review May 19, 2021 04:58
void dispose() {
_foregroundRenderObject?.dispose();
_backgroundRenderObject?.dispose();
super.dispose();
Copy link
Contributor

Choose a reason for hiding this comment

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

why not null out the references as well as disposing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -2841,6 +2887,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
header += ' NEEDS-COMPOSITING-BITS-UPDATE';
if (!attached)
header += ' DETACHED';
if (_debugDisposed)
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe move this higher up in the list and short circuit all the other checks if the object is disposed? NEEDS-LAYOUT, NEEDS-PAINT, etc are probably all misleading if the object has been disposed.

@override
RenderObject get renderObject => _renderObject!;
RenderObject get renderObject {
assert(_renderObject != null, 'Object disposed');
Copy link
Contributor

Choose a reason for hiding this comment

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

Object disposed --> RenderObject disposed? or '$runtimeType unmounted'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Going with $runtimeType disposed`, since ROs don't have a notion of unmounting.

Copy link
Contributor

Choose a reason for hiding this comment

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

But isn't this the case where this RenderObjectElement has been unmounted not really the case that the RenderObject has been disposed? The RenderObject probably has been disposed given it is null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahh I missed which thing I was in. I see what you're saying now. Will change to unmounted.

// Don't use the getter, the _renderObject may be null if the element
// is defunct, and the getter asserts that it is not null. This method
// does not guarantee a non-null return anyway.
result = element._renderObject;
Copy link
Contributor

Choose a reason for hiding this comment

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

can we instead return null immediately at the top of this method if the element is defunct?
Seems like a bug if you ever had non-defunct elements in a tree with defunct elements.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. We have other assertions around element that make sure you don't end up in that kind of state.

@@ -5768,6 +5778,11 @@ abstract class RenderObjectElement extends Element {

@override
void unmount() {
assert(
!renderObject.debugDisposed!,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this will also blow up if there is a double unmount causing the _renderObject to already be null. do we care about that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You should not unmount an element more than once. If you do, asserts will fire in the unmount method itself.

Comment on lines 1259 to 1261
if (isRepaintBoundary) {
_layer = null;
}
Copy link
Member

Choose a reason for hiding this comment

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

Why only null this out if we are a repaint boundary? Shouldn't we release all resources?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What other resources does the top level RenderObject have?

Children will get dropped or disposed by elements holding them, unless we ourselves created the children, which I'll add to the top level documentation on RO.

Copy link
Member

Choose a reason for hiding this comment

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

What I meant is: "Why only null our _layer if it is a repaint boundary? If you're not a repaint boundary, who is responsible for freeing up this resource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ahhh, I guess we could null out our reference. In another branch, this code also calls dispose on thelayer, which we should only do if we're a repaint boundary. But yeah we can unconditionally null it out.

});
return true;
}());
assert(() {
Copy link
Member

Choose a reason for hiding this comment

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

nit: to reduce clutter, could this just be part of the previous assert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done


/// Release any resources held by this render object.
///
/// If this render object has created any children directly, it should dispose
Copy link
Member

Choose a reason for hiding this comment

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

should -> must dispose them before calling super.dispose() ? (since the method asserts that children are disposed?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

/// If this render object has created any children directly, it should dispose
/// of those children in this method as well. However, it should not
/// dispose of children that were created by some other object, such as
/// a [RenderObjectElement].
Copy link
Member

Choose a reason for hiding this comment

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

Not sure I understand what is meant by this "However"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried to clarify. Basically, only dispose ROs you created directly, which might include your children but does not automatically include them.


bool _debugDisposed = false;

/// Release any resources held by this render object.
Copy link
Member

Choose a reason for hiding this comment

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

It's probably worth mentioning in the overall class doc on RenderObjects when to dispose them?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added some docs at the top.

@@ -208,4 +208,19 @@ Future<void> main() async {
image.dispose();
expect(image.debugGetOpenHandleStackTraces()!.length, 0);
}, skip: kIsWeb); // Web doesn't track open image handles.

test('Render image disposes its image when it is disposed', () async {
Copy link
Member

Choose a reason for hiding this comment

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

nit: RenderImage

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -5768,6 +5778,11 @@ abstract class RenderObjectElement extends Element {

@override
void unmount() {
Copy link
Member

Choose a reason for hiding this comment

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

Update the docs on unmount to indicate that that's also the place where resources may get released?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

/// Whether this has been disposed.
///
/// If asserts are disabled, this property is always null.
bool? get debugDisposed {
Copy link
Member

Choose a reason for hiding this comment

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

Should there be more methods guarded with an assert on this? e.g. presumably, you shouldn't re-attach a RenderObject that has been disposed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added to attach, scheduleIniitialPaint, layout, markNeedsCompositingBitsUpdate, markNeedsPaint, replaceRootLayer, scheduleInitialSemantics, markNeedsSemanticsUpdate.

Other things are covered by _debugCanPerformMutations. I was surprised more of these weren't.

}
assert(() {
visitChildren((RenderObject child) {
assert(child._debugDisposed);
Copy link
Member

Choose a reason for hiding this comment

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

nit: access via regular getter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done - I was avoiding the ! but it doesn't really matter much for asserts and will be less confusing if someone modifies the getter


bool _debugDisposed = false;

/// Release any resources held by this render object.
Copy link
Member

Choose a reason for hiding this comment

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

We usually also say that the object is no longer usable after calling dispose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@dnfield
Copy link
Contributor Author

dnfield commented May 19, 2021

Added a fix for sectors as well, something in there needed to dispose children it created.

@override
void dispose() {
visitChildren((RenderObject child) {
child.dispose();
Copy link
Member

Choose a reason for hiding this comment

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

This contradicts the documentation on dispose that you should only dispose children that you created yourself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This particular render object does create its own children. We'd get an assertion error in the tests otherwise when the element tried to dispose them. It's just kind of confusing to me as to exactly where this happens...

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it does create its own children. They are added from the outside here:

rootCircle.add(RenderSolidColor(const Color(0xFF00FFFF), desiredDeltaTheta: kTwoPi * 0.15));

Copy link
Contributor

Choose a reason for hiding this comment

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

is there a way to enumerate just the children that the selector created itself? Would help clarify why the children are disposed and avoid bugs if say someone added a subclass that added more children.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@goderbauer is right, but the test that's failing is actually failing from SectorApp rather than this file. I'm looking into it.

@@ -461,6 +469,12 @@ class RenderBoxToRenderSectorAdapter extends RenderBox with RenderObjectWithChil
this.child = child;
}

@override
void dispose() {
child?.dispose();
Copy link
Member

Choose a reason for hiding this comment

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

dito?

Comment on lines 1259 to 1261
if (isRepaintBoundary) {
_layer = null;
}
Copy link
Member

Choose a reason for hiding this comment

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

What I meant is: "Why only null our _layer if it is a repaint boundary? If you're not a repaint boundary, who is responsible for freeing up this resource?

///
/// [RenderObject]s are responsible for cleaning up any expensive resources
/// they hold when [dispose] is called, such as [Picture] or [Image] objects.
/// They must _only_ dispose of their [layer] object if [isRepaintBoundary] is
Copy link
Member

Choose a reason for hiding this comment

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

This is not super-clear to me. If I own the layer (e.g. because I set it in my paint method) why am I not in charge of releasing it as well? How can my parent still use it if I (the owner) are no longer arround?

Copy link
Contributor

Choose a reason for hiding this comment

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

@dnfield and I haven't finished this discussion, so I'm also interested in the ownership model here. I think there's shared ownership going on, but we lack means to express it. In a ref-counted language I'd be thinking about it as follows: a layer's ref count is bumped by its parent layer and its render object. The count can be in the following states:

2 - when a layer has both a parent layer and an owning RenderObject
1 - when a layer has either a parent layer or an owning RenderObject, but not both
0 - when nothing references the layer, this is when I'd clear the resources of this layer, this is also when recursively this layer's descendants' ref-counts are dropped by one and, possibly, reach 0, get their resources cleaned up, and so on.

This way we don't need to treat repaint boundaries as special. Any render object can own a sub-tree of layers, and the recursive disposal of render objects introduced by this PR makes sure we don't leak layers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this doc is incorrect and I have updated it.

I may have to revisit this as I continue to work out the details about layer disposal, but I think it makes more sense now. PTAL.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks. The doc comment is clear, but I'm not sure if what it describes is the right thing to do. Perhaps it is, and I just don't have a full picture in my head. Here's a couple of examples that I'm not sure are handled properly:

  • A repaint boundary has a RenderOpacity child. The repaint boundary disposes of its layer. Is the RepaintBoundary pointing to a disposed layer (i.e. a dangling pointer)?
  • A repaint boundary has another repaint boundary as a child. The parent disposes of its layer. Do we recursively delete pictures of the child repaint boundary? If not, how do we know to not delete them? If yes, how does the child repaint boundary know what its resources got cleaned-up and it needs to repaint?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've changed this wording because it's no longer relevant to this patch. This patch doesn't add anything for disposing of layers, that will be coming in a separate patch.

To start to answer these questions though - if a repaint boundary disposes its layer, it means that repaint boundary is going way and won't be drawn again without getting completely recreated/repainted. And so if it has any child repaint boundaries that have not been moved to some active part of the tree, they will not get drawn again either unless recreated/repainted.

/// [RenderObject]s are responsible for cleaning up any expensive resources
/// they hold when [dispose] is called, such as [Picture] or [Image] objects.
/// This includes any [Layer]s that the render object has directly created. By
/// default, the base implementation of dispose will dispose of the [layer], and
Copy link
Member

Choose a reason for hiding this comment

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

I don't think subclasses can change this "default" behavior since they have to call super.dispose? Should we maybe just say that Layers created by the render object must be disposes manually unless they have been set to the layer property, which is automatically disposed?

Copy link
Member

Choose a reason for hiding this comment

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

Also: This patch doesn't contain any layer disposing mechanics (it just nulls out the property). Do you plan to add that logic to this PR or will that be added later? If it is latter, maybe we should just document what's implemented for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reworded this a bit -layer.dispose will be in a separate patch, instead just talking about nulling the property.

Comment on lines 1270 to 1271
/// If [isRepaintBoundary] returns true, the layer tree rooted at this
/// object's layer will also dispose.
Copy link
Member

Choose a reason for hiding this comment

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

Is this still valid? Looks like _layer is now nulled regardless of isRepaintBoundary?

Copy link
Member

Choose a reason for hiding this comment

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

(Also note my comment above regarding that layers do not implement a disposal concept yet...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, removing this for now.

visitChildren((RenderObject child) {
assert(
child.debugDisposed!,
'${child.runtimeType} (child of $runtimeType) must be disposed before calling super.dispose().',
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add a note about this condition to the doc comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This corresponds to the doc comment

  /// Implementations of this method must end with a call to the inherited
  /// method, as in `super.dispose()`.

@@ -6221,13 +6221,18 @@ class DefaultAssetBundle extends InheritedWidget {
/// A given render object can be placed at most once in the widget tree. This
/// widget enforces that restriction by keying itself using a [GlobalObjectKey]
/// for the given render object.
///
/// This widget will call [RenderObject.dispose] on the [renderBox] when it is
Copy link
Member

Choose a reason for hiding this comment

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

It's somewhat surprising that the adaptor takes ownership of the passed in renderBox and disposes it, no?

I originally expected that you'd be in charge of doing that in onUnmount if you don't want to reuse it?

Copy link
Member

Choose a reason for hiding this comment

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

(Maybe also document this in the renderBox property?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thought is to keep this as a less-breaking change. Otherwise users will have to update their code to use the unmount property now where they didn't before. Updated the renderBox property.

Perhaps at some point it would be worth changing this, but my assumption is that most users use this widget as if it will dispose the render object, rather than caching a render object to use repeatedly with this widget.

Copy link
Member

Choose a reason for hiding this comment

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

I see. I was thinking that maybe the default onUnmount handler would just dispose the renderBox, but if you don't want that you could provide a custom one and make it to do something else (e.g. keep the renderObject alive longer).

But maybe that can be added later. Maybe we should however specifically call out in the doc here that the provided renderBox becomes unusable when the adapter is unmounted from the tree?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added another line explaining that.

/// Called when it is safe to dispose of the render object and its
/// descendants.
///
/// Typically, this should dispose of any children attached to the
Copy link
Member

Choose a reason for hiding this comment

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

The two paragraphs sound a little contradictory: One the one hand its safe to dispose the render object (first paragraph) on the other hand it must not be disposed (second paragraph)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, I've updated this to try to clarify it and added links to some relevant areas.

Copy link
Member

@goderbauer goderbauer left a comment

Choose a reason for hiding this comment

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

LGTM

@@ -6221,13 +6221,18 @@ class DefaultAssetBundle extends InheritedWidget {
/// A given render object can be placed at most once in the widget tree. This
/// widget enforces that restriction by keying itself using a [GlobalObjectKey]
/// for the given render object.
///
/// This widget will call [RenderObject.dispose] on the [renderBox] when it is
Copy link
Member

Choose a reason for hiding this comment

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

I see. I was thinking that maybe the default onUnmount handler would just dispose the renderBox, but if you don't want that you could provide a custom one and make it to do something else (e.g. keep the renderObject alive longer).

But maybe that can be added later. Maybe we should however specifically call out in the doc here that the provided renderBox becomes unusable when the adapter is unmounted from the tree?

Copy link
Contributor

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

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

lgtm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants