-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Release retained resources from layers/dispose pictures #84740
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
Conversation
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
| /// | ||
| /// This class manages automatically creation and diposing of layer handles for | ||
| /// its associated [layer]. | ||
| class LayerHolder<T extends Layer?> { |
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.
reviewers: I'm somewhat tempted to make this a mixin on RenderObject so it can override and handle dispose for you. The problem is that it would mean a RenderObject could only easily handle one additional layer, and we'd have to give the layer a weird property name like additionalLayer or something.
The framework only needs one additional layer for any of the RenderObjects that tests have picked up, but I'm a bit put off by the naming weirdness and the odd restriction. I'm open to ideas though!
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.
What if LayerHandle instead of a dispose method rendering it unusable simply had a set layer that accepted null? Then the LayerHandle wouldn't need to be nulled out in various dispose methods, and we wouldn't need LayerHolder?
Strawman:
class LayerHandle {
set layer(Layer? layer) {
if (identical(layer, _layer)) return;
_layer._unref();
_layer = layer;
}
Layer get layer => _layer!;
}Instead of this:
child._parentHandle!.dispose();
child._parentHandle = null;One would do:
child._parentHandle.layer = null;Render objects could use LayerHandle and LayerHolder wouldn't be necessary.
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've more or less done this, but made it Layer? get layer => _layer;.
| } | ||
|
|
||
| /// Used for tests that directly invoke [RenderObject.paint] or | ||
| /// [RenderObject.debugPaint]. |
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 documentation should still say what it does (including what it does in release builds)
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.
Done. Also updated docs on the canvas getter to explain what tests might need to do if they directly invoke paint/debugPaint.
|
/cc @ds84182 - this breaks a render object you created internally. I'll likely have to introduce an intermediate step here with some changes to the Layer interface that don't do/assert anything yet, fix up _FocusOverlay, and then re-introduce the asserts - but happy to discuss further with you if you have a better idea. |
e4172c9 to
da6f798
Compare
| 'Do not directly call dispose on a $runtimeType. Instead, ' | ||
| 'use createHandle and LayerHandle.dispose.', | ||
| ); | ||
| _debugDisposed = true; |
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.
Commenting out this line is enough to make internal tests pass (since it undoes other assertions that you avoid using a layer if it is disposed).
I think it might be a good idea to do that until the internal RO that needs updating is updated, with a TODO to enable this.
Layers are technically usable after disposal, but triggering this assert means you failed to hold a reference to a layer that you wanted to keep alive, and its native resources are gone. Alternatively, it means that you did want to dispose that RO and failed to dispose it when you should have, meaning you held native resources longer than you needed to.
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.
@ds84182 FYI
|
This patch now passes internally. |
| count = _refCount; | ||
| return true; | ||
| }()); | ||
| return count; |
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 will throw an error complaining that count hasn't been initialized. Can we instead throw something that talks about debuHandleCount being only supported in debug mode?
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 pretty sure this is how we handle these sorts of debug* elsewhere. I'd really just rather leave this method as sparse as possible.
| int _refCount = 0; | ||
|
|
||
| void _unref() { | ||
| assert(_refCount > 0); |
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.
Should we attach a message to this assert?
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 would have to be a bug in the framework, if someone broke either createHandle or LayerHandle.dispose. End users should never see this, and if they somehow do they don't have a way to fix it short of patching the framework/filing a bug. Without a message here, they'll get a message encouraging them to file a bug with the stack trace/reproduction.
| @protected | ||
| @visibleForTesting | ||
| void dispose() { | ||
| assert(!_debugDisposed); |
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.
Should we attach a message to this assert?
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.
Done
| 'use createHandle and LayerHandle.dispose.', | ||
| ); | ||
| // TODO(dnfield): enable this. https://github.com/flutter/flutter/issues/85066 | ||
| // _debugDisposed = true; |
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.
Can we print a warning to nudge people to fix the lifecycles of their objects?
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.
We could, but that'd mean commenting out more asserts elsewhere instead of this one.
There's only one customer that is actually affected by this in all of customer_testing/google3. I don't plan to leave this commented out for long.
| /// the handles for that layer similarly to the implementation of | ||
| /// [RenderObject.layer]. | ||
| /// | ||
| /// Layers can be reused even after their native resources has been released, |
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.
nit: s/has/have/
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.
Ah, this whole paragraph is obsolete. Removed it.
|
|
||
| /// A composited layer containing a [Picture]. | ||
| /// | ||
| /// Picture layers are always leaves in the layer tree. |
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.
Since this class is responsible for calling Picture.dispose it is effectively the owner of the Picture object it holds. We should note that in the dartdocs.
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.
Added
|
|
||
| /// A utility for managing a layer maintaining a handle to it. | ||
| /// | ||
| /// This class manages automatically creation and diposing of layer handles for |
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.
typo: diposing
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.
Fixed
| /// | ||
| /// This class manages automatically creation and diposing of layer handles for | ||
| /// its associated [layer]. | ||
| class LayerHolder<T extends Layer?> { |
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.
What if LayerHandle instead of a dispose method rendering it unusable simply had a set layer that accepted null? Then the LayerHandle wouldn't need to be nulled out in various dispose methods, and we wouldn't need LayerHolder?
Strawman:
class LayerHandle {
set layer(Layer? layer) {
if (identical(layer, _layer)) return;
_layer._unref();
_layer = layer;
}
Layer get layer => _layer!;
}Instead of this:
child._parentHandle!.dispose();
child._parentHandle = null;One would do:
child._parentHandle.layer = null;Render objects could use LayerHandle and LayerHolder wouldn't be necessary.
| /// [RenderObject.debugPaint]. | ||
| /// | ||
| /// When accessing the [canvas], the context expects that it has started the | ||
| /// painted operation on the render object via [paintChild] or |
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.
nit: "paint operation"?
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.
Done
| /// context, which means it's fragile to hold a reference to the canvas | ||
| /// returned by this getter. | ||
| /// | ||
| /// This getter requires that the current [RenderObject] being registered for |
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.
nit: s/being/is/
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.
Done
| ContainerLayer? get layer { | ||
| assert(!isRepaintBoundary || (_layer == null || _layer is OffsetLayer)); | ||
| return _layer; | ||
| assert(!isRepaintBoundary || (_layerHandle.layer == null || _layerHandle.layer is OffsetLayer)); |
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.
nit: redundant parens, I think
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.
Done
| } | ||
|
|
||
| final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>(); | ||
| final List<LayerHandle<PictureLayer>> _pictureLayerHandles = <LayerHandle<PictureLayer>>[]; |
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.
Why do render objects need to maintain handles to pictures? It seems as if this is needed to prolong the lifetimes of pictures, but I don't see these handles used for anything, so I'm not sure why we'd want to prolong their lifetimes. If a picture layer is still used by a ContainerLayer (transitively from some render object), then that container layer would keep a handle making sure that the picture is not collected. Is there a use-case I'm missing?
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 a good catch.
I wrote this code first before working out how to handle refcounting on ContainerLayers, and some of my experiments with trying to do that were trying to dispose picture layers at the wrong time and causing problems. Having the RenderObjects do it was working.
However, that caused problems for repaint boundaries. When I finally worked out the attach/append/detach/remove distinction, that problem went away but I didn't revisit this code.
I'm removing this code and the associated debug* field/methods for it. All tests are still passing with it, and apps I'm testing with locally still correctly release memory without it.
| /// If this render object is a repaint boundary, the framework automatically | ||
| /// creates an [OffsetLayer] and populates this field prior to calling the | ||
| /// [paint] method. The [paint] method must not replace the value of this | ||
| /// field. |
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 doc comment may be implying things that are no longer correct. For example, "It is also OK to leave this field as null and create a new layer on every repaint, but without the performance benefit" is probably not true. If the field is not populated we may end up leaking memory, no?
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'll update this to mention that you should use a LayerHandle so that the layer gets appropriately disposed.
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.
The real problem is if the RO tries to hold another layer in a private field and then reuse it.
| }()); | ||
| return object; | ||
| } | ||
| static RenderObject? _activePaint; |
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 missed this in the previous PR but would it make more sense to put this field on the PipelineOwner instead of making it a static?
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've reverted this change since it's not needed anymore - this has gone back to being a debug static thing.
yjbanov
left a comment
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.
| /// a layer for later usage, it must create a handle to that layer. This is | ||
| /// handled automatically for the [RenderObject.layer] property, but additional | ||
| /// layers should be handled with a [LayerHandle] class. | ||
| /// layers must user their own [LayerHandle]. |
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.
nit: s/user/use/
| /// layer in [RenderObject.paint], it should dispose of the handle to the | ||
| /// old layer. It should also dispose of any layer handles it holds in | ||
| /// [RenderObject.dispose]. | ||
| class LayerHandle<T extends Layer?> { |
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 think this can be <T extends Layer> (i.e. the non-null upper bound). This is because internally T? will automatically allow null values, even if the layer type used by LayerHandle is not nullable.
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.
Ahh ok. This works if you don't use LayerHandle<Layer?> anywhere else.
| } | ||
|
|
||
| ClipPathLayer? _clipPathLayer; | ||
| final LayerHandle<ClipPathLayer?> _clipPathLayer = LayerHandle<ClipPathLayer?>(); |
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 think this can be LayerHandle<ClipPathLayer> (i.e. no ?). null will still be allowed because LayerHandle class specifies T?.
| _paintWithDelegate(context, offset); | ||
| } else { | ||
| _clipRectLayer = context.pushClipRect( | ||
| _clipRectLayer.layer = context.pushClipRect( |
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 know this would be a significant breaking change, but it would be nice if all context.push* methods would just take LayerHandle as an argument. Then they could be void because they could populate the handle internally (sort of like a ref parameter in C#):
context.pushClipRect(..., _clipRectLayerHandle); // ta-da!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.
yeah, this would be nice. It may make sense to make some pushHandle methods and deprecate the existing ones.
| /// imply that it has been removed from its parent. | ||
| final LayerHandle<Layer> _parentHandle = LayerHandle<Layer>(); | ||
|
|
||
| /// Incremenetd by [LayerHandle]. |
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.
typo in "Incremenetd"
| /// | ||
| /// A handle is also automatically managed for [RenderObject.layer]. | ||
| /// | ||
| /// If a [RenderObject] creates layers in addition to its [RenderObject.layer], |
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.
"... and it intends to reuse those layers separately from [RenderObject.layer]" (a handle is not required if the additional layers are disposed of simultaneously with RenderObject.layer; the layers will be removed when the layer tree is disassembled).
| /// old layer. It should also dispose of any layer handles it holds in | ||
| /// [RenderObject.dispose]. | ||
| class LayerHandle<T extends Layer?> { | ||
| /// Creates a new LayerHandle, optionally referencing a [Layer]. |
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.
nit: I'd use plain English "Create a new layer handle".
|
|
||
| T? _layer; | ||
|
|
||
| /// The [Layer] this class holds. |
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.
nit: "The [Layer] whose resources this handle keeps alive."?
| /// Setting a new value will dispose the previously held layer if there are | ||
| /// no other open handles to that layer. | ||
| /// | ||
| /// To dispose of this handle, set this value to `null`. |
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.
nit: instead of an extra paragraph specifically for null, we could say "Setting a new value or null will dispose..." in the previous paragraph. It would make it clear that nulling out the layer does not guarantee disposal, that all other handles must also be cleared before the layer is disposed of.
| /// | ||
| /// Picture layers are always leaves in the layer tree. | ||
| /// Picture layers are always leaves in the layer tree. They are also | ||
| /// responsible for disposing of the [Picture] object they hold. This is done |
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.
nit: "This is typically done" (in case some users don't use the upper layers of the framework, e.g. perhaps they have their own custom render object system)
|
Thanks @yjbanov - addressed all the nits. |
|
|
||
| /// The [Layer] whose resources this object keeps alive. | ||
| /// | ||
| /// Setting a new value will or null dispose the previously held layer if |
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.
typo: "will" should probably come after "or null"?
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.
Yeah, I'll fix this.

This implements
Layer.dispose, and makes sure that theRenderObject.layerandPaintingContext._currentLayerget properly disposed when render objects are done with them.It also adds some affordances for managing layer disposal when a render object needs an additional layer, which is a relatively common case for a repaint boundary that also needs to do some clipping.
This is the last major user-facing piece of https://flutter.dev/go/picture-lifecycle, however there is some additional work planned beyond this. Specifically, flutter/engine#26375 can still help developers catch leaks (including framework/engine developers), and flutter/engine#26870 will further reduce GC related activity that is no longer as essential since we more eagerly release native resources after this patch.
Locally, this patch is showing significant boosts in transition perf benchmarks. I believe this is related to reduced GC pressure and reduced need to perform GCs.
/cc @knopp @flar - I think this will be helpful with some of the diff context work
/cc @yjbanov since we've been discussing this for web
/cc @jacob314 - mistakes around this are the kind of thing we'll want to catch with the work I've been discussing about GC-but-not-disposed objects.
/cc @goderbauer @Hixie @zanderso
fixes #81514