Unverified Commit 419a2853 authored by Dan Field's avatar Dan Field Committed by GitHub

Revert "Live image cache" (#51441)

* Revert "Live image cache (#51249)"

This reverts commit e2dcdb60.

* fix tests for other commit
parent 7cbe55cf
...@@ -20,7 +20,7 @@ void main() { ...@@ -20,7 +20,7 @@ void main() {
final developer.ServiceProtocolInfo info = await developer.Service.getInfo(); final developer.ServiceProtocolInfo info = await developer.Service.getInfo();
if (info.serverUri == null) { if (info.serverUri == null) {
fail('This test _must_ be run with --enable-vmservice.'); throw TestFailure('This test _must_ be run with --enable-vmservice.');
} }
await timelineObtainer.connect(info.serverUri); await timelineObtainer.connect(info.serverUri);
await timelineObtainer.setDartFlags(); await timelineObtainer.setDartFlags();
...@@ -58,8 +58,7 @@ void main() { ...@@ -58,8 +58,7 @@ void main() {
'name': 'ImageCache.clear', 'name': 'ImageCache.clear',
'args': <String, dynamic>{ 'args': <String, dynamic>{
'pendingImages': 1, 'pendingImages': 1,
'keepAliveImages': 0, 'cachedImages': 0,
'liveImages': 1,
'currentSizeInBytes': 0, 'currentSizeInBytes': 0,
'isolateId': isolateId, 'isolateId': isolateId,
} }
...@@ -150,7 +149,7 @@ class TimelineObtainer { ...@@ -150,7 +149,7 @@ class TimelineObtainer {
Future<void> close() async { Future<void> close() async {
expect(_completers, isEmpty); expect(_completers, isEmpty);
await _observatorySocket?.close(); await _observatorySocket.close();
} }
} }
......
...@@ -96,7 +96,6 @@ mixin PaintingBinding on BindingBase, ServicesBinding { ...@@ -96,7 +96,6 @@ mixin PaintingBinding on BindingBase, ServicesBinding {
void evict(String asset) { void evict(String asset) {
super.evict(asset); super.evict(asset);
imageCache.clear(); imageCache.clear();
imageCache.clearLiveImages();
} }
/// Listenable that notifies when the available fonts on the system have /// Listenable that notifies when the available fonts on the system have
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:developer'; import 'dart:developer';
import 'dart:ui' show hashValues;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -16,24 +15,18 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB ...@@ -16,24 +15,18 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
/// ///
/// Implements a least-recently-used cache of up to 1000 images, and up to 100 /// Implements a least-recently-used cache of up to 1000 images, and up to 100
/// MB. The maximum size can be adjusted using [maximumSize] and /// MB. The maximum size can be adjusted using [maximumSize] and
/// [maximumSizeBytes]. /// [maximumSizeBytes]. Images that are actively in use (i.e. to which the
/// /// application is holding references, either via [ImageStream] objects,
/// The cache also holds a list of "live" references. An image is considered /// [ImageStreamCompleter] objects, [ImageInfo] objects, or raw [dart:ui.Image]
/// live if its [ImageStreamCompleter]'s listener count has never dropped to /// objects) may get evicted from the cache (and thus need to be refetched from
/// zero after adding at least one listener. The cache uses /// the network if they are referenced in the [putIfAbsent] method), but the raw
/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] to determine when /// bits are kept in memory for as long as the application is using them.
/// this has happened.
/// ///
/// The [putIfAbsent] method is the main entry-point to the cache API. It /// The [putIfAbsent] method is the main entry-point to the cache API. It
/// returns the previously cached [ImageStreamCompleter] for the given key, if /// returns the previously cached [ImageStreamCompleter] for the given key, if
/// available; if not, it calls the given callback to obtain it first. In either /// available; if not, it calls the given callback to obtain it first. In either
/// case, the key is moved to the "most recently used" position. /// case, the key is moved to the "most recently used" position.
/// ///
/// A caller can determine whether an image is already in the cache by using
/// [containsKey], which will return true if the image is tracked by the cache
/// in a pending or compelted state. More fine grained information is available
/// by using the [statusForKey] method.
///
/// 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.
/// ///
...@@ -78,11 +71,6 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB ...@@ -78,11 +71,6 @@ const int _kDefaultSizeBytes = 100 << 20; // 100 MiB
class ImageCache { class ImageCache {
final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{}; final Map<Object, _PendingImage> _pendingImages = <Object, _PendingImage>{};
final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{}; final Map<Object, _CachedImage> _cache = <Object, _CachedImage>{};
/// ImageStreamCompleters with at least one listener. These images may or may
/// not fit into the _pendingImages or _cache objects.
///
/// Unlike _cache, the [_CachedImage] for this may have a null byte size.
final Map<Object, _CachedImage> _liveImages = <Object, _CachedImage>{};
/// Maximum number of entries to store in the cache. /// Maximum number of entries to store in the cache.
/// ///
...@@ -162,27 +150,20 @@ class ImageCache { ...@@ -162,27 +150,20 @@ class ImageCache {
int get currentSizeBytes => _currentSizeBytes; int get currentSizeBytes => _currentSizeBytes;
int _currentSizeBytes = 0; int _currentSizeBytes = 0;
/// Evicts all pending and keepAlive entries from the cache. /// Evicts all entries from the cache.
/// ///
/// This is useful if, for instance, the root asset bundle has been updated /// This is useful if, for instance, the root asset bundle has been updated
/// and therefore new images must be obtained. /// and therefore new images must be obtained.
/// ///
/// Images which have not finished loading yet will not be removed from the /// Images which have not finished loading yet will not be removed from the
/// cache, and when they complete they will be inserted as normal. /// cache, and when they complete they will be inserted as normal.
///
/// This method does not clear live references to images, since clearing those
/// would not reduce memory pressure. Such images still have listeners in the
/// application code, and will still remain resident in memory.
///
/// To clear live references, use [clearLiveImages].
void clear() { void clear() {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.instantSync( Timeline.instantSync(
'ImageCache.clear', 'ImageCache.clear',
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'pendingImages': _pendingImages.length, 'pendingImages': _pendingImages.length,
'keepAliveImages': _cache.length, 'cachedImages': _cache.length,
'liveImages': _liveImages.length,
'currentSizeInBytes': _currentSizeBytes, 'currentSizeInBytes': _currentSizeBytes,
}, },
); );
...@@ -193,23 +174,11 @@ class ImageCache { ...@@ -193,23 +174,11 @@ class ImageCache {
} }
/// 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 /// Pending images waiting for completion are removed as well, returning true
/// if successful. When a pending image is removed the listener on it is /// if successful.
/// removed as well to prevent it from adding itself to the cache if it
/// eventually completes.
/// ///
/// If this method removes a pending image, it will also remove /// When a pending image is removed the listener on it is removed as well to
/// the corresponding live tracking of the image, since it is no longer clear /// prevent it from adding itself to the cache if it eventually completes.
/// if the image will ever complete or have any listeners, and failing to
/// remove the live reference could leave the cache in a state where all
/// subsequent calls to [putIfAbsent] will return an [ImageStreamCompleter]
/// that will never complete.
///
/// If this method removes a completed image, it will _not_ remove the live
/// reference to the image, which will only be cleared when the listener
/// count on the completer drops to zero. To clear live image references,
/// whether completed or not, use [clearLiveImages].
/// ///
/// 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].
...@@ -229,19 +198,13 @@ class ImageCache { ...@@ -229,19 +198,13 @@ class ImageCache {
}); });
} }
pendingImage.removeListener(); pendingImage.removeListener();
// Remove from live images - the cache will not be able to mark
// it as complete, and it might be getting evicted because it
// will never complete, e.g. it was loaded in a FakeAsync zone.
// In such a case, we need to make sure subsequent calls to
// putIfAbsent don't return this image that may never complete.
_liveImages.remove(key);
return true; return true;
} }
final _CachedImage image = _cache.remove(key); final _CachedImage image = _cache.remove(key);
if (image != null) { if (image != null) {
if (!kReleaseMode) { if (!kReleaseMode) {
Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{ Timeline.instantSync('ImageCache.evict', arguments: <String, dynamic>{
'type': 'keepAlive', 'type': 'completed',
'sizeiInBytes': image.sizeBytes, 'sizeiInBytes': image.sizeBytes,
}); });
} }
...@@ -256,34 +219,6 @@ class ImageCache { ...@@ -256,34 +219,6 @@ class ImageCache {
return false; return false;
} }
/// Updates the least recently used image cache with this image, if it is
/// less than the [maximumSizeBytes] of this cache.
///
/// Resizes the cache as appropriate to maintain the constraints of
/// [maximumSize] and [maximumSizeBytes].
void _touch(Object key, _CachedImage image, TimelineTask timelineTask) {
assert(timelineTask != null);
if (image.sizeBytes != null && image.sizeBytes <= maximumSizeBytes) {
_currentSizeBytes += image.sizeBytes;
_cache[key] = image;
_checkCacheSize(timelineTask);
}
}
void _trackLiveImage(Object key, _CachedImage image) {
// Avoid adding unnecessary callbacks to the completer.
_liveImages.putIfAbsent(key, () {
// Even if no callers to ImageProvider.resolve have listened to the stream,
// the cache is listening to the stream and will remove itself once the
// image completes to move it from pending to keepAlive.
// Even if the cache size is 0, we still add this listener.
image.completer.addOnLastListenerRemovedCallback(() {
_liveImages.remove(key);
});
return image;
});
}
/// Returns the previously cached [ImageStream] for the given key, if available; /// Returns the previously cached [ImageStream] for the given key, if available;
/// if not, calls the given callback to obtain it first. In either case, the /// if not, calls the given callback to obtain it first. In either case, the
/// key is moved to the "most recently used" position. /// key is moved to the "most recently used" position.
...@@ -317,32 +252,17 @@ class ImageCache { ...@@ -317,32 +252,17 @@ class ImageCache {
} }
// Remove the provider from the list so that we can move it to the // Remove the provider from the list so that we can move it to the
// recently used position below. // recently used position below.
// Don't use _touch here, which would trigger a check on cache size that is
// not needed since this is just moving an existing cache entry to the head.
final _CachedImage image = _cache.remove(key); final _CachedImage image = _cache.remove(key);
if (image != null) { if (image != null) {
if (!kReleaseMode) { if (!kReleaseMode) {
timelineTask.finish(arguments: <String, dynamic>{'result': 'keepAlive'}); timelineTask.finish(arguments: <String, dynamic>{'result': 'completed'});
} }
// The image might have been keptAlive but had no listeners (so not live).
// Make sure the cache starts tracking it as live again.
_trackLiveImage(key, image);
_cache[key] = image; _cache[key] = image;
return image.completer; return image.completer;
} }
final _CachedImage liveImage = _liveImages[key];
if (liveImage != null) {
_touch(key, liveImage, timelineTask);
if (!kReleaseMode) {
timelineTask.finish(arguments: <String, dynamic>{'result': 'keepAlive'});
}
return liveImage.completer;
}
try { try {
result = loader(); result = loader();
_trackLiveImage(key, _CachedImage(result, null));
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (!kReleaseMode) { if (!kReleaseMode) {
timelineTask.finish(arguments: <String, dynamic>{ timelineTask.finish(arguments: <String, dynamic>{
...@@ -362,37 +282,21 @@ class ImageCache { ...@@ -362,37 +282,21 @@ class ImageCache {
if (!kReleaseMode) { if (!kReleaseMode) {
listenerTask = TimelineTask(parent: timelineTask)..start('listener'); listenerTask = TimelineTask(parent: timelineTask)..start('listener');
} }
// If we're doing tracing, we need to make sure that we don't try to finish
// the trace entry multiple times if we get re-entrant calls from a multi-
// frame provider here.
bool listenedOnce = false; bool listenedOnce = false;
// We shouldn't use the _pendingImages map if the cache is disabled, but we
// will have to listen to the image at least once so we don't leak it in
// the live image tracking.
// If the cache is disabled, this variable will be set.
_PendingImage untrackedPendingImage;
void listener(ImageInfo info, bool syncCall) { void listener(ImageInfo info, bool syncCall) {
// Images that fail to load don't contribute to cache size. // Images that fail to load don't contribute to cache size.
final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4; final int imageSize = info?.image == null ? 0 : info.image.height * info.image.width * 4;
final _CachedImage image = _CachedImage(result, imageSize); final _CachedImage image = _CachedImage(result, imageSize);
if (!_liveImages.containsKey(key)) { final _PendingImage pendingImage = _pendingImages.remove(key);
assert(syncCall);
result.addOnLastListenerRemovedCallback(() {
_liveImages.remove(key);
});
}
_liveImages[key] = image;
final _PendingImage pendingImage = untrackedPendingImage ?? _pendingImages.remove(key);
if (pendingImage != null) { if (pendingImage != null) {
pendingImage.removeListener(); pendingImage.removeListener();
} }
// Only touch if the cache was enabled when resolve was initially called.
if (untrackedPendingImage == null) {
_touch(key, image, listenerTask);
}
if (imageSize <= maximumSizeBytes) {
_currentSizeBytes += imageSize;
_cache[key] = image;
_checkCacheSize(listenerTask);
}
if (!kReleaseMode && !listenedOnce) { if (!kReleaseMode && !listenedOnce) {
listenerTask.finish(arguments: <String, dynamic>{ listenerTask.finish(arguments: <String, dynamic>{
'syncCall': syncCall, 'syncCall': syncCall,
...@@ -405,58 +309,20 @@ class ImageCache { ...@@ -405,58 +309,20 @@ class ImageCache {
} }
listenedOnce = true; listenedOnce = true;
} }
final ImageStreamListener streamListener = ImageStreamListener(listener);
if (maximumSize > 0 && maximumSizeBytes > 0) { if (maximumSize > 0 && maximumSizeBytes > 0) {
final ImageStreamListener streamListener = ImageStreamListener(listener);
_pendingImages[key] = _PendingImage(result, streamListener); _pendingImages[key] = _PendingImage(result, streamListener);
} else { // Listener is removed in [_PendingImage.removeListener].
untrackedPendingImage = _PendingImage(result, streamListener); result.addListener(streamListener);
} }
// Listener is removed in [_PendingImage.removeListener].
result.addListener(streamListener);
return result; return result;
} }
/// The [ImageCacheStatus] information for the given `key`.
ImageCacheStatus statusForKey(Object key) {
return ImageCacheStatus._(
pending: _pendingImages.containsKey(key),
keepAlive: _cache.containsKey(key),
live: _liveImages.containsKey(key),
);
}
/// Returns whether this `key` has been previously added by [putIfAbsent]. /// Returns whether this `key` has been previously added by [putIfAbsent].
bool containsKey(Object key) { bool containsKey(Object key) {
return _pendingImages[key] != null || _cache[key] != null; return _pendingImages[key] != null || _cache[key] != null;
} }
/// The number of live images being held by the [ImageCache].
///
/// Compare with [ImageCache.currentSize] for keepAlive images.
int get liveImageCount => _liveImages.length;
/// The number of images being tracked as pending in the [ImageCache].
///
/// Compare with [ImageCache.currentSize] for keepAlive images.
int get pendingImageCount => _pendingImages.length;
/// Clears any live references to images in this cache.
///
/// An image is considered live if its [ImageStreamCompleter] has never hit
/// zero listeners after adding at least one listener. The
/// [ImageStreamCompleter.addOnLastListenerRemovedCallback] is used to
/// determine when this has happened.
///
/// This is called after a hot reload to evict any stale references to image
/// data for assets that have changed. Calling this method does not relieve
/// memory pressure, since the live image caching only tracks image instances
/// that are also being held by at least one other object.
void clearLiveImages() {
_liveImages.clear();
}
// Remove images from the cache until both the length and bytes are below // Remove images from the cache until both the length and bytes are below
// maximum, or the cache is empty. // maximum, or the cache is empty.
void _checkCacheSize(TimelineTask timelineTask) { void _checkCacheSize(TimelineTask timelineTask) {
...@@ -488,76 +354,6 @@ class ImageCache { ...@@ -488,76 +354,6 @@ class ImageCache {
} }
} }
/// Information about how the [ImageCache] is tracking an image.
///
/// A [pending] image is one that has not completed yet. It may also be tracked
/// as [live] because something is listening to it.
///
/// A [keepAlive] image is being held in the cache, which uses Least Recently
/// Used semantics to determine when to evict an image. These images are subject
/// to eviction based on [ImageCache.maximumSizeBytes] and
/// [ImageCache.maximumSize]. It may be [live], but not [pending].
///
/// A [live] image is being held until its [ImageStreamCompleter] has no more
/// listeners. It may also be [pending] or [keepAlive].
///
/// An [untracked] image is not being cached.
///
/// To obtain an [ImageCacheStatus], use [ImageCache.statusForKey] or
/// [ImageProvider.obtainCacheStatus].
class ImageCacheStatus {
const ImageCacheStatus._({
this.pending = false,
this.keepAlive = false,
this.live = false,
}) : assert(!pending || !keepAlive);
/// An image that has been submitted to [ImageCache.putIfAbsent], but
/// not yet completed.
final bool pending;
/// An image that has been submitted to [ImageCache.putIfAbsent], has
/// completed, fits based on the sizing rules of the cache, and has not been
/// evicted.
///
/// Such images will be kept alive even if [live] is false, as long
/// as they have not been evicted from the cache based on its sizing rules.
final bool keepAlive;
/// An image that has been submitted to [ImageCache.putIfAbsent] and has at
/// least one listener on its [ImageStreamCompleter].
///
/// Such images may also be [keepAlive] if they fit in the cache based on its
/// sizing rules. They may also be [pending] if they have not yet resolved.
final bool live;
/// An image that is tracked in some way by the [ImageCache], whether
/// [pending], [keepAlive], or [live].
bool get tracked => pending || keepAlive || live;
/// An image that either has not been submitted to
/// [ImageCache.putIfAbsent] or has otherwise been evicted from the
/// [keepAlive] and [live] caches.
bool get untracked => !pending && !keepAlive && !live;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ImageCacheStatus
&& other.pending == pending
&& other.keepAlive == keepAlive
&& other.live == live;
}
@override
int get hashCode => hashValues(pending, keepAlive, live);
@override
String toString() => '${objectRuntimeType(this, 'ImageCacheStatus')}(pending: $pending, live: $live, keepAlive: $keepAlive)';
}
class _CachedImage { class _CachedImage {
_CachedImage(this.completer, this.sizeBytes); _CachedImage(this.completer, this.sizeBytes);
......
...@@ -17,12 +17,6 @@ import 'binding.dart'; ...@@ -17,12 +17,6 @@ import 'binding.dart';
import 'image_cache.dart'; import 'image_cache.dart';
import 'image_stream.dart'; import 'image_stream.dart';
/// Signature for the callback taken by [_createErrorHandlerAndKey].
typedef _KeyAndErrorHandlerCallback<T> = void Function(T key, ImageErrorListener handleError);
/// Signature used for error handling by [_createErrorHandlerAndKey].
typedef _AsyncKeyErrorHandler<T> = Future<void> Function(T key, dynamic exception, StackTrace stack);
/// Configuration information passed to the [ImageProvider.resolve] method to /// Configuration information passed to the [ImageProvider.resolve] method to
/// select a specific image. /// select a specific image.
/// ///
...@@ -324,28 +318,7 @@ abstract class ImageProvider<T> { ...@@ -324,28 +318,7 @@ abstract class ImageProvider<T> {
final ImageStream stream = createStream(configuration); final ImageStream stream = createStream(configuration);
// Load the key (potentially asynchronously), set up an error handling zone, // Load the key (potentially asynchronously), set up an error handling zone,
// and call resolveStreamForKey. // and call resolveStreamForKey.
_createErrorHandlerAndKey( _createErrorHandlerAndKey(configuration, stream);
configuration,
(T key, ImageErrorListener errorHandler) {
resolveStreamForKey(configuration, stream, key, errorHandler);
},
(T key, dynamic exception, StackTrace stack) async {
await null; // wait an event turn in case a listener has been added to the image stream.
final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
stream.setCompleter(imageCompleter);
imageCompleter.setError(
exception: exception,
stack: stack,
context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
},
);
},
);
return stream; return stream;
} }
...@@ -359,66 +332,30 @@ abstract class ImageProvider<T> { ...@@ -359,66 +332,30 @@ abstract class ImageProvider<T> {
return ImageStream(); return ImageStream();
} }
/// Returns the cache location for the key that this [ImageProvider] creates. void _createErrorHandlerAndKey(ImageConfiguration configuration, ImageStream stream) {
///
/// The location may be [ImageCacheStatus.untracked], indicating that this
/// image provider's key is not available in the [ImageCache].
///
/// The `cache` and `configuration` parameters must not be null. If the
/// `handleError` parameter is null, errors will be reported to
/// [FlutterError.onError], and the method will return null.
///
/// A completed return value of null indicates that an error has occurred.
Future<ImageCacheStatus> obtainCacheStatus({
@required ImageConfiguration configuration,
ImageErrorListener handleError,
}) {
assert(configuration != null); assert(configuration != null);
final Completer<ImageCacheStatus> completer = Completer<ImageCacheStatus>(); assert(stream != null);
_createErrorHandlerAndKey(
configuration,
(T key, ImageErrorListener innerHandleError) {
completer.complete(PaintingBinding.instance.imageCache.statusForKey(key));
},
(T key, dynamic exception, StackTrace stack) async {
if (handleError != null) {
handleError(exception, stack);
} else {
FlutterError.onError(FlutterErrorDetails(
context: ErrorDescription('while checking the cache location of an image'),
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
yield DiagnosticsProperty<T>('Image key', key, defaultValue: null);
},
exception: exception,
stack: stack,
));
completer.complete(null);
}
},
);
return completer.future;
}
/// This method is used by both [resolve] and [obtainCacheStatus] to ensure
/// that errors thrown during key creation are handled whether synchronous or
/// asynchronous.
void _createErrorHandlerAndKey(
ImageConfiguration configuration,
_KeyAndErrorHandlerCallback<T> successCallback,
_AsyncKeyErrorHandler<T> errorCallback,
) {
T obtainedKey; T obtainedKey;
bool didError = false; bool didError = false;
Future<void> handleError(dynamic exception, StackTrace stack) async { Future<void> handleError(dynamic exception, StackTrace stack) async {
if (didError) { if (didError) {
return; return;
} }
if (!didError) {
errorCallback(obtainedKey, exception, stack);
}
didError = true; didError = true;
await null; // wait an event turn in case a listener has been added to the image stream.
final _ErrorImageCompleter imageCompleter = _ErrorImageCompleter();
stream.setCompleter(imageCompleter);
imageCompleter.setError(
exception: exception,
stack: stack,
context: ErrorDescription('while resolving an image'),
silent: true, // could be a network error or whatnot
informationCollector: () sync* {
yield DiagnosticsProperty<ImageProvider>('Image provider', this);
yield DiagnosticsProperty<ImageConfiguration>('Image configuration', configuration);
yield DiagnosticsProperty<T>('Image key', obtainedKey, defaultValue: null);
},
);
} }
// If an error is added to a synchronous completer before a listener has been // If an error is added to a synchronous completer before a listener has been
...@@ -447,7 +384,7 @@ abstract class ImageProvider<T> { ...@@ -447,7 +384,7 @@ abstract class ImageProvider<T> {
key.then<void>((T key) { key.then<void>((T key) {
obtainedKey = key; obtainedKey = key;
try { try {
successCallback(key, handleError); resolveStreamForKey(configuration, stream, key, handleError);
} catch (error, stackTrace) { } catch (error, stackTrace) {
handleError(error, stackTrace); handleError(error, stackTrace);
} }
......
...@@ -203,11 +203,6 @@ class ImageChunkEvent extends Diagnosticable { ...@@ -203,11 +203,6 @@ class ImageChunkEvent extends Diagnosticable {
/// ///
/// ImageStream objects are backed by [ImageStreamCompleter] objects. /// ImageStream objects are backed by [ImageStreamCompleter] objects.
/// ///
/// The [ImageCache] will consider an image to be live until the listener count
/// drops to zero after adding at least one listener. The
/// [addOnLastListenerRemovedCallback] method is used for tracking this
/// information.
///
/// See also: /// See also:
/// ///
/// * [ImageProvider], which has an example that includes the use of an /// * [ImageProvider], which has an example that includes the use of an
...@@ -397,23 +392,6 @@ abstract class ImageStreamCompleter extends Diagnosticable { ...@@ -397,23 +392,6 @@ abstract class ImageStreamCompleter extends Diagnosticable {
break; break;
} }
} }
if (_listeners.isEmpty) {
for (final VoidCallback callback in _onLastListenerRemovedCallbacks) {
callback();
}
_onLastListenerRemovedCallbacks.clear();
}
}
final List<VoidCallback> _onLastListenerRemovedCallbacks = <VoidCallback>[];
/// Adds a callback to call when [removeListener] results in an empty
/// list of listeners.
///
/// This callback will never fire if [removeListener] is never called.
void addOnLastListenerRemovedCallback(VoidCallback callback) {
assert(callback != null);
_onLastListenerRemovedCallbacks.add(callback);
} }
/// Calls all the registered listeners to notify them of a new image. /// Calls all the registered listeners to notify them of a new image.
......
...@@ -8,7 +8,6 @@ import 'dart:typed_data'; ...@@ -8,7 +8,6 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart'; import 'package:flutter/semantics.dart';
...@@ -66,30 +65,7 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si ...@@ -66,30 +65,7 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si
/// If the image is later used by an [Image] or [BoxDecoration] or [FadeInImage], /// If the image is later used by an [Image] or [BoxDecoration] or [FadeInImage],
/// it will probably be loaded faster. The consumer of the image does not need /// it will probably be loaded faster. The consumer of the image does not need
/// to use the same [ImageProvider] instance. The [ImageCache] will find the image /// to use the same [ImageProvider] instance. The [ImageCache] will find the image
/// as long as both images share the same key, and the image is held by the /// as long as both images share the same key.
/// cache.
///
/// The cache may refuse to hold the image if it is disabled, the image is too
/// large, or some other criteria implemented by a custom [ImageCache]
/// implementation.
///
/// The [ImageCache] holds a reference to all images passed to [putIfAbsent] as
/// long as their [ImageStreamCompleter] has at least one listener. This method
/// will wait until the end of the frame after its future completes before
/// releasing its own listener. This gives callers a chance to listen to the
/// stream if necessary. A caller can determine if the image ended up in the
/// cache by calling [ImageProvider.obtainCacheStatus]. If it is only held as
/// [ImageCacheStatus.live], and the caller wishes to keep the resolved
/// image in memory, the caller should immediately call `provider.resolve` and
/// add a listener to the returned [ImageStream]. The image will remain pinned
/// in memory at least until the caller removes its listener from the stream,
/// even if it would not otherwise fit into the cache.
///
/// Callers should be cautious about pinning large images or a large number of
/// images in memory, as this can result in running out of memory and being
/// killed by the operating system. The lower the avilable physical memory, the
/// more susceptible callers will be to running into OOM issues. These issues
/// manifest as immediate process death, sometimes with no other error messages.
/// ///
/// The [BuildContext] and [Size] are used to select an image configuration /// The [BuildContext] and [Size] are used to select an image configuration
/// (see [createLocalImageConfiguration]). /// (see [createLocalImageConfiguration]).
...@@ -115,12 +91,7 @@ Future<void> precacheImage( ...@@ -115,12 +91,7 @@ Future<void> precacheImage(
if (!completer.isCompleted) { if (!completer.isCompleted) {
completer.complete(); completer.complete();
} }
// Give callers until at least the end of the frame to subscribe to the stream.removeListener(listener);
// image stream.
// See ImageCache._liveImages
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
stream.removeListener(listener);
});
}, },
onError: (dynamic exception, StackTrace stackTrace) { onError: (dynamic exception, StackTrace stackTrace) {
if (!completer.isCompleted) { if (!completer.isCompleted) {
......
...@@ -3,10 +3,7 @@ ...@@ -3,10 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:typed_data' show Uint8List; import 'dart:typed_data' show Uint8List;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
...@@ -27,88 +24,4 @@ void main() { ...@@ -27,88 +24,4 @@ void main() {
}); });
expect(binding.instantiateImageCodecCalledCount, 1); expect(binding.instantiateImageCodecCalledCount, 1);
}); });
test('evict clears live references', () async {
final TestPaintingBinding binding = TestPaintingBinding();
expect(binding.imageCache.clearCount, 0);
expect(binding.imageCache.liveClearCount, 0);
binding.evict('/path/to/asset.png');
expect(binding.imageCache.clearCount, 1);
expect(binding.imageCache.liveClearCount, 1);
});
} }
class TestBindingBase implements BindingBase {
@override
void initInstances() {}
@override
void initServiceExtensions() {}
@override
Future<void> lockEvents(Future<void> Function() callback) async {}
@override
bool get locked => throw UnimplementedError();
@override
Future<void> performReassemble() {
throw UnimplementedError();
}
@override
void postEvent(String eventKind, Map<String, dynamic> eventData) {}
@override
Future<void> reassembleApplication() {
throw UnimplementedError();
}
@override
void registerBoolServiceExtension({String name, AsyncValueGetter<bool> getter, AsyncValueSetter<bool> setter}) {}
@override
void registerNumericServiceExtension({String name, AsyncValueGetter<double> getter, AsyncValueSetter<double> setter}) {}
@override
void registerServiceExtension({String name, ServiceExtensionCallback callback}) {}
@override
void registerSignalServiceExtension({String name, AsyncCallback callback}) {}
@override
void registerStringServiceExtension({String name, AsyncValueGetter<String> getter, AsyncValueSetter<String> setter}) {}
@override
void unlocked() {}
@override
Window get window => throw UnimplementedError();
}
class TestPaintingBinding extends TestBindingBase with ServicesBinding, PaintingBinding {
@override
final FakeImageCache imageCache = FakeImageCache();
@override
ImageCache createImageCache() => imageCache;
}
class FakeImageCache extends ImageCache {
int clearCount = 0;
int liveClearCount = 0;
@override
void clear() {
clearCount += 1;
super.clear();
}
@override
void clearLiveImages() {
liveClearCount += 1;
super.clearLiveImages();
}
}
\ No newline at end of file
...@@ -9,14 +9,13 @@ import '../rendering/rendering_tester.dart'; ...@@ -9,14 +9,13 @@ import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart'; import 'mocks_for_image_cache.dart';
void main() { void main() {
group('ImageCache', () { group(ImageCache, () {
setUpAll(() { setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache TestRenderingFlutterBinding(); // initializes the imageCache
}); });
tearDown(() { tearDown(() {
imageCache.clear(); imageCache.clear();
imageCache.clearLiveImages();
imageCache.maximumSize = 1000; imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760; imageCache.maximumSizeBytes = 10485760;
}); });
...@@ -170,14 +169,7 @@ void main() { ...@@ -170,14 +169,7 @@ void main() {
return completer1; return completer1;
}) as TestImageStreamCompleter; }) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
imageCache.clear(); imageCache.clear();
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
imageCache.clearLiveImages();
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, false);
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () { final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage, () {
return completer2; return completer2;
...@@ -248,106 +240,7 @@ void main() { ...@@ -248,106 +240,7 @@ void main() {
expect(resultingCompleter1, completer1); expect(resultingCompleter1, completer1);
expect(imageCache.containsKey(testImage), true); expect(imageCache.containsKey(testImage), true);
});
test('putIfAbsent updates LRU properties of a live image', () async {
imageCache.maximumSize = 1;
const TestImage testImage = TestImage(width: 8, height: 8);
const TestImage testImage2 = TestImage(width: 10, height: 10);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()..testSetImage(testImage);
final TestImageStreamCompleter completer2 = TestImageStreamCompleter()..testSetImage(testImage2);
completer1.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
final TestImageStreamCompleter resultingCompleter1 = imageCache.putIfAbsent(testImage, () {
return completer1;
}) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).keepAlive, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage2).untracked, true);
final TestImageStreamCompleter resultingCompleter2 = imageCache.putIfAbsent(testImage2, () {
return completer2;
}) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).keepAlive, false); // evicted
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage2).pending, false);
expect(imageCache.statusForKey(testImage2).keepAlive, true); // took the LRU spot.
expect(imageCache.statusForKey(testImage2).live, false); // no listeners
expect(resultingCompleter1, completer1);
expect(resultingCompleter2, completer2);
});
test('Live image cache avoids leaks of unlistened streams', () async {
imageCache.maximumSize = 3;
const TestImageProvider(1, 1)..resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2)..resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3)..resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4)..resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5)..resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6)..resolve(ImageConfiguration.empty);
// wait an event loop to let image resolution process.
await null;
expect(imageCache.currentSize, 3);
expect(imageCache.liveImageCount, 0);
});
test('Disabled image cache does not leak live images', () async {
imageCache.maximumSize = 0;
const TestImageProvider(1, 1)..resolve(ImageConfiguration.empty);
const TestImageProvider(2, 2)..resolve(ImageConfiguration.empty);
const TestImageProvider(3, 3)..resolve(ImageConfiguration.empty);
const TestImageProvider(4, 4)..resolve(ImageConfiguration.empty);
const TestImageProvider(5, 5)..resolve(ImageConfiguration.empty);
const TestImageProvider(6, 6)..resolve(ImageConfiguration.empty);
// wait an event loop to let image resolution process.
await null;
expect(imageCache.currentSize, 0);
expect(imageCache.liveImageCount, 0);
});
test('Evicting a pending image clears the live image', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter();
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).untracked, true);
});
test('Evicting a completed image does not clear the live image', () async {
const TestImage testImage = TestImage(width: 8, height: 8);
final TestImageStreamCompleter completer1 = TestImageStreamCompleter()
..testSetImage(testImage)
..addListener(ImageStreamListener((ImageInfo info, bool syncCall) {}));
imageCache.putIfAbsent(testImage, () => completer1);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, true);
imageCache.evict(testImage);
expect(imageCache.statusForKey(testImage).pending, false);
expect(imageCache.statusForKey(testImage).live, true);
expect(imageCache.statusForKey(testImage).keepAlive, false);
}); });
}); });
} }
...@@ -36,7 +36,6 @@ void main() { ...@@ -36,7 +36,6 @@ void main() {
tearDown(() { tearDown(() {
FlutterError.onError = oldError; FlutterError.onError = oldError;
PaintingBinding.instance.imageCache.clear(); PaintingBinding.instance.imageCache.clear();
PaintingBinding.instance.imageCache.clearLiveImages();
}); });
group('ImageProvider', () { group('ImageProvider', () {
...@@ -53,18 +52,15 @@ void main() { ...@@ -53,18 +52,15 @@ void main() {
const ImageProvider provider = ExactAssetImage('does-not-exist'); const ImageProvider provider = ExactAssetImage('does-not-exist');
final Object key = await provider.obtainKey(ImageConfiguration.empty); final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true); expect(imageCache.containsKey(provider), false);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty); provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true); expect(imageCache.containsKey(key), true);
expect(imageCache.pendingImageCount, 1);
await error.future; await error.future;
expect(imageCache.statusForKey(provider).untracked, true); expect(imageCache.containsKey(provider), false);
expect(imageCache.pendingImageCount, 0);
}, skip: isBrowser); }, skip: isBrowser);
test('AssetImageProvider - evicts on null load', () async { test('AssetImageProvider - evicts on null load', () async {
...@@ -75,18 +71,15 @@ void main() { ...@@ -75,18 +71,15 @@ void main() {
final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: TestAssetBundle()); final ImageProvider provider = ExactAssetImage('does-not-exist', bundle: TestAssetBundle());
final Object key = await provider.obtainKey(ImageConfiguration.empty); final Object key = await provider.obtainKey(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).untracked, true); expect(imageCache.containsKey(key), false);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty); provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(key).pending, true); expect(imageCache.containsKey(key), true);
expect(imageCache.pendingImageCount, 1);
await error.future; await error.future;
expect(imageCache.statusForKey(provider).untracked, true); expect(imageCache.containsKey(key), false);
expect(imageCache.pendingImageCount, 0);
}, skip: isBrowser); }, skip: isBrowser);
test('ImageProvider can evict images', () async { test('ImageProvider can evict images', () async {
...@@ -158,17 +151,6 @@ void main() { ...@@ -158,17 +151,6 @@ void main() {
expect(await caughtError.future, true); expect(await caughtError.future, true);
}); });
test('obtainKey errors will be caught - check location', () async {
final ImageProvider imageProvider = ObtainKeyErrorImageProvider();
final Completer<bool> caughtError = Completer<bool>();
FlutterError.onError = (FlutterErrorDetails details) {
caughtError.complete(true);
};
await imageProvider.obtainCacheStatus(configuration: ImageConfiguration.empty);
expect(await caughtError.future, true);
});
test('resolve sync errors will be caught', () async { test('resolve sync errors will be caught', () async {
bool uncaught = false; bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification( final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
...@@ -218,26 +200,24 @@ void main() { ...@@ -218,26 +200,24 @@ void main() {
test('File image with empty file throws expected error and evicts from cache', () async { test('File image with empty file throws expected error and evicts from cache', () async {
final Completer<StateError> error = Completer<StateError>(); final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) { FlutterError.onError = (FlutterErrorDetails details) {
print(details.exception);
error.complete(details.exception as StateError); error.complete(details.exception as StateError);
}; };
final MemoryFileSystem fs = MemoryFileSystem(); final MemoryFileSystem fs = MemoryFileSystem();
final File file = fs.file('/empty.png')..createSync(recursive: true); final File file = fs.file('/empty.png')..createSync(recursive: true);
final FileImage provider = FileImage(file); final FileImage provider = FileImage(file);
expect(imageCache.statusForKey(provider).untracked, true); expect(imageCache.containsKey(provider), false);
expect(imageCache.pendingImageCount, 0);
provider.resolve(ImageConfiguration.empty); provider.resolve(ImageConfiguration.empty);
expect(imageCache.statusForKey(provider).pending, true); expect(imageCache.containsKey(provider), true);
expect(imageCache.pendingImageCount, 1);
expect(await error.future, isStateError); expect(await error.future, isStateError);
expect(imageCache.statusForKey(provider).untracked, true); expect(imageCache.containsKey(provider), false);
expect(imageCache.pendingImageCount, 0);
}); });
group('NetworkImage', () { group(NetworkImage, () {
MockHttpClient httpClient; MockHttpClient httpClient;
setUp(() { setUp(() {
...@@ -262,13 +242,11 @@ void main() { ...@@ -262,13 +242,11 @@ void main() {
final Completer<dynamic> caughtError = Completer<dynamic>(); final Completer<dynamic> caughtError = Completer<dynamic>();
final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl)); final ImageProvider imageProvider = NetworkImage(nonconst(requestUrl));
expect(imageCache.pendingImageCount, 0); expect(imageCache.containsKey(imageProvider), false);
expect(imageCache.statusForKey(imageProvider).untracked, true);
final ImageStream result = imageProvider.resolve(ImageConfiguration.empty); final ImageStream result = imageProvider.resolve(ImageConfiguration.empty);
expect(imageCache.pendingImageCount, 1); expect(imageCache.containsKey(imageProvider), true);
expect(imageCache.statusForKey(imageProvider).pending, true);
result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) { result.addListener(ImageStreamListener((ImageInfo info, bool syncCall) {
}, onError: (dynamic error, StackTrace stackTrace) { }, onError: (dynamic error, StackTrace stackTrace) {
...@@ -277,8 +255,7 @@ void main() { ...@@ -277,8 +255,7 @@ void main() {
final dynamic err = await caughtError.future; final dynamic err = await caughtError.future;
expect(imageCache.pendingImageCount, 0); expect(imageCache.containsKey(imageProvider), false);
expect(imageCache.statusForKey(imageProvider).untracked, true);
expect( expect(
err, err,
......
...@@ -8,7 +8,6 @@ import 'dart:ui' as ui; ...@@ -8,7 +8,6 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -24,18 +23,6 @@ Future<ui.Image> createTestImage([List<int> bytes = kTransparentImage]) async { ...@@ -24,18 +23,6 @@ Future<ui.Image> createTestImage([List<int> bytes = kTransparentImage]) async {
} }
void main() { void main() {
int originalCacheSize;
setUp(() {
originalCacheSize = imageCache.maximumSize;
imageCache.clear();
imageCache.clearLiveImages();
});
tearDown(() {
imageCache.maximumSize = originalCacheSize;
});
testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async { testWidgets('Verify Image resets its RenderImage when changing providers', (WidgetTester tester) async {
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider(); final TestImageProvider imageProvider1 = TestImageProvider();
...@@ -776,7 +763,7 @@ void main() { ...@@ -776,7 +763,7 @@ void main() {
expect(isSync, isTrue); expect(isSync, isTrue);
}); });
testWidgets('Precache removes original listener immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async { testWidgets('Precache remove listeners immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter(); final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
final TestImageProvider provider = TestImageProvider(streamCompleter: imageStreamCompleter); final TestImageProvider provider = TestImageProvider(streamCompleter: imageStreamCompleter);
...@@ -1377,197 +1364,6 @@ void main() { ...@@ -1377,197 +1364,6 @@ void main() {
expect(imageProviders.skip(309 - 15).every(loadCalled), true); expect(imageProviders.skip(309 - 15).every(loadCalled), true);
expect(imageProviders.take(309 - 15).every(loadNotCalled), true); expect(imageProviders.take(309 - 15).every(loadNotCalled), true);
}); });
testWidgets('Same image provider in multiple parts of the tree, no cache room left', (WidgetTester tester) async {
imageCache.maximumSize = 0;
final ui.Image image = await tester.runAsync(createTestImage);
final TestImageProvider provider1 = TestImageProvider();
final TestImageProvider provider2 = TestImageProvider();
expect(provider1.loadCallCount, 0);
expect(provider2.loadCallCount, 0);
expect(imageCache.liveImageCount, 0);
await tester.pumpWidget(Column(
children: <Widget>[
Image(image: provider1),
Image(image: provider2),
Image(image: provider1),
Image(image: provider1),
Image(image: provider2),
],
));
expect(imageCache.liveImageCount, 2);
expect(imageCache.statusForKey(provider1).live, true);
expect(imageCache.statusForKey(provider1).pending, false);
expect(imageCache.statusForKey(provider1).keepAlive, false);
expect(imageCache.statusForKey(provider2).live, true);
expect(imageCache.statusForKey(provider2).pending, false);
expect(imageCache.statusForKey(provider2).keepAlive, false);
expect(provider1.loadCallCount, 1);
expect(provider2.loadCallCount, 1);
provider1.complete(image);
await tester.idle();
provider2.complete(image);
await tester.idle();
expect(imageCache.liveImageCount, 2);
expect(imageCache.currentSize, 0);
await tester.pumpWidget(Image(image: provider2));
await tester.idle();
expect(imageCache.statusForKey(provider1).untracked, true);
expect(imageCache.statusForKey(provider2).live, true);
expect(imageCache.statusForKey(provider2).pending, false);
expect(imageCache.statusForKey(provider2).keepAlive, false);
expect(imageCache.liveImageCount, 1);
await tester.pumpWidget(const SizedBox());
await tester.idle();
expect(provider1.loadCallCount, 1);
expect(provider2.loadCallCount, 1);
expect(imageCache.liveImageCount, 0);
});
testWidgets('precacheImage does not hold weak ref for more than a frame', (WidgetTester tester) async {
imageCache.maximumSize = 0;
final TestImageProvider provider = TestImageProvider();
Future<void> precache;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
precache = precacheImage(provider, context);
return Container();
}
)
);
provider.complete();
await precache;
// Should have ended up with only a weak ref, not in cache because cache size is 0
expect(imageCache.liveImageCount, 1);
expect(imageCache.containsKey(provider), false);
final ImageCacheStatus providerLocation = await provider.obtainCacheStatus(configuration: ImageConfiguration.empty);
expect(providerLocation, isNotNull);
expect(providerLocation.live, true);
expect(providerLocation.keepAlive, false);
expect(providerLocation.pending, false);
// Check that a second resolve of the same image is synchronous.
expect(provider._lastResolvedConfiguration, isNotNull);
final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
bool isSync;
final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; });
// Still have live ref because frame has not pumped yet.
await tester.pump();
expect(imageCache.liveImageCount, 1);
SchedulerBinding.instance.scheduleFrame();
await tester.pump();
// Live ref should be gone - we didn't listen to the stream.
expect(imageCache.liveImageCount, 0);
expect(imageCache.currentSize, 0);
stream.addListener(listener);
expect(isSync, true); // because the stream still has the image.
expect(imageCache.liveImageCount, 0);
expect(imageCache.currentSize, 0);
expect(provider.loadCallCount, 1);
});
testWidgets('precacheImage allows time to take over weak refernce', (WidgetTester tester) async {
final TestImageProvider provider = TestImageProvider();
Future<void> precache;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
precache = precacheImage(provider, context);
return Container();
}
)
);
provider.complete();
await precache;
// Should have ended up in the cache and have a weak reference.
expect(imageCache.liveImageCount, 1);
expect(imageCache.currentSize, 1);
expect(imageCache.containsKey(provider), true);
// Check that a second resolve of the same image is synchronous.
expect(provider._lastResolvedConfiguration, isNotNull);
final ImageStream stream = provider.resolve(provider._lastResolvedConfiguration);
bool isSync;
final ImageStreamListener listener = ImageStreamListener((ImageInfo image, bool syncCall) { isSync = syncCall; });
// Should have ended up in the cache and still have a weak reference.
expect(imageCache.liveImageCount, 1);
expect(imageCache.currentSize, 1);
expect(imageCache.containsKey(provider), true);
stream.addListener(listener);
expect(isSync, true);
expect(imageCache.liveImageCount, 1);
expect(imageCache.currentSize, 1);
expect(imageCache.containsKey(provider), true);
SchedulerBinding.instance.scheduleFrame();
await tester.pump();
expect(imageCache.liveImageCount, 1);
expect(imageCache.currentSize, 1);
expect(imageCache.containsKey(provider), true);
stream.removeListener(listener);
expect(imageCache.liveImageCount, 0);
expect(imageCache.currentSize, 1);
expect(imageCache.containsKey(provider), true);
expect(provider.loadCallCount, 1);
});
testWidgets('evict an image during precache', (WidgetTester tester) async {
// This test checks that the live image tracking does not hold on to a
// pending image that will never complete because it has been evicted from
// the cache.
// The scenario may arise in a test harness that is trying to load real
// images using `tester.runAsync()`, and wants to make sure that widgets
// under test have not also tried to resolve the image in a FakeAsync zone.
// The image loaded in the FakeAsync zone will never complete, and the
// runAsync call wants to make sure it gets a load attempt from the correct
// zone.
final Uint8List bytes = Uint8List.fromList(kTransparentImage);
final MemoryImage provider = MemoryImage(bytes);
await tester.runAsync(() async {
final List<Future<void>> futures = <Future<void>>[];
await tester.pumpWidget(Builder(builder: (BuildContext context) {
futures.add(precacheImage(provider, context));
imageCache.evict(provider);
futures.add(precacheImage(provider, context));
return const SizedBox.expand();
}));
await Future.wait<void>(futures);
expect(imageCache.statusForKey(provider).keepAlive, true);
expect(imageCache.statusForKey(provider).live, true);
// Schedule a frame to get precacheImage to stop listening.
SchedulerBinding.instance.scheduleFrame();
await tester.pump();
expect(imageCache.statusForKey(provider).keepAlive, true);
expect(imageCache.statusForKey(provider).live, false);
});
});
} }
class ConfigurationAwareKey { class ConfigurationAwareKey {
...@@ -1609,9 +1405,8 @@ class TestImageProvider extends ImageProvider<Object> { ...@@ -1609,9 +1405,8 @@ class TestImageProvider extends ImageProvider<Object> {
ImageStreamCompleter _streamCompleter; ImageStreamCompleter _streamCompleter;
ImageConfiguration _lastResolvedConfiguration; ImageConfiguration _lastResolvedConfiguration;
bool get loadCalled => _loadCallCount > 0; bool get loadCalled => _loadCalled;
int get loadCallCount => _loadCallCount; bool _loadCalled = false;
int _loadCallCount = 0;
@override @override
Future<Object> obtainKey(ImageConfiguration configuration) { Future<Object> obtainKey(ImageConfiguration configuration) {
...@@ -1626,13 +1421,12 @@ class TestImageProvider extends ImageProvider<Object> { ...@@ -1626,13 +1421,12 @@ class TestImageProvider extends ImageProvider<Object> {
@override @override
ImageStreamCompleter load(Object key, DecoderCallback decode) { ImageStreamCompleter load(Object key, DecoderCallback decode) {
_loadCallCount += 1; _loadCalled = true;
return _streamCompleter; return _streamCompleter;
} }
void complete([ui.Image image]) { void complete() {
image ??= TestImage(); _completer.complete(ImageInfo(image: TestImage()));
_completer.complete(ImageInfo(image: image));
} }
void fail(dynamic exception, StackTrace stackTrace) { void fail(dynamic exception, StackTrace stackTrace) {
......
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