Release retained resources from layers/dispose pictures#84740
Release retained resources from layers/dispose pictures#84740fluttergithubbot merged 14 commits intoflutter:masterfrom
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.
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.
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.
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.
this documentation should still say what it does (including what it does in release builds)
There was a problem hiding this comment.
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.
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.
|
This patch now passes internally. |
| count = _refCount; | ||
| return true; | ||
| }()); | ||
| return count; |
There was a problem hiding this comment.
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.
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.
Should we attach a message to this assert?
There was a problem hiding this comment.
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.
Should we attach a message to this assert?
| '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.
Can we print a warning to nudge people to fix the lifecycles of their objects?
There was a problem hiding this comment.
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.
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.
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.
|
|
||
| /// A utility for managing a layer maintaining a handle to it. | ||
| /// | ||
| /// This class manages automatically creation and diposing of layer handles for |
| /// | ||
| /// 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.
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 |
| /// 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 |
| 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.
nit: redundant parens, I think
| } | ||
|
|
||
| final LayerHandle<ContainerLayer> _layerHandle = LayerHandle<ContainerLayer>(); | ||
| final List<LayerHandle<PictureLayer>> _pictureLayerHandles = <LayerHandle<PictureLayer>>[]; |
There was a problem hiding this comment.
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.
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.
| @@ -2056,8 +2108,8 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im | |||
| /// field. | |||
There was a problem hiding this comment.
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.
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.
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.
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.
I've reverted this change since it's not needed anymore - this has gone back to being a debug static thing.
| /// 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]. |
| /// 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.
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.
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.
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.
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.
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]. |
| /// | ||
| /// 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.
"... 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.
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.
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.
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.
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.
typo: "will" should probably come after "or null"?
There was a problem hiding this comment.
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