Commit 25af2773 authored by Florian Huonder's avatar Florian Huonder Committed by Jonah Williams

Clearing pendingImages when the cache is cleared or evicted. (#23860)

parent d8b57c2a
...@@ -26,7 +26,7 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB ...@@ -26,7 +26,7 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
/// Generally this class is not used directly. The [ImageProvider] class and its /// Generally this class is not used directly. The [ImageProvider] class and its
/// subclasses automatically handle the caching of images. /// subclasses automatically handle the caching of images.
class ImageCache { class ImageCache {
final Map<Object, ImageStreamCompleter> _pendingImages = <Object, ImageStreamCompleter>{}; final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{}; final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
/// Maximum number of entries to store in the cache. /// Maximum number of entries to store in the cache.
...@@ -48,8 +48,7 @@ class ImageCache { ...@@ -48,8 +48,7 @@ class ImageCache {
return; return;
_maximumSize = value; _maximumSize = value;
if (maximumSize == 0) { if (maximumSize == 0) {
_cache.clear(); clear();
_currentSizeBytes = 0;
} else { } else {
_checkCacheSize(); _checkCacheSize();
} }
...@@ -78,8 +77,7 @@ class ImageCache { ...@@ -78,8 +77,7 @@ class ImageCache {
return; return;
_maximumSizeBytes = value; _maximumSizeBytes = value;
if (_maximumSizeBytes == 0) { if (_maximumSizeBytes == 0) {
_cache.clear(); clear();
_currentSizeBytes = 0;
} else { } else {
_checkCacheSize(); _checkCacheSize();
} }
...@@ -98,10 +96,15 @@ class ImageCache { ...@@ -98,10 +96,15 @@ class ImageCache {
/// cache, and when they complete they will be inserted as normal. /// cache, and when they complete they will be inserted as normal.
void clear() { void clear() {
_cache.clear(); _cache.clear();
_pendingImages.clear();
_currentSizeBytes = 0; _currentSizeBytes = 0;
} }
/// Evicts a single entry from the cache, returning true if successful. /// Evicts a single entry from the cache, returning true if successful.
/// Pending images waiting for completion are removed as well, returning true if successful.
///
/// When a pending image is removed the listener on it is removed as well to prevent
/// it from adding itself to the cache if it eventually completes.
/// ///
/// The [key] must be equal to an object used to cache an image in /// The [key] must be equal to an object used to cache an image in
/// [ImageCache.putIfAbsent]. /// [ImageCache.putIfAbsent].
...@@ -113,6 +116,11 @@ class ImageCache { ...@@ -113,6 +116,11 @@ class ImageCache {
/// ///
/// * [ImageProvider], for providing images to the [Image] widget. /// * [ImageProvider], for providing images to the [Image] widget.
bool evict(Object key) { bool evict(Object key) {
final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
return true;
}
final _CachedImage image = _cache.remove(key); final _CachedImage image = _cache.remove(key);
if (image != null) { if (image != null) {
_currentSizeBytes -= image.sizeBytes; _currentSizeBytes -= image.sizeBytes;
...@@ -134,7 +142,7 @@ class ImageCache { ...@@ -134,7 +142,7 @@ class ImageCache {
ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) { ImageStreamCompleter putIfAbsent(Object key, ImageStreamCompleter loader(), { ImageErrorListener onError }) {
assert(key != null); assert(key != null);
assert(loader != null); assert(loader != null);
ImageStreamCompleter result = _pendingImages[key]; ImageStreamCompleter result = _pendingImages[key]?.completer;
// Nothing needs to be done because the image hasn't loaded yet. // Nothing needs to be done because the image hasn't loaded yet.
if (result != null) if (result != null)
return result; return result;
...@@ -166,13 +174,16 @@ class ImageCache { ...@@ -166,13 +174,16 @@ class ImageCache {
_maximumSizeBytes = imageSize + 1000; _maximumSizeBytes = imageSize + 1000;
} }
_currentSizeBytes += imageSize; _currentSizeBytes += imageSize;
_pendingImages.remove(key); final _PendingImage pendingImage = _pendingImages.remove(key);
if (pendingImage != null) {
pendingImage.removeListener();
}
_cache[key] = image; _cache[key] = image;
result.removeListener(listener);
_checkCacheSize(); _checkCacheSize();
} }
if (maximumSize > 0 && maximumSizeBytes > 0) { if (maximumSize > 0 && maximumSizeBytes > 0) {
_pendingImages[key] = result; _pendingImages[key] = _PendingImage(result, listener);
result.addListener(listener); result.addListener(listener);
} }
return result; return result;
...@@ -199,3 +210,14 @@ class _CachedImage { ...@@ -199,3 +210,14 @@ class _CachedImage {
final ImageStreamCompleter completer; final ImageStreamCompleter completer;
final int sizeBytes; final int sizeBytes;
} }
class _PendingImage {
_PendingImage(this.completer, this.listener);
final ImageStreamCompleter completer;
final ImageListener listener;
void removeListener() {
completer.removeListener(listener);
}
}
...@@ -86,7 +86,6 @@ void main() { ...@@ -86,7 +86,6 @@ void main() {
expect(p.value, equals(16)); expect(p.value, equals(16));
// cache has three entries: 3(14), 4(15), 1(16) // cache has three entries: 3(14), 4(15), 1(16)
}); });
test('clear removes all images and resets cache size', () async { test('clear removes all images and resets cache size', () async {
...@@ -139,7 +138,72 @@ void main() { ...@@ -139,7 +138,72 @@ void main() {
expect(result, null); expect(result, null);
expect(caughtError, true); expect(caughtError, true);
}); });
test('already pending image is returned when it is put into the cache again', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
});
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
});
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer1);
});
test('pending image is removed when cache is cleared', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
});
imageCache.clear();
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
});
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test('pending image is removed when image is evicted', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
final TestImageStreamCompleter completer2 = TestImageStreamCompleter();
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
});
imageCache.evict(testImage);
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2;
});
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test('failed image can successfully be removed from the cache\'s pending images', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
const FailingTestImageProvider(1, 1, image: testImage).resolve(ImageConfiguration.empty).addListener((ImageInfo image, bool synchronousCall){}, onError: (dynamic exception, StackTrace stackTrace) {
final bool evicationResult = imageCache.evict(1);
expect(evicationResult, isTrue);
});
});
}); });
} }
...@@ -47,6 +47,15 @@ class TestImageProvider extends ImageProvider<int> { ...@@ -47,6 +47,15 @@ class TestImageProvider extends ImageProvider<int> {
String toString() => '$runtimeType($key, $imageValue)'; String toString() => '$runtimeType($key, $imageValue)';
} }
class FailingTestImageProvider extends TestImageProvider {
const FailingTestImageProvider(int key, int imageValue, { ui.Image image }) : super(key, imageValue, image: image);
@override
ImageStreamCompleter load(int key) {
return OneFrameImageStreamCompleter(Future<ImageInfo>.sync(() => Future<ImageInfo>.error('loading failed!')));
}
}
Future<ImageInfo> extractOneFrame(ImageStream stream) { Future<ImageInfo> extractOneFrame(ImageStream stream) {
final Completer<ImageInfo> completer = Completer<ImageInfo>(); final Completer<ImageInfo> completer = Completer<ImageInfo>();
void listener(ImageInfo image, bool synchronousCall) { void listener(ImageInfo image, bool synchronousCall) {
...@@ -84,3 +93,5 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> { ...@@ -84,3 +93,5 @@ class ErrorImageProvider extends ImageProvider<ErrorImageProvider> {
return SynchronousFuture<ErrorImageProvider>(this); return SynchronousFuture<ErrorImageProvider>(this);
} }
} }
class TestImageStreamCompleter extends ImageStreamCompleter {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment