-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Dispose of images after using them #66688
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
|
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Who is you? Prefer a bit of passive voice instead maybe?
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.
Rephrased.
| : assert(image != null), | ||
| assert(scale != null); | ||
|
|
||
| /// Creates an [ImageInfo] with a cloned [image]. |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is comparing clones useful?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
concrete example of a Client is an Image widget? Or an image cache?
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.
Any method or class receiving this object.
| ); | ||
| renderObject | ||
| ..image = image | ||
| ..image = image?.clone() |
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 does this clone if it doesn't dispose? won't this create extra image clones
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have also been confused by the fact that cloning and disposing are done by separate entities.
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, 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grammer: ... is called only called 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.
Fixed
| /// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should document that listeners are in charge of disposing the image they receive.
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 to template.
| /// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename _listeners to _activeListeners to avoid confusion in the code of what's included here?
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
| ); | ||
| return RenderImage( | ||
| image: image, | ||
| image: image?.clone(), |
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.
Who disposes this?
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 render object.
| ); | ||
| renderObject | ||
| ..image = image | ||
| ..image = image?.clone() |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be 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.
This causes the render object to dispose the image.
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is oddly named since it does not dispose the image from the info given to it. Maybe rename to replaceImage?
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
| // if (createPassiveListener) { | ||
| // _imageStream!.completer?.addPassiveListener(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.
?
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.
haha whups :) Thought I needed this at one point but I do not.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
k
| : assert(image != null), | ||
| assert(scale != null); | ||
|
|
||
| /// Creates an [ImageInfo] with a cloned [image]. |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
avoid => in setState since it doesn't return anything
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
Hixie
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.
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) |
goderbauer
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.
LGTM with some tiny nits.
| /// void _updateImage(ImageInfo imageInfo, bool synchronousCall) { | ||
| /// setState(() { | ||
| /// // Trigger a build whenever the image changes. | ||
| /// _imageInfo?.image?.dispose(); |
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 you dispose the _imageInfo directly here instead of the image?
| /// @override | ||
| /// void dispose() { | ||
| /// _imageStream.removeListener(ImageStreamListener(_updateImage)); | ||
| /// _imageInfo?.image?.dispose(); |
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 here?
| /// | ||
| /// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.