Skip to content

Conversation

@dnfield
Copy link
Contributor

@dnfield dnfield commented Sep 25, 2020

Description

This patch should close the image disposal saga.

Does the following:

  • Adds ImageInfo.clone and ImageInfo.isCloneOf.
  • Makes all stateful holders of ui.Image objects properly clone and dispose of them.
  • Behavior change for ImageStreamCompleter: Removing the last listener after registering at least one listener results in the object being disposed, and new listeners cannot be added.
  • Allow ImageStreamCompleter to 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.
  • Makes all non-stateful holders of ui.Image (i.e. paintImage and RawImage) 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.clone
flutter/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.

  • No, no existing tests failed, so this is not a breaking change.

@flutter-dashboard flutter-dashboard bot added a: tests "flutter test", flutter_test, or one of our tests framework flutter/packages/flutter repository. See also f: labels. labels Sep 25, 2020
@dnfield
Copy link
Contributor Author

dnfield commented Sep 25, 2020

Apparently this broke something in gallery - marking as draft while I figure that out.

@dnfield dnfield marked this pull request as draft September 25, 2020 22:29
@dnfield
Copy link
Contributor Author

dnfield commented Sep 25, 2020

It's a bug in dart:ui. Fix will be posted shortly.

@dnfield
Copy link
Contributor Author

dnfield commented Sep 25, 2020

Blocked by flutter/engine#21422

@dnfield
Copy link
Contributor Author

dnfield commented Sep 28, 2020

Engine fix has been rolled in, this is ready for review again.

@dnfield dnfield marked this pull request as ready for review September 28, 2020 18:57
@dnfield
Copy link
Contributor Author

dnfield commented Sep 28, 2020

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
Copy link
Contributor

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?

Copy link
Contributor Author

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].
Copy link
Contributor

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`.
///
///
Copy link
Contributor

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?

Copy link
Contributor Author

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]
Copy link
Contributor

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?

Copy link
Contributor Author

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()
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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?

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'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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

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 had to revert that refactor because it doesn't work to have the image hold a stateful object like that.

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 have the widget pass through, and its caller (the call to the RawImage constructor) have to call clone()?

Copy link
Contributor Author

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(
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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 ...

Copy link
Contributor Author

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.
///
Copy link
Member

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.

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 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,
Copy link
Member

Choose a reason for hiding this comment

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

same here

Copy link
Contributor Author

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);
Copy link
Member

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?

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

);
return RenderImage(
image: image,
image: image?.clone(),
Copy link
Member

Choose a reason for hiding this comment

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

Who disposes this?

Copy link
Contributor Author

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()
Copy link
Member

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;
Copy link
Member

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?

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 causes the render object to dispose the image.

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 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]) {
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 oddly named since it does not dispose the image from the info given to it. Maybe rename to replaceImage?

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

Comment on lines 1242 to 1244
// if (createPassiveListener) {
// _imageStream!.completer?.addPassiveListener(null);
// }
Copy link
Member

Choose a reason for hiding this comment

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

?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@dnfield
Copy link
Contributor Author

dnfield commented Sep 29, 2020

My idea to refactor Rawimage doesn't quite work, because it still involves trying to make a Widget hold on to a stateful object. The basic problem is if we give RawImage a clone of the image, it will never know when to dispose of it. State.dispose isn't correct because it might create multiple states.

// Give any interested parties a chance to listen to the stream before we
// potentially dispose it.
// scheduleMicrotask(() {
SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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].
Copy link
Contributor

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.

Copy link
Contributor Author

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));
Copy link
Contributor

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

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

Copy link
Contributor

@Hixie Hixie left a 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.

@Hixie
Copy link
Contributor

Hixie commented Oct 1, 2020

(LGTM)

@dnfield dnfield merged commit a795469 into flutter:master Oct 1, 2020
@dnfield dnfield deleted the image_dispose_better branch October 1, 2020 16:48
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 with some tiny nits.

/// void _updateImage(ImageInfo imageInfo, bool synchronousCall) {
/// setState(() {
/// // Trigger a build whenever the image changes.
/// _imageInfo?.image?.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 you dispose the _imageInfo directly here instead of the image?

/// @override
/// void dispose() {
/// _imageStream.removeListener(ImageStreamListener(_updateImage));
/// _imageInfo?.image?.dispose();
Copy link
Member

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
Copy link
Member

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)) {
Copy link
Member

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]
Copy link
Member

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.
Copy link
Member

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
Copy link
Member

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?

dnfield added a commit to dnfield/flutter that referenced this pull request Oct 1, 2020
dnfield added a commit that referenced this pull request Oct 1, 2020
* Revert "docs for image disposal (#67066)"

This reverts commit bcb2ac5.

* Revert "Dispose of images after using them (#66688)"

This reverts commit a795469.
@liyuqian
Copy link
Contributor

liyuqian commented Oct 2, 2020

Before the revert, we detected a big reduction in max memory usage. Look forward to the reland!

@liyuqian liyuqian added perf: memory Performance issues related to memory c: performance Relates to speed or footprint issues (see "perf:" labels) labels Oct 2, 2020
@kinarobin kinarobin mentioned this pull request Feb 23, 2021
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: tests "flutter test", flutter_test, or one of our tests c: performance Relates to speed or footprint issues (see "perf:" labels) framework flutter/packages/flutter repository. See also f: labels. perf: memory Performance issues related to memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

removing Image widgets does not reduce Graphics memory allocation

6 participants