Dispose of images after using them#66688
Conversation
|
Apparently this broke something in gallery - marking as draft while I figure that out. |
|
It's a bug in dart:ui. Fix will be posted shortly. |
|
Blocked by flutter/engine#21422 |
|
Engine fix has been rolled in, this is ready for review again. |
|
Ok, tests area ll passing now :) |
| /// ImageInfo objects are used by [ImageStream] objects to represent the | ||
| /// actual data of the image once it has been obtained. | ||
| /// | ||
| /// If you receive an [ImageInfo] object, it is your responsibility to call |
There was a problem hiding this comment.
Who is you? Prefer a bit of passive voice instead maybe?
| : assert(image != null), | ||
| assert(scale != null); | ||
|
|
||
| /// Creates an [ImageInfo] with a cloned [image]. |
There was a problem hiding this comment.
when would someone want/need to create a clone?
|
|
||
| /// Whether this [ImageInfo] is a [clone] of the `other`. | ||
| /// | ||
| /// |
There was a problem hiding this comment.
Why is comparing clones useful?
There was a problem hiding this comment.
Expanded with example.
| /// own image reference, it can no longer access the image, but other clients | ||
| /// will be able to access their own references. | ||
| /// | ||
| /// This method must be used in cases where a client holding an [ImageInfo] |
There was a problem hiding this comment.
concrete example of a Client is an Image widget? Or an image cache?
There was a problem hiding this comment.
Any method or class receiving this object.
| ); | ||
| renderObject | ||
| ..image = image | ||
| ..image = image?.clone() |
There was a problem hiding this comment.
why does this clone if it doesn't dispose? won't this create extra image clones
There was a problem hiding this comment.
The render object disposes of it. This can't dispose because it's stateless - it needs to be kept alive by whatever created RawImage.
There was a problem hiding this comment.
This is pretty confusing - so the State class has its copy, which it will clone/dispose as needed. Then the RawImage is just a pass through, and finally the RO accepts the clone and does a dispose.
Maybe a safer arrangement would be for the RO to clone in the setter, so that the behavior is internal to it?
There was a problem hiding this comment.
I'm trying to keep the contract as you should never have to clone something you received. If we have some places that clone immediately I think it's more confusing.
However, I agree that RawImage is a sore spot, and I think it'd be preferable to just make it private since it's easy to misuse.
There was a problem hiding this comment.
I have also been confused by the fact that cloning and disposing are done by separate entities.
There was a problem hiding this comment.
Yeah, if it needs to act like an implementation detail of the Image class it should be private. But we do have other RawFoo classes exposed in the framework so that people could build on top of that, though I think there is also a rendering helper method too?
I dunno.
There was a problem hiding this comment.
Refactored RawImage to not be a special case. There's a private impl that's a special case now though.
There was a problem hiding this comment.
I had to revert that refactor because it doesn't work to have the image hold a stateful object like that.
There was a problem hiding this comment.
why not have the widget pass through, and its caller (the call to the RawImage constructor) have to call clone()?
There was a problem hiding this comment.
The widget might create multiple RenderObjects (via multiple elements). It has no defined lifetime, and so it's not clear when the widget's clone would ever get disposed.
| image.completer.addOnLastListenerRemovedCallback(image.handleRemove); | ||
| return image; | ||
| }).sizeBytes ??= image.sizeBytes; | ||
| return _LiveImage( |
There was a problem hiding this comment.
The comment above here is confusing and/or outdated since no listener is added in the code below?
There was a problem hiding this comment.
Updated the comment.
| _processNewListener(listener); | ||
| } | ||
|
|
||
| /// Adds a listener callback that is called only called if there are listeners |
There was a problem hiding this comment.
grammer: ... is called only called if ...
| /// object is available or an error is reported. If a concrete image is | ||
| /// already available, or if an error has been already reported, this object | ||
| /// will notify the listener synchronously. | ||
| /// |
There was a problem hiding this comment.
This should document that listeners are in charge of disposing the image they receive.
| /// already available, or if an error has been already reported, this object | ||
| /// will notify the listener synchronously. | ||
| /// | ||
| /// If the [ImageStreamCompleter] completes multiple images over its lifetime, |
There was a problem hiding this comment.
Template should cover both. I'll also update ImageStreamListener.onImage.
| /// {@macro flutter.painting.imageStream.addListener} | ||
| void addListener(ImageStreamListener listener) { | ||
| _checkDisposed(); | ||
| _listeners.add(listener); |
There was a problem hiding this comment.
nit: rename _listeners to _activeListeners to avoid confusion in the code of what's included here?
| ); | ||
| return RenderImage( | ||
| image: image, | ||
| image: image?.clone(), |
| ); | ||
| renderObject | ||
| ..image = image | ||
| ..image = image?.clone() |
There was a problem hiding this comment.
I have also been confused by the fact that cloning and disposing are done by separate entities.
|
|
||
| @override | ||
| void didUnmountRenderObject(RenderImage renderObject) { | ||
| renderObject.image = null; |
There was a problem hiding this comment.
This doesn't need to be disposed?
There was a problem hiding this comment.
This causes the render object to dispose the image.
There was a problem hiding this comment.
I've refactored RawImage into a public StatefulWidget and made the current impl private. The public impl now does not need to be special cased.
| }); | ||
| } | ||
|
|
||
| void _disposeImage([ImageInfo? newInfo]) { |
There was a problem hiding this comment.
This is oddly named since it does not dispose the image from the info given to it. Maybe rename to replaceImage?
| // if (createPassiveListener) { | ||
| // _imageStream!.completer?.addPassiveListener(null); | ||
| // } |
There was a problem hiding this comment.
haha whups :) Thought I needed this at one point but I do not.
There was a problem hiding this comment.
Oh. I think I do need something like this. I'll fix it and add the relevant test.
The problem right now is that if the TickerMode goes off, we could inadvertently dispose of the image stream when we stop listening to it. We'll have to add a passive listener and remove it.
This reverts commit ea19b4e.
|
My idea to refactor |
| // Give any interested parties a chance to listen to the stream before we | ||
| // potentially dispose it. | ||
| // scheduleMicrotask(() { | ||
| SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) { |
There was a problem hiding this comment.
addPostFrameCallback doesn't request a new frame. Are you guaranteed that this code is called only during a frame?
There was a problem hiding this comment.
No, but we want to keep it alive until the next scheduled frame to give other clients time to subscribe.
The downside is that if you precache an image and then never ever schedule a frame again, you'll hold the memory. But I'm not clear on how that would happen - you shouldn't be precaching images if you're not going to schedule frames to draw them.
| : assert(image != null), | ||
| assert(scale != null); | ||
|
|
||
| /// Creates an [ImageInfo] with a cloned [image]. |
There was a problem hiding this comment.
i think this class should have a dispose, since it owns something that can be disposed. That would be more intuitive than having to clean out its contents.
There was a problem hiding this comment.
The only reason I didn't do that is because we have some objects that eventually are just given the image out of an ImageInfo.
I think it's confusing either way, I'm just not sure which way is less confusing :)
|
|
||
| if (!widget.gaplessPlayback) | ||
| setState(() { _imageInfo = null; }); | ||
| setState(() => _replaceImage(info: null)); |
There was a problem hiding this comment.
avoid => in setState since it doesn't return anything
Hixie
left a comment
There was a problem hiding this comment.
I think this is the best we've come up with for this problem. I continue to hope that one day we can find an even better solution, but I've no idea what that could even be.
|
(LGTM) |
| /// void _updateImage(ImageInfo imageInfo, bool synchronousCall) { | ||
| /// setState(() { | ||
| /// // Trigger a build whenever the image changes. | ||
| /// _imageInfo?.image?.dispose(); |
There was a problem hiding this comment.
Should you dispose the _imageInfo directly here instead of the image?
| /// @override | ||
| /// void dispose() { | ||
| /// _imageStream.removeListener(ImageStreamListener(_updateImage)); | ||
| /// _imageInfo?.image?.dispose(); |
| /// | ||
| /// If the listener from this state is the last listener on the stream, the | ||
| /// stream will be disposed. To keep the stream alive, set `keepStreamAlive` | ||
| /// to true, which will add a passive listener on the stream and is compatible |
There was a problem hiding this comment.
nit: we no longer have passive listeners.
| /// // If the image reference is exactly the same, or its a clone of the | ||
| /// // current reference, we can immediately dispose of it and avoid | ||
| /// // recalculating anything. | ||
| /// if (value == _imageInfo || value != null && _imageInfo != null && value.isCloneOf(_imageInfo)) { |
There was a problem hiding this comment.
Change this to what the actual code in rendering/image.dart is? I.e. don't dispose if value == _imageInfo!
| /// | ||
| /// See also: | ||
| /// | ||
| /// * [Image.clone] |
There was a problem hiding this comment.
nit: add some context why to look there.
| /// Disposes of this object. | ||
| /// | ||
| /// Once this method has been called, the object should not be used anymore, | ||
| /// and no clones of it or the image it contains can be made. |
There was a problem hiding this comment.
I wonder if it is worthwhile to add asserts that a disposed imageinfo isn't used anymore (e.g. assert in the image and clone getter?)
| @visibleForTesting | ||
| bool get hasListeners => _listeners.isNotEmpty; | ||
|
|
||
| /// We should avoid disposing a completer if it has never had a listener, even |
There was a problem hiding this comment.
nit: "should avoid" sounds pretty weak. Are there situations where we dispose this even though somebody anted it alive?
This reverts commit a795469.
|
Before the revert, we detected a big reduction in max memory usage. Look forward to the reland! |
Description
This patch should close the image disposal saga.
Does the following:
ImageInfo.cloneandImageInfo.isCloneOf.ui.Imageobjects properlycloneanddisposeof them.ImageStreamCompleter: Removing the last listener after registering at least one listener results in the object being disposed, and new listeners cannot be added.ImageStreamCompleterto have "passive" listeners, which are listeners that do not drive frames forward but keep the stream alive and get updates if there are other "active" listeners. For example, the image cache listens to the stream this way.ui.Image(i.e.paintImageandRawImage) assert that whoever gave the image to them has not disposed it on them.Related Issues
Fixes #56482 for real this time, by only triggering additional GCs if we've disposed an image large enough to get the VM's attention.
A previous, rejected attempt here: #64582
Related engine patches:
flutter/engine#21057 (adds
Image.cloneflutter/engine#20746 (an overly aggressive attempt to use Dart_HintFreed to achieve this goal, got reverted).
flutter/engine#20754 (uses HintFreed specifically in CanvasImage::dispose)
Tests
I added the following tests:
A bunch of tests for image, image cache, image stream, image stream completer, to make sure we dispose of the right things at the right times.
There is also a devicelab memory benchmark for this use case that was added in #64762
Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.