-
Notifications
You must be signed in to change notification settings - Fork 29.6k
Live image cache #50318
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
Live image cache #50318
Conversation
|
Argh. I meant to open this as a draft. Oh well. |
| expect(imageStreamCompleter.listeners.length, 0); | ||
| // Make sure the second listener can be called re-entrantly. | ||
| listeners[0].onImage(null, null); | ||
| listeners[0].onImage(null, 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 is meant to land in #50316
| /// | ||
| /// To obtain an [ImageCacheLocation], use [FlutterImageCache.locationForKey] or | ||
| /// [ImageProvider.findCacheLocation]. | ||
| class ImageCacheLocation { |
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.
/cc @jamesderlin who was looking for a way to know if an image was pending in the cache.
d953dda to
257ef91
Compare
| final FlutterImageCache flutterImageCache = imageCache as FlutterImageCache; | ||
| expect(flutterImageCache.locationForKey(testImage).pending, true); | ||
| expect(flutterImageCache.locationForKey(testImage).weak, true); | ||
| flutterImageCache.clear(); | ||
| expect(flutterImageCache.locationForKey(testImage).pending, false); | ||
| expect(flutterImageCache.locationForKey(testImage).weak, true); | ||
| flutterImageCache.clearWeakImages(); | ||
| expect(flutterImageCache.locationForKey(testImage).pending, false); | ||
| expect(flutterImageCache.locationForKey(testImage).weak, false); |
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.
clear does remove the pending image, but this test no longer captures how to check if an image is pending or not.
This test could be independently rewritten without these changes to use the existing containsKey API, although the new API offers more robust ways to check if an image is pending.
| /// This method exists so that [FlutterImageCache] can add to its weak cache | ||
| /// once an [ImageStreamCompleter] has been created by this cache. | ||
| @protected | ||
| void _onImageStreamCompleterCreated(Object key, ImageStreamCompleter completer) {} |
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.
_handleImageStreamCompleterCreated per style guide
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.
Removing this since we're just going to break the API
|
|
||
| /// Clears any weak references to images in this cache. | ||
| /// | ||
| /// This should not ordinarily be called by applications. |
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.
Rather than saying it shouldn't be called, say when it should be called.
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
| /// This cache also exposes internal dimensions of the implementation, such as | ||
| /// checking whether an image for a given key is pending, weak, or completed. | ||
| /// See [ImageCacheLocation] for more details. | ||
| class FlutterImageCache extends ImageCache { |
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.
in the api design review meeting we discussed just merging FlutterImageCache into ImageCache, and make the test-only features be methods called debugFoo... with their bodies in asserts.
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 leaning towards making these non-debug/assert methods at this point - this could be valuable information at runtime (e.g. if an application wants to know how many live images are on the screen before decoding more). I'm also documenting [precacheImage] as relying on this behavior now.
| stream.removeListener(listener); | ||
| // Give callers at least one frame to subscribe to the image stream | ||
| // so that FlutterImageCache can weakly hold it without losing it. | ||
| SchedulerBinding.instance.scheduleFrameCallback((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.
should probably be schedulePostFrameCallback so that you don't actually schedule 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.
Done, and updated tests relevant to this to actually schedule a frame where needed.
| /// method. | ||
| ImageCache get imageCache => _imageCache; | ||
| ImageCache _imageCache; | ||
|
|
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.
please avoid the word "weak" throughout
| /// the network if they are referenced in the [putIfAbsent] method), but the raw | ||
| /// bits are kept in memory for as long as the application is using them. | ||
| /// | ||
| /// The cache also holds a list of "live" references. An image is considered |
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 paragraph above says that active images may also get removed from the cache, this paragraph talks about live images. Maybe specify in the above paragraph when "active" images can get evicted (e.g. when they no longer are "live").
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.
Also: It's probably not super clear what the difference between "actively in use" and live 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.
I'm removing the above paragraph - thanks for catching that.
| return image.completer; | ||
| } | ||
|
|
||
| result = _liveImages[key]; |
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.
Shouldn't these go back into the regular cache (if they fit) if they have been used again?
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, yes. I'll fix this.
| class ImageCache { | ||
| final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{}; | ||
| final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{}; | ||
| final Map<Object, ImageStreamCompleter> _liveImages = <Object, ImageStreamCompleter>{}; |
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.
For future readers: Maybe add a comment here what "live" means?
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
| } | ||
|
|
||
| /// The [ImageCacheLocation] information for the given `key`. | ||
| ImageCacheLocation locationForKey(Object key) => ImageCacheLocation._( |
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: per style guide, use => only if everything fits on one line.
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. I had this on one line, realized it was too long, and then forgot to change the fat arrow :)
| return _pendingImages[key] != null || _cache[key] != null; | ||
| } | ||
|
|
||
| /// The number of live images being held by the [ImageCache]. Compare with |
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: first paragraph should be one sentence summary.
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 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.
Split these
| // hence the need for the `didError` flag. | ||
| final Zone dangerZone = Zone.current.fork( | ||
| specification: ZoneSpecification( | ||
| handleUncaughtError: (Zone zone, ZoneDelegate delegate, Zone parent, Object error, StackTrace stackTrace) { |
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.
/cc @jonahwilliams
|
|
||
| final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[]; | ||
|
|
||
| /// A adds a callback to call when [removeListener] results in an empty |
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: "A adds a..." ??
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
| /// A adds a callback to call when [removeListener] results in an empty | ||
| /// list of listeners. | ||
| /// | ||
| /// This callback will never fire if [removeListener] is never called. |
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.
Feels odd that there's no way to remove a onlastlistenerremoved 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.
I don't have a use case for it. I also suspect that it would be better to track these by ID if we ever want to do that, to avoid the confusion about what happens when someone removes a duplicate listener. WDYT?
| /// as long as both images share the same key, and the image is held by the | ||
| /// cache. | ||
| /// | ||
| /// The cache may refuse to hold the image it is disabled, the image is too |
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.
Is this sentence missing an "if" or something?
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.
yes, done
| /// as long as both images share the same key, and the image is held by the | ||
| /// cache. | ||
| /// | ||
| /// The cache may refuse to hold the image it is disabled, the image is too |
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.
Also: there are two toos (one at the end of this line and one in the next line)
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
|
I've merged in the image tracing tests. This should be fully ready for review and landing on approval. |
611b5a8 to
d60d64e
Compare
|
(Fixed some bugs in my last push - I had forgotten to delete some code and had a bad assert in there) |
|
|
||
| /// The number of live images being held by the [ImageCache]. | ||
| /// | ||
| /// Compare with [ImageCache.currentSize] for completed held images. |
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.
Not sure what "completed held images" 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.
Missed a text replacement. done.
| /// | ||
| /// A [pending] image is one that has not completed yet. It may also be tracked | ||
| /// as [live] because something is listening 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.
nit: remove one blank line.
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 an error is added to a synchronous completer before a listener has been |
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.
Wait, isn't the other occurence also in this file? Could this be just be refactored into a private method? I don't think there's any danger of anybody accessing a private method from here?
| return; | ||
| } | ||
| _liveImages[key] = image; | ||
| image.completer.addOnLastListenerRemovedCallback(() { |
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 nobody ever listens to the stream (and hence this callback never fires)? It appears that we'd never remove the image from the live cache?
|
Ahh I got mixed up about the dangerzone stuff - I thought that one was on ImageCache. I'll refactor. |
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
|
Quick update on this: When I opened the PR, it was a breaking change. During the course of work on this PR, the tests it broke no longer were breaking (in fact, they disappeared entirely!). On landing it, it no longer breaks our compatibility guidelines (https://flutter.dev/docs/resources/compatibility). I'll still send an email to Flutter-announce explaining this. |
|
And if anyone would really like to see a migration guide for this, I can certainly write one :) |
This reverts commit 2f09d60.
|
Please I want to know version1.17.0 ImageCache add liveImage how to useful。 |
|
It is automatic |
Sorry, actually I mean want to ask what it does |
|
The design doc and linked PRs have more details, but in a nutshell this makes sure that once you resolve an image, it continues to be reachable as long as its listener count has not dropped to zero, even if it did not otherwise fit in the image cache. This is important especially for large images or cache configurations that do not allow for many images. |
WIP - this should not land until #50316; design doc for this is at https://flutter.dev/go/widget-tree-image-cache, although the approach has changed a bit since writing the doc. /cc @goderbauer @ds84182 @liyuqian @gaaclarke @ignatz @alml
Description
Related Issues
fixes #48731
fixes #49456
fixes #45406
Tests
I added the following tests:
Checklist
Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (
[x]). This will ensure a smooth and quick review process.///).flutter analyze --flutter-repo) does not report any problems on my PR.Breaking Change
Did any tests fail when you ran them? Please read Handling breaking changes.
I don't believe this is a breaking change, but once this is cleaned up a bit I'll manually run it through google's internal test suites.