Unverified Commit e2dcdb60 authored by Dan Field's avatar Dan Field Committed by GitHub

Live image cache (#51249)

* Revert "Revert "Live image cache (#50318)" (#51131)"

This reverts commit 2f09d601.

* Fix eviction of a pending image
parent 9ba4eb04
......@@ -20,7 +20,7 @@ void main() {
final developer.ServiceProtocolInfo info = await developer.Service.getInfo();
if (info.serverUri == null) {
throw TestFailure('This test _must_ be run with --enable-vmservice.');
fail('This test _must_ be run with --enable-vmservice.');
}
await timelineObtainer.connect(info.serverUri);
await timelineObtainer.setDartFlags();
......@@ -58,7 +58,8 @@ void main() {
'name': 'ImageCache.clear',
'args': <String, dynamic>{
'pendingImages': 1,
'cachedImages': 0,
'keepAliveImages': 0,
'liveImages': 1,
'currentSizeInBytes': 0,
'isolateId': isolateId,
}
......@@ -149,7 +150,7 @@ class TimelineObtainer {
Future<void> close() async {
expect(_completers, isEmpty);
await _observatorySocket.close();
await _observatorySocket?.close();
}
}
......
......@@ -96,6 +96,7 @@ mixin PaintingBinding on BindingBase, ServicesBinding {
void evict(String asset) {
super.evict(asset);
imageCache.clear();
imageCache.clearLiveImages();
}
/// Listenable that notifies when the available fonts on the system have
......
......@@ -17,6 +17,12 @@ import 'binding.dart';
import 'image_cache.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
/// select a specific image.
///
......@@ -318,7 +324,28 @@ abstract class ImageProvider<T> {
final ImageStream stream = createStream(configuration);
// Load the key (potentially asynchronously), set up an error handling zone,
// and call resolveStreamForKey.
_createErrorHandlerAndKey(configuration, stream);
_createErrorHandlerAndKey(
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;
}
......@@ -332,30 +359,66 @@ abstract class ImageProvider<T> {
return ImageStream();
}
void _createErrorHandlerAndKey(ImageConfiguration configuration, ImageStream stream) {
/// Returns the cache location for the key that this [ImageProvider] creates.
///
/// 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(stream != null);
final Completer<ImageCacheStatus> completer = Completer<ImageCacheStatus>();
_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;
bool didError = false;
Future<void> handleError(dynamic exception, StackTrace stack) async {
if (didError) {
return;
}
if (!didError) {
errorCallback(obtainedKey, exception, stack);
}
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
......@@ -384,7 +447,7 @@ abstract class ImageProvider<T> {
key.then<void>((T key) {
obtainedKey = key;
try {
resolveStreamForKey(configuration, stream, key, handleError);
successCallback(key, handleError);
} catch (error, stackTrace) {
handleError(error, stackTrace);
}
......
......@@ -203,6 +203,11 @@ class ImageChunkEvent extends Diagnosticable {
///
/// 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:
///
/// * [ImageProvider], which has an example that includes the use of an
......@@ -392,6 +397,23 @@ abstract class ImageStreamCompleter extends Diagnosticable {
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.
......
......@@ -8,6 +8,7 @@ import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/semantics.dart';
......@@ -65,7 +66,30 @@ ImageConfiguration createLocalImageConfiguration(BuildContext context, { Size si
/// 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
/// to use the same [ImageProvider] instance. The [ImageCache] will find the image
/// as long as both images share the same key.
/// 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 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
/// (see [createLocalImageConfiguration]).
......@@ -91,7 +115,12 @@ Future<void> precacheImage(
if (!completer.isCompleted) {
completer.complete();
}
stream.removeListener(listener);
// Give callers until at least the end of the frame to subscribe to the
// image stream.
// See ImageCache._liveImages
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
stream.removeListener(listener);
});
},
onError: (dynamic exception, StackTrace stackTrace) {
if (!completer.isCompleted) {
......
......@@ -3,7 +3,10 @@
// found in the LICENSE file.
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/widgets.dart';
import 'package:flutter/painting.dart';
......@@ -24,4 +27,88 @@ void main() {
});
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,13 +9,14 @@ import '../rendering/rendering_tester.dart';
import 'mocks_for_image_cache.dart';
void main() {
group(ImageCache, () {
group('ImageCache', () {
setUpAll(() {
TestRenderingFlutterBinding(); // initializes the imageCache
});
tearDown(() {
imageCache.clear();
imageCache.clearLiveImages();
imageCache.maximumSize = 1000;
imageCache.maximumSizeBytes = 10485760;
});
......@@ -169,7 +170,14 @@ void main() {
return completer1;
}) as TestImageStreamCompleter;
expect(imageCache.statusForKey(testImage).pending, true);
expect(imageCache.statusForKey(testImage).live, true);
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, () {
return completer2;
......@@ -240,7 +248,106 @@ void main() {
expect(resultingCompleter1, completer1);
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);
});
});
}
......@@ -111,6 +111,17 @@ void main() {
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 {
bool uncaught = false;
final Zone testZone = Zone.current.fork(specification: ZoneSpecification(
......@@ -160,7 +171,6 @@ void main() {
test('File image with empty file throws expected error - (image cache)', () async {
final Completer<StateError> error = Completer<StateError>();
FlutterError.onError = (FlutterErrorDetails details) {
print(details.exception);
error.complete(details.exception as StateError);
};
final MemoryFileSystem fs = MemoryFileSystem();
......@@ -172,7 +182,7 @@ void main() {
expect(await error.future, isStateError);
});
group(NetworkImage, () {
group('NetworkImage', () {
MockHttpClient httpClient;
setUp(() {
......
......@@ -8,6 +8,7 @@ import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -23,6 +24,18 @@ Future<ui.Image> createTestImage([List<int> bytes = kTransparentImage]) async {
}
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 {
final GlobalKey key = GlobalKey();
final TestImageProvider imageProvider1 = TestImageProvider();
......@@ -763,7 +776,7 @@ void main() {
expect(isSync, isTrue);
});
testWidgets('Precache remove listeners immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async {
testWidgets('Precache removes original listener immediately after future completes, does not crash on successive calls #25143', (WidgetTester tester) async {
final TestImageStreamCompleter imageStreamCompleter = TestImageStreamCompleter();
final TestImageProvider provider = TestImageProvider(streamCompleter: imageStreamCompleter);
......@@ -1364,6 +1377,197 @@ void main() {
expect(imageProviders.skip(309 - 15).every(loadCalled), 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 {
......@@ -1405,8 +1609,9 @@ class TestImageProvider extends ImageProvider<Object> {
ImageStreamCompleter _streamCompleter;
ImageConfiguration _lastResolvedConfiguration;
bool get loadCalled => _loadCalled;
bool _loadCalled = false;
bool get loadCalled => _loadCallCount > 0;
int get loadCallCount => _loadCallCount;
int _loadCallCount = 0;
@override
Future<Object> obtainKey(ImageConfiguration configuration) {
......@@ -1421,12 +1626,13 @@ class TestImageProvider extends ImageProvider<Object> {
@override
ImageStreamCompleter load(Object key, DecoderCallback decode) {
_loadCalled = true;
_loadCallCount += 1;
return _streamCompleter;
}
void complete() {
_completer.complete(ImageInfo(image: TestImage()));
void complete([ui.Image image]) {
image ??= TestImage();
_completer.complete(ImageInfo(image: image));
}
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